29

我的目标是观察输入值并在其值以编程方式更改时触发处理程序。我只需要现代浏览器。

我已经尝试了很多组合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

这似乎符合我的预期,但感觉就像是一种 hack,因为我正在覆盖现有的 value 属性并使用value(属性和属性)的双重状态。它还破坏了change似乎不喜欢修改后的属性的事件。

我的其他尝试:

  • 一个 setTimeout/setInterval 循环,但这也不干净
  • 各种watchobservepolyfill,但它们因输入值属性而中断

什么是达到相同结果的正确方法?

现场演示:http: //jsfiddle.net/L7Emx/4/

[编辑]澄清一下:我的代码正在监视其他应用程序可以推送更新的输入元素(例如,由于 ajax 调用,或由于其他字段的更改)。我无法控制其他应用程序如何推送更新,我只是一个观察者。

[编辑 2] 为了澄清我所说的“现代浏览器”的含义,我对适用于 IE 11 和 Chrome 30 的解决方案非常满意。

[更新]根据接受的答案更新了演示:http: //jsfiddle.net/L7Emx/10/

@mohit-jain 建议的技巧是为用户交互添加第二个输入。

4

7 回答 7

17

如果您的解决方案的唯一问题是破坏值集上的更改事件。然后您可以在现场手动触发该事件。(但如果用户通过浏览器更改输入,这不会监控设置 - 请参阅下面的编辑

<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>

更多关于手动触发事件


编辑

手动事件触发和 mutator 方法的问题是当用户从浏览器更改字段值时 value 属性不会改变。解决方法是使用两个字段。一个隐藏的,我们可以与之进行程序化交互。另一个是可见的,用户可以与之交互。经过这种考虑方法很简单。

  1. 在隐藏的输入字段上改变 value 属性以观察更改并触发手动 onchange 事件。在设置值上更改可见字段的值以提供用户反馈。
  2. 在可见字段值更改时更新观察者隐藏的值。
于 2013-10-15T10:12:17.860 回答
2

以下在我尝试过的所有地方都有效,包括 IE11(甚至低至 IE9 仿真模式)。

通过在定义setter 的输入元素原型链中查找对象并修改此 setter 以触发事件(我在示例中已调用它),同时仍保持旧行为,这使您的defineProperty想法更进一步。.valuemodified

当您运行下面的代码片段时,您可以在文本输入框中键入/粘贴/诸如此类,也可以单击附加" more"到输入元素的.value. 无论哪种情况,<span>的内容都会同步更新。

这里唯一没有处理的是由设置属性引起的更新。如果需要,您可以使用 a 来处理,但请注意和属性MutationObserver之间没有一对一的关系(后者只是前者的默认值)。.valuevalue

// 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>

于 2017-10-05T10:42:36.460 回答
1

由于您已经在使用 polyfills 进行观察/观察等,让我借此机会向您推荐 Angularjs

它以 ng-models 的形式提供了这个功能。您可以将观察者放在模型的值上,当它发生变化时,您可以调用其他函数。

这是您想要的一个非常简单但有效的解决方案:

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');
  }
});
于 2013-10-14T16:52:29.810 回答
1
于 2013-10-14T17:04:46.537 回答
0

我不久前写了以下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的属性上设置一个假,并在由.valuevalueonpropertychange

于 2013-10-20T19:51:31.537 回答
0

有一种方法可以做到这一点。对此没有 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/

于 2013-10-15T10:07:38.413 回答
0

这是一个老问题,但使用新的 JS 代理对象,在值更改时触发事件非常容易:

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">

这会根据原始输入 DOM 对象创建一个 JS 代理对象。您可以像与原始 DOM 对象交互一样与代理对象交互。不同之处在于我们已经覆盖了 setter。当 'value' 属性改变时,我们会执行正常的、预期的操作,obj[prop] = value但是如果属性的值为 '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

于 2019-06-11T21:07:36.853 回答