Trigger action on programmatic change to an input value
Asked Answered
B

7

29

My objective is to observe an input value and trigger a handler when its value gets changed programmatically. I only need it for modern browsers.

I have tried many combinations using defineProperty and this is my latest iteration:

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

This seems to do what I expect, but it feels like a hack as I am overriding the existing value property and playing with the dual status of value (attribute and property). It also breaks the change event that doesn't seem to like the modified property.

My other attempts:

  • a setTimeout/setInterval loop, but this is not clean either
  • various watch and observe polyfills, but they break for an input value property

What would be a proper way to achieve the same result?

Live demo: http://jsfiddle.net/L7Emx/4/

[Edit] To clarify: My code is watching an input element where other applications can push updates (as a result of ajax calls for example, or as a result of changes in other fields). I have no control on how the other applications push updates, I am just an observer.

[Edit 2] To clarify what I mean by "modern browser", I'd be very happy with a solution that works on IE 11 and Chrome 30.

[Update] Updated demo based on the accepted answer: http://jsfiddle.net/L7Emx/10/

The trick suggested by @mohit-jain is to add a second input for user interaction.

Bookmark answered 11/10, 2013 at 19:37 Comment(8)
is it always you doing the change or are you watching for someone else changing it?Wedekind
@Wedekind My code is watching an input element where other applications can push updates (as a result of ajax calls for example, or as a result of changes in other fields).Bookmark
I think your best solution is to implement a timeout which monitors itWedekind
Please notice that the value attribute which you're changing is equivalent to the .defaultValue property. Instead of [sg]etAttribute you should use a closure variableHein
the value of input depends if you use onLoad or No wrap - in <head>.Kostival
@Hein I actually did this on purpose, to make sure value returns the value attribute the first time it is accessed. I had never heard about the defaultValue property before, I need to look into this.Bookmark
Now how is the "change" event broken?Basanite
@Qantas94Heavy in my demo: if you type something in the input field, on blur the change event will trigger but can't access the value you just typed.Bookmark
T
17

if the only problem with your solution is breaking of change event on value set. thn you can fire that event manually on set. (But this wont monitor set in case a user makes a change to the input via browser -- see edit bellow)

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

more on firing events manually


EDIT:

The problem with manual event firing and the mutator approach is that the value property won't change when user changes the field value from browser. the work around is to use two fields. one hidden with which we can have programmatic interaction. Another is visible with which user can interact. After this consideration approach is simple enough.

  1. mutate value property on hidden input-field to observe the changes and fire manual onchange event. on set value change the value of visible field to give user feedback.
  2. on visible field value change update the value of hidden for observer.
Thuythuya answered 15/10, 2013 at 10:12 Comment(10)
we do get the change event for that. that can be used to figure out user behaviour. we are trying to achieve something which is not normally supported. input fields are meant to change their value without programmer or user knowing about it.Thuythuya
The issue is that if you use this method, you cannot use the change event anymore to figure out what the user entered. That's the point of my question (cf. demo).Bookmark
thats there. but there's a work around. you can use a pair of inputs. one hidden and one visible. editing the answer.Thuythuya
ok, just remember that I am only an observer. I can create a second input for my needs, but not replace the existing one.Bookmark
that will complicate things. Is it a hard fix rule that you cant change the type of existing field? check the answer. see if thats acceptable. a simple jquery code can be used for changing the type of the existing field and put a new one in its place.Thuythuya
So the issue with the updated code is that it won't see a programmatic change, like document.getElementById("myInputVisible").value="New Value"Bookmark
well thats why the pair. observe the changes on the hidden field. there you can monitor both the programmatic changes and user's changes. your actual field is the hidden one. visible is just a wrapper or container i should say. see testSet method.Thuythuya
ok, so what we're saying is that I should actually make the original field hidden (it will still capture programmatic changes) and present the user with a clone that will capture user entries. Let me experiment with it, sounds like something that could work!Bookmark
yes that exactly what i think should work. you can easily put a method for monitoring any input field this way. well atleast the text onces.Thuythuya
yes, it does. put a method for testing programmatic changes as well. for future readers.Thuythuya
M
2

The following works everywhere I've tried it, including IE11 (even down to IE9 emulation mode).

It takes your defineProperty idea a bit further by finding the object in the input element prototype chain that defines the .value setter and modifying this setter to trigger an event (I've called it modified in the example), while still keeping the old behavior.

When you run the snippet below, you can type / paste / whatnot in the text input box, or you can click the button that appends " more" to the input element's .value. In either case, the <span>'s content is synchronously updated.

The only thing that's not handled here is an update caused by setting the attribute. You could handle that with a MutationObserver if you want, but note that there's not a one-to-one relationship between .value and the value attribute (the latter is just the default value for the former).

// 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>
Mastoid answered 5/10, 2017 at 10:42 Comment(0)
M
1

Since you are already using polyfills for watch/observe, etc, let me take the opportunity to suggest to you Angularjs.

It offers exactly this functionality in the form of it's ng-models. You can put watchers on the model's value, and when it changes, you can then call other functions.

Here is a very simple, but working solution to what you want:

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

Basically, make a text input and bind it to a model:

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

then put a watcher on the angularjs model on this input in the controller.

$scope.$watch(function() {
  return $scope.variable
}, function(newVal, oldVal) {
  if(newVal !== null) {
    window.alert('programmatically changed');
  }
});
Malcolm answered 14/10, 2013 at 16:52 Comment(7)
@Hash I'd would definitely be interested in more information on how Angularjs does it (note the bounty I just added, I am looking for a "detailed canonical answer").Bookmark
@Christophe: I've made and added a fiddle with some basic code explaining the core logic.Malcolm
the demo doesn't seem to do what I want. Here is my attempt to change the value programmatically: jsfiddle.net/jv8pK/1Bookmark
@Christophe: My demo does change the input value programatically. When you click the "Change", the value of the model, which is bound to the input changes. It is programatic. You can also change it directly in the code, but the way you've done it is not right in Angularjs. Any change to a model has to be within the controller. These are some things which you'll have to learn when you use angular. See this updated fiddle: jsfiddle.net/RedDevil/jv8pK/4Malcolm
On a sidenote, I'd recommend you to try angular's tutorial to understand all this a bit easily: docs.angularjs.org/tutorialMalcolm
@Harsh: I think the question actually was how to listen to changes without modifying the existing codebase - there would've been lots of other (and simpler) notification frameworks than Angular.Hein
@Harsh I understand. See the edit on my question, this was actually in the comments. I have now moved it to the question itself, I hope it clarifies the objective.Bookmark
C
1

I only need it for modern browsers.

How modern would you like to go? Ecma Script 7 (6 will be made final in December) might contain Object.observe. This would allow you to create native observables. And yes, you can run it! How?

To experiment with this feature, you need to enable the Enable Experimental JavaScript flag in Chrome Canary and restart the browser. The flag can be found under 'about:flags’

More info: read this.

So yeah, this is highly experimental and not ready in the current set of browsers. Also, it's still not fully ready and not 100% if it's coming to ES7, and the final date for ES7 isn't even set yet. Still, I wanted to let you know for future use.

Claytonclaytonia answered 14/10, 2013 at 17:4 Comment(1)
+1 Thanks, good to know! But definitely too "modern" at this point... I'd need to support at least IE 11.Bookmark
F
0

There is a way to do this. There is no DOM event for this, however there is a javascript event that triggers on an object property change.

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

In the callback you can do whatever.


In this example, we can see this effect (Check the 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;

When obj change, it activates the watch listener.

You have more information in this link : http://james.padolsey.com/javascript/monitoring-dom-properties/

Fancher answered 15/10, 2013 at 10:7 Comment(3)
This is not working for me. From the MDN site: Warning: Generally you should avoid using watch() and unwatch() when possible. These two methods are implemented only in Gecko, and they're intended primarily for debugging use. Source: Mozilla Developer NetworkBookmark
Yes, you're right, but in my link, he write his own method watch and unwatch with setInterval? Tried you this?Fancher
Sorry, I missed that last link. Yes, I tried this, and my hope is to get something better than a setInterval (cf. question and comment from the bounty)Bookmark
C
0

I wrote the following Gist a little while ago, which allows to listen for custom events cross browser (including IE8+).

Have a look at how I'm listening for onpropertychange on 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();
    }
  }
};

I'm not sure the IE8 solution works cross browser, but you could set a fake eventlistener on the property value of your input and run a callback once the value of value changes triggered by onpropertychange.

Centering answered 20/10, 2013 at 19:51 Comment(1)
Unfortunately this won't work for programmatic changes on browsers like IE 11 or Chrome.Bookmark
S
0

This is an old question, but with the newish JS Proxy object, triggering an event on a value change is pretty easy:

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

This creates a JS proxy object based on the original input DOM object. You can interact with the proxy object in the same way you'd interact with the origin DOM object. The difference is that we've overridden the setter. When the 'value' prop is changed, we carry out the normal, expected operation, obj[prop] = value, but then if the prop's value is 'value' we fire a custom InputEvent.

Note that you can fire whatever kind of event you'd like with new CustomEvent or new Event. "change" might be more appropriate.

Read more on proxies and custom events here: 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

Skiba answered 11/6, 2019 at 21:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.