程序化更改输入值时触发操作

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循环,但这也不够干净
  • 各种watch和observe polyfill,但它们会破坏输入值属性

有没有更好的方法来实现同样的结果呢?

在线演示:http://jsfiddle.net/L7Emx/4/

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

[编辑2] 澄清一下我所说的“现代浏览器”,我希望能在IE 11和Chrome 30上运行的解决方案。

[更新] 基于已接受答案的演示:http://jsfiddle.net/L7Emx/10/

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


你是一直在做这个更改还是在观察别人做更改? - stevemarvell
@stevemarvell 我的代码正在监视一个输入元素,其他应用程序可以向其推送更新(例如,通过ajax调用或其他字段的更改)。 - Christophe
我认为你最好的解决方案是实现一个监控它的超时机制。 - stevemarvell
输入值取决于您是使用onLoad还是No wrap - in <head>。 - X-Pippes
1
@Qantas94Heavy 在我的演示中:如果你在输入字段中输入了什么,当失去焦点时,change事件将触发,但无法访问你刚才输入的值。 - Christophe
显示剩余3条评论
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>

更多关于手动触发事件的信息


编辑

手动触发事件和变异器方法的问题在于当用户从浏览器更改字段值时,值属性不会更改。解决方法是使用两个字段。一个是隐藏的,可以进行编程交互。另一个可见,用户可以进行交互。考虑到这一点,方法足够简单。

  1. 突变隐藏输入字段上的值属性以观察更改并手动触发 onchange 事件。在设置值时,更改可见字段的值以向用户提供反馈。
  2. 在可见字段值更改时更新观察者的隐藏值。

问题在于,如果您使用此方法,就不能再使用更改事件来确定用户输入的内容。这就是我提出问题的原因(参见演示)。 - Christophe
好的,请记住我只是一个观察者。我可以为自己的需求创建第二个输入,但不能替换现有的输入。 - Christophe
那会让事情变得复杂。更改现有字段的类型是否是一个难以修复的规则?检查答案。看看是否可以接受。可以使用简单的jQuery代码来更改现有字段的类型并放置一个新字段。 - Mohit
更新后的代码存在问题,它无法看到编程更改,比如document.getElementById("myInputVisible").value="New Value"。 - Christophe
是的,它可以。同时为了未来的读者,请放置一个测试程序更改的方法。 - Mohit
显示剩余5条评论

2
以下代码在我尝试过的所有地方都有效,包括IE11(甚至是IE9仿真模式)。
它通过找到输入元素原型链中定义.value setter的对象并修改此setter来触发事件(在示例中我称其为modified),同时保持旧行为,进一步实现了您的defineProperty想法。
当您运行下面的代码片段时,您可以在文本输入框中键入/粘贴/输入等内容,或者单击将"more"附加到输入元素的.value的按钮。无论哪种情况,的内容都会同步更新。
唯一没有处理的是由设置属性引起的更新。如果您想要处理它,可以使用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>


1

既然您已经在使用watch/observe等polyfill,那么我想借此机会向您推荐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');
  }
});

1
@Hash 我肯定会对Angularjs如何实现它感兴趣(请注意我刚刚添加的悬赏,我正在寻找“详细的规范答案”)。 - Christophe
@Christophe:我的演示程序确实可以通过编程方式更改输入值。当您单击“更改”时,与输入绑定的模型的值会更改。这是编程方式的。您也可以直接在代码中进行更改,但您所做的方式在Angularjs中不正确。对模型的任何更改都必须在控制器内进行。这些都是您在使用Angular时需要学习的一些内容。请参阅此更新的fiddle:http://jsfiddle.net/RedDevil/jv8pK/4/ - kumarharsh
顺便提一下,我建议你尝试一下Angular的教程,这样理解起来会更容易一些:http://docs.angularjs.org/tutorial/ - kumarharsh
@Harsh:我认为问题实际上是如何在不修改现有代码库的情况下监听更改 - 除了Angular之外,还有许多其他(更简单)的通知框架。 - Bergi
@Harsh 我明白了。请看我在问题中的编辑,这实际上是在评论中提到的。我现在已经将其移动到问题本身,希望它能够澄清目标。 - Christophe
显示剩余2条评论

1

我只需要适用于现代浏览器的版本。

你想要多先进的版本?Ecma Script 7(6将于12月份正式发布)可能包含Object.observe。这将允许您创建本地可观察对象。是的,您可以运行它!怎么做?

要尝试此功能,您需要在Chrome Canary中启用“启用实验性JavaScript”标志并重新启动浏览器。该标志可以在'about:flags’下找到。

更多信息:阅读此内容

所以,这是高度实验性的,在当前一组浏览器中还没有准备好。而且,它仍然不完全准备好,并且尚不确定是否会出现在ES7中,甚至ES7的最终日期也没有确定。但是,我想让您知道以备将来使用。


+1 谢谢,知道了!但是这个肯定太“现代化”了...我需要支持至少IE 11。 - Christophe

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/


1
这对我不起作用。来自 MDN 网站:警告:通常情况下,应尽可能避免使用 watch() 和 unwatch()。这两种方法仅在 Gecko 中实现,并且主要用于调试。来源:Mozilla 开发者网络 - Christophe
是的,你说得对,但在我的链接中,他使用setInterval编写了自己的watch和unwatch方法。你试过了吗? - Donovan Charpin
1
抱歉,我错过了上一个链接。是的,我尝试过这个方法,但我的希望是得到比setInterval更好的解决方案(请参考悬赏问题和评论)。 - Christophe

0
我一段时间前写了以下Gist,它可以跨浏览器(包括IE8+)监听自定义事件。
看看我如何在IE8上监听onpropertychange
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的解决方案是否适用于跨浏览器,但您可以在输入框的value属性上设置一个虚假的eventlistener,并在由onpropertychange触发的value值更改时运行回调函数。


不幸的是,这对于像IE 11或Chrome这样的浏览器上的编程更改是行不通的。 - Christophe

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,但是如果prop的值为“value”,则会触发自定义InputEvent。

请注意,您可以使用new CustomEventnew Event触发任何类型的事件。 "change"可能更合适。

在此处阅读有关代理和自定义事件的更多信息: https://developer.mozilla.org/zh-CN/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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接