Инициировать действие при программном изменении входного значения

Моя цель — наблюдать за входным значением и запускать обработчик, когда его значение изменяется программно. Мне это нужно только для современных браузеров.

Я пробовал много комбинаций, используя defineProperty, и это моя последняя итерация:

var myInput=document.getElementById("myInput");
Object.defineProperty(myInput,"value",{
    get:function(){
        return this.getAttribute("value");
    },
    set:function(val){
        console.log("set");
        // handle value change here
        this.setAttribute("value",val);
    }
});
myInput.value="new value"; // should trigger console.log and handler

Кажется, это делает то, что я ожидаю, но это похоже на взлом, поскольку я переопределяю существующее свойство значения и играю с двойным статусом value (атрибут и свойство). Это также прерывает событие change, которому, похоже, не нравится измененное свойство.

Другие мои попытки:

  • цикл setTimeout/setInterval, но это тоже не чисто
  • различные полифилы watch и observe, но они ломаются из-за свойства входного значения

Что было бы правильным способом достижения того же результата?

Живая демонстрация: http://jsfiddle.net/L7Emx/4/

[Изменить] Чтобы уточнить: мой код наблюдает за элементом ввода, где другие приложения могут отправлять обновления (например, в результате вызовов ajax или в результате изменений в других полях). Я не могу контролировать, как другие приложения загружают обновления, я просто наблюдатель.

[Редактировать 2] Чтобы уточнить, что я имею в виду под «современным браузером», я был бы очень доволен решением, которое работает в IE 11 и Chrome 30.

[Обновление] Демонстрация обновлена на основе принятого ответа: http://jsfiddle.net/L7Emx/10/< /а>

Трюк, предложенный @mohit-jain, состоит в том, чтобы добавить второй вход для взаимодействия с пользователем.


person Christophe    schedule 11.10.2013    source источник
comment
Вы всегда делаете изменения или наблюдаете за тем, как кто-то другой их меняет?   -  person stevemarvell    schedule 12.10.2013
comment
@stevemarvell Мой код отслеживает элемент ввода, куда другие приложения могут отправлять обновления (например, в результате вызовов ajax или в результате изменений в других полях).   -  person Christophe    schedule 12.10.2013
comment
Я думаю, что ваше лучшее решение - реализовать тайм-аут, который его отслеживает.   -  person stevemarvell    schedule 12.10.2013
comment
Обратите внимание, что изменяемый вами атрибут value эквивалентен свойству .defaultValue. Вместо [sg]etAttribute вы должны использовать переменную закрытия   -  person Bergi    schedule 14.10.2013
comment
значение ввода зависит от того, используете ли вы onLoad или No wrap - в ‹head›.   -  person X-Pippes    schedule 14.10.2013
comment
@Bergi На самом деле я сделал это специально, чтобы значение возвращало атрибут значения при первом доступе к нему. Я никогда раньше не слышал о свойстве defaultValue, мне нужно изучить это.   -  person Christophe    schedule 14.10.2013
comment
Теперь, как прерывается событие изменения?   -  person Qantas 94 Heavy    schedule 15.10.2013
comment
@ Qantas94Heavy в моей демонстрации: если вы что-то введете в поле ввода, при размытии сработает событие изменения, но вы не сможете получить доступ к только что введенному значению.   -  person Christophe    schedule 15.10.2013


Ответы (7)


если единственная проблема с вашим решением - нарушение события изменения в наборе значений. Затем вы можете запустить это событие вручную на съемочной площадке. (Но это не будет отслеживать установку, если пользователь изменит ввод через браузер - см. изменить ниже)

<html>
  <body>
    <input type='hidden' id='myInput' />
    <input type='text' id='myInputVisible' />
    <input type='button' value='Test' onclick='return testSet();'/>
    <script>
      //hidden input which your API will be changing
      var myInput=document.getElementById("myInput");
      //visible input for the users
      var myInputVisible=document.getElementById("myInputVisible");
      //property mutation for hidden input
      Object.defineProperty(myInput,"value",{
        get:function(){
          return this.getAttribute("value");
        },
        set:function(val){
          console.log("set");

          //update value of myInputVisible on myInput set
          myInputVisible.value = val;

          // handle value change here
          this.setAttribute("value",val);

          //fire the event
          if ("createEvent" in document) {  // Modern browsers
            var evt = document.createEvent("HTMLEvents");
            evt.initEvent("change", true, false);
            myInput.dispatchEvent(evt);
          }
          else {  // IE 8 and below
            var evt = document.createEventObject();
            myInput.fireEvent("onchange", evt);
          }
        }
      });  

      //listen for visible input changes and update hidden
      myInputVisible.onchange = function(e){
        myInput.value = myInputVisible.value;
      };

      //this is whatever custom event handler you wish to use
      //it will catch both the programmatic changes (done on myInput directly)
      //and user's changes (done on myInputVisible)
      myInput.onchange = function(e){
        console.log(myInput.value);
      };

      //test method to demonstrate programmatic changes 
      function testSet(){
        myInput.value=Math.floor((Math.random()*100000)+1);
      }
    </script>
  </body>
</html>

подробнее о запуске событий вручную


ИЗМЕНИТЬ:

Проблема с запуском события вручную и подходом с мутатором заключается в том, что свойство value не изменится, когда пользователь изменит значение поля из браузера. обходной путь заключается в использовании двух полей. один скрытый, с которым мы можем программно взаимодействовать. Виден другой, с которым пользователь может взаимодействовать. После этого рассмотрения подход достаточно прост.

  1. свойство mutate value в скрытом поле ввода, чтобы наблюдать за изменениями и запускать событие onchange вручную. при установленном значении измените значение видимого поля, чтобы дать пользователю обратную связь.
  2. при изменении значения видимого поля обновить значение скрытого для наблюдателя.
person Mohit    schedule 15.10.2013
comment
мы получаем событие изменения для этого. которые можно использовать для определения поведения пользователей. мы пытаемся достичь чего-то, что обычно не поддерживается. поля ввода предназначены для изменения своего значения без ведома программиста или пользователя. - person Mohit; 15.10.2013
comment
Проблема в том, что если вы используете этот метод, вы больше не можете использовать событие изменения, чтобы выяснить, что ввел пользователь. В этом суть моего вопроса (см. демо). - person Christophe; 15.10.2013
comment
это там. но есть обходной путь. вы можете использовать пару входов. один скрытый и один видимый. редактирование ответа. - person Mohit; 15.10.2013
comment
Хорошо, просто помните, что я всего лишь наблюдатель. Я могу создать второй вход для своих нужд, но не заменить существующий. - person Christophe; 15.10.2013
comment
это усложнит дело. Это правило жесткого исправления, согласно которому вы не можете изменить тип существующего поля? проверьте ответ. посмотрите, приемлемо ли это. простой код jquery можно использовать для изменения типа существующего поля и размещения нового на его месте. - person Mohit; 15.10.2013
comment
Таким образом, проблема с обновленным кодом заключается в том, что он не увидит программных изменений, таких как document.getElementById(myInputVisible).value=New Value - person Christophe; 15.10.2013
comment
ну вот почему пара. наблюдайте за изменениями в скрытом поле. там вы можете отслеживать как программные изменения, так и изменения пользователя. ваше фактическое поле скрыто. visible - это просто обертка или контейнер, я должен сказать. см. метод testSet. - person Mohit; 15.10.2013
comment
Итак, мы говорим, что я должен сделать исходное поле скрытым (оно по-прежнему будет фиксировать программные изменения) и предоставить пользователю клон, который будет фиксировать пользовательские записи. Позвольте мне поэкспериментировать с этим, звучит как что-то, что может сработать! - person Christophe; 15.10.2013
comment
да, это именно то, что я думаю, должно работать. таким образом вы можете легко установить метод для мониторинга любого поля ввода. ну хоть текст разок. - person Mohit; 15.10.2013
comment
Да, это так. поместите метод для тестирования программных изменений, а также. для будущих читателей. - person Mohit; 15.10.2013

Следующее работает везде, где я его пробовал, включая IE11 (даже в режиме эмуляции IE9).

Это продвигает вашу идею defineProperty немного дальше, находя объект в цепочке прототипов элемента ввода, который определяет установщик .value, и модифицируя этот установщик для запуска события (в примере я назвал его modified), сохраняя при этом старое поведение.

Когда вы запускаете приведенный ниже фрагмент, вы можете ввести / вставить / еще что-то в поле ввода текста или нажать кнопку, которая добавляет " more" к .value элемента ввода. В любом случае содержимое <span> обновляется синхронно.

Единственное, что здесь не обрабатывается, — это обновление, вызванное установкой атрибута. Вы можете справиться с этим с помощью MutationObserver, если хотите, но обратите внимание, что между .value и атрибутом value нет однозначной связи (последний является просто значением по умолчанию для первого).

// make all input elements trigger an event when programmatically setting .value
monkeyPatchAllTheThings();                

var input = document.querySelector("input");
var span = document.querySelector("span");

function updateSpan() {
    span.textContent = input.value;
}

// handle user-initiated changes to the value
input.addEventListener("input", updateSpan);

// handle programmatic changes to the value
input.addEventListener("modified", updateSpan);

// handle initial content
updateSpan();

document.querySelector("button").addEventListener("click", function () {
    input.value += " more";
});


function monkeyPatchAllTheThings() {

    // create an input element
    var inp = document.createElement("input");

    // walk up its prototype chain until we find the object on which .value is defined
    var valuePropObj = Object.getPrototypeOf(inp);
    var descriptor;
    while (valuePropObj && !descriptor) {
         descriptor = Object.getOwnPropertyDescriptor(valuePropObj, "value");
         if (!descriptor)
            valuePropObj = Object.getPrototypeOf(valuePropObj);
    }

    if (!descriptor) {
        console.log("couldn't find .value anywhere in the prototype chain :(");
    } else {
        console.log(".value descriptor found on", "" + valuePropObj);
    }

    // remember the original .value setter ...
    var oldSetter = descriptor.set;

    // ... and replace it with a new one that a) calls the original,
    // and b) triggers a custom event
    descriptor.set = function () {
        oldSetter.apply(this, arguments);

        // for simplicity I'm using the old IE-compatible way of creating events
        var evt = document.createEvent("Event");
        evt.initEvent("modified", true, true);
        this.dispatchEvent(evt);
    };

    // re-apply the modified descriptor
    Object.defineProperty(valuePropObj, "value", descriptor);
}
<input><br><br>
The input contains "<span></span>"<br><br>
<button>update input programmatically</button>

person balpha    schedule 05.10.2017

Мне это нужно только для современных браузеров.

Насколько современным вы хотели бы стать? Ecma Script 7 (окончательный вариант 6 будет сделан в декабре) может содержать Object.observe. Это позволит вам создавать собственные наблюдаемые объекты. И да, вы можете запустить его! Как?

Чтобы поэкспериментировать с этой функцией, вам нужно включить флаг «Включить экспериментальный JavaScript» в Chrome Canary и перезапустить браузер. Флаг можно найти под 'about:flags’

Дополнительная информация: прочитайте это.

Так что да, это очень экспериментально и не готово к текущему набору браузеров. Кроме того, он все еще не полностью готов и не на 100%, если он выйдет на ES7, а окончательная дата ES7 еще даже не установлена. Тем не менее, я хотел, чтобы вы знали для будущего использования.

person MarcoK    schedule 14.10.2013
comment
+1 Спасибо, приятно знать! Но определенно слишком современный на данный момент... Мне нужно поддерживать хотя бы IE 11. - person Christophe; 14.10.2013

Поскольку вы уже используете полифиллы для наблюдения/наблюдания и т. д., позвольте мне воспользоваться возможностью и предложить вам Angularjs.

Он предлагает именно эту функциональность в виде своих ng-моделей. Вы можете поместить наблюдателей на значение модели, и когда оно изменится, вы сможете вызывать другие функции.

Вот очень простое, но рабочее решение того, что вы хотите:

http://jsfiddle.net/RedDevil/jv8pK/

По сути, введите текст и привяжите его к модели:

<input type="text" data-ng-model="variable">

затем поместите наблюдатель за моделью angularjs на этот ввод в контроллере.

$scope.$watch(function() {
  return $scope.variable
}, function(newVal, oldVal) {
  if(newVal !== null) {
    window.alert('programmatically changed');
  }
});
person kumarharsh    schedule 14.10.2013
comment
@Hash Мне определенно было бы интересно узнать больше о том, как это делает Angularjs (обратите внимание на награду, которую я только что добавил, я ищу подробный канонический ответ). - person Christophe; 14.10.2013
comment
@Christophe: я сделал и добавил скрипт с некоторым базовым кодом, объясняющим основную логику. - person kumarharsh; 14.10.2013
comment
демо, кажется, не делает то, что я хочу. Вот моя попытка изменить значение программно: jsfiddle.net/jv8pK/1 - person Christophe; 14.10.2013
comment
@Christophe: Моя демонстрация программно изменяет входное значение. При нажатии кнопки Изменить значение модели, привязанной к входным данным, изменяется. Это программно. Вы также можете изменить его прямо в коде, но то, как вы это сделали, не подходит для Angularjs. Любое изменение модели должно происходить внутри контроллера. Вот некоторые вещи, которые вам придется изучить, когда вы будете использовать angular. См. эту обновленную скрипту: jsfiddle.net/RedDevil/jv8pK/4 - person kumarharsh; 14.10.2013
comment
Кстати, я бы порекомендовал вам попробовать учебник по angular, чтобы немного понять все это: docs.angularjs. орг/учебник - person kumarharsh; 14.10.2013
comment
@Harsh: я думаю, что вопрос на самом деле заключался в том, как прослушивать изменения без изменения существующей кодовой базы - было бы много других (и более простых) сред уведомлений, чем Angular. - person Bergi; 14.10.2013
comment
@ Жестко, я понимаю. Смотрите редактирование моего вопроса, это было на самом деле в комментариях. Теперь я переместил его к самому вопросу, надеюсь, он проясняет цель. - person Christophe; 15.10.2013

Есть способ сделать это. Для этого нет события DOM, однако есть событие javascript, которое срабатывает при изменении свойства объекта.

document.form1.textfield.watch("value", function(object, oldval, newval){})
                ^ Object watched  ^                      ^        ^
                                  |_ property watched    |        |
                                                         |________|____ old and new value

В обратном вызове вы можете делать что угодно.


В этом примере мы можем увидеть этот эффект (см. jsFiddle):

var obj = { prop: 123 };

obj.watch('prop', function(propertyName, oldValue, newValue){
    console.log('Old value is '+oldValue); // 123
    console.log('New value is '+newValue); // 456
});

obj.prop = 456;

При изменении obj активируется прослушиватель watch.

У вас есть дополнительная информация по этой ссылке: http://james.padolsey.com/javascript/monitoring-dom-properties/

person Donovan Charpin    schedule 15.10.2013
comment
Это не работает для меня. С сайта MDN: Предупреждение: по возможности следует избегать использования watch() и unwatch(). Эти два метода реализованы только в Gecko и предназначены в первую очередь для отладки. Источник: Сеть разработчиков Mozilla - person Christophe; 15.10.2013
comment
Да, вы правы, но в моей ссылке он пишет свой собственный метод watch и unwatch с setInterval? Ты пробовал это? - person Donovan Charpin; 15.10.2013
comment
Извините, я пропустил последнюю ссылку. Да, я пробовал это, и я надеюсь получить что-то лучше, чем setInterval (см. вопрос и комментарий от награды) - person Christophe; 15.10.2013

Некоторое время назад я написал следующий Gist, который позволяет прослушивать пользовательские события в разных браузерах (включая IE8+).

Посмотрите, как я слушаю onpropertychange в IE8.

util.listenToCustomEvents = function (event_name, callback) {
  if (document.addEventListener) {
    document.addEventListener(event_name, callback, false);
  } else {
    document.documentElement.attachEvent('onpropertychange', function (e) {
    if(e.propertyName == event_name) {
      callback();
    }
  }
};

Я не уверен, что решение IE8 работает в разных браузерах, но вы можете установить фальшивое eventlistener для свойства value вашего ввода и запустить обратный вызов, как только значение value изменится, вызванное onpropertychange.

person frequent    schedule 20.10.2013
comment
К сожалению, это не сработает для программных изменений в таких браузерах, как IE 11 или Chrome. - person Christophe; 21.10.2013

Это старый вопрос, но с новым объектом JS Proxy запуск события при изменении значения довольно прост:

let proxyInput = new Proxy(input, {
  set(obj, prop, value) {
    obj[prop] = value;
    if(prop === 'value'){
      let event = new InputEvent('input', {data: value})
      obj.dispatchEvent(event);
    }
    return true;
  }
})

input.addEventListener('input', $event => {
  output.value = `Input changed, new value: ${$event.data}`;
});

proxyInput.value = 'thing'
window.setTimeout(() => proxyInput.value = 'another thing', 1500);
<input id="input">
<output id="output">

Это создает прокси-объект JS на основе исходного входного объекта DOM. Вы можете взаимодействовать с прокси-объектом так же, как с исходным объектом DOM. Разница в том, что мы переопределили сеттер. Когда реквизит «значение» изменяется, мы выполняем обычную ожидаемую операцию obj[prop] = value, но затем, если значением реквизита является «значение», мы запускаем пользовательское событие InputEvent.

Обратите внимание, что вы можете запускать любое событие с помощью new CustomEvent или new Event. "изменение" может быть более подходящим.

Узнайте больше о прокси-серверах и пользовательских событиях здесь: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy

https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events

Caniuse: https://caniuse.com/#search=proxy

person SethWhite    schedule 11.06.2019