2

Is there a standard way of catching all changes to the value of an HTML input element, despite whether it's changed by user input or changed programmatically?

Considering the following code (which is purely for example purposes and not written with good coding practices, you may consider it as some pseudo-code that happens to be able to run inside some web browsers :P )

<!DOCTYPE html>
<html>
<head>
    <title>Test Page</title>
    <script>
        window.onload = () => {
            var test = document.getElementById("test");

            test.onchange = () => {
                console.log("OnChange Called");
                console.log(test.value);
            }  // Called when the input element loses focus and its value changed

            test.oninput = () => {
                console.log("OnInput Called");
                console.log(test.value);
            }  // Called whenever the input value changes

            test.onkeyup = () => {
                console.log("OnKeyUp Called");
                console.log(test.value);
            }  // some pre-HTML5 way of getting real-time input value changes
        }
    </script>
</head>
<body>
    <input type="text" name="test" id="test">
</body>
</html>

However none of those events will fire when the value of the input element is changed programmatically, like someone doing a

document.getElementById("test").value = "Hello there!!";

To catch the value that's changed programmatically, usually one of two things can be done in the old days:

1) Tell the coders to fire the onchange event manually each time they change the input value programmatically, something like

document.getElementById("test").value = "Hello there!!";
document.getElementById("test").onchange();

However for this project at hand the client won't accept this kind of solution since they have many contractors/sub-contractors that come and go and I guess they just don't trust their contractors to follow this kind of rules strictly, and more importantly, they have a working solution from one of their previous contracts which is the second old way of doing things

2) set a timer that checks the input element value periodically and calls a function whenever it's changed, something like this

        var pre_value = test.value;

        setInterval(() => {
            if (test.value !== pre_value) {
                console.log("Value changed");
                console.log(test.value);
                pre_value = test.value;
            }
        }, 200);  // checks the value every 200ms and see if it's changed

This looks like some dinosaur from way back the jQuery v1.6 era, which is quite bad for all sorts of reasons IMHO, but somehow works for the client's requirements.

Now we are in 2019 and I'm wondering if there are some modern way to replace the above kind of code? The JavaScript setter/getter seems promising, but when I tried the following code, it just breaks the HTML input element

        Object.defineProperty(test, "value", {
            set: v => {
                this.value = v;
                console.log("Setter called");
                console.log(test.value);
            },
            get: ()=> {
                console.log("Getter called");
                return this.value;
            }
        });

The setter function will be called when the test.value is programmatically assigned, but the input element on the HTML page will somehow be broken.

So any idea on how to catch all changes to the value of an HTML input element and call the handler function other than the ancient "use a polling timer" method?

NOTICE: Take note that all the code here are just for example purposes and should not be used in real systems, where it's better to use the addEventListener/attachEvent/dispatchEvent/fireEvent etc. methods

hellopeach
  • 924
  • 8
  • 15
  • Isn't this a valid for your query? https://stackoverflow.com/questions/8796988/binding-multiple-events-to-a-listener-without-jquery – Bhojendra Rauniyar Mar 25 '19 at 06:16
  • The best is to call whatever callback should be fired from the js responsible of the changes. Firing an event is a bad hack, systematically calling a callback on value change is a call for endless loops. – Kaiido Mar 25 '19 at 06:21
  • Are you just concerned with text-based input, as in the question, or do you also want to intercept changes to ` – CertainPerformance Mar 25 '19 at 06:22

2 Answers2

2

To observe assignments and retrieval to the .value of an element, Object.defineProperty is the way to go, but you need to call the original setter and getter functions inside your custom methods, which are available on HTMLInputElement.prototype:

const { getAttribute, setAttribute } = test;
test.setAttribute = function(...args) {
  if (args[0] === 'value') {
    console.log('Setting value');
  }
  return setAttribute.apply(this, args);
};
test.getAttribute = function(...args) {
  if (args[0] === 'value') {
    console.log('Getting value');
  }
  return getAttribute.apply(this, args);
};



test.setAttribute('value', 'foo');
console.log(test.getAttribute('value'));
<input type="text" name="test" id="test">

Note the use of methods rather than arrow functions - this is important, it allows the this context to be preserved. (you could also use something like set.call(input, v), but that's less flexible)

That's just for changes to .value. You can monkeypatch something similar for setAttribute('value, if you want:

const { setAttribute } = test;
test.setAttribute = function(...args) {
  if (args[0] === 'value') {
    console.log('attribute set!');
  }
  return setAttribute.apply(this, args);
};

test.setAttribute('value', 'foo');
<input type="text" name="test" id="test">
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • Fails for `test.setAttribute('value', 'foo');` also fails for `[].files = FileList` and for `parent
    .reset()`
    – Kaiido Mar 25 '19 at 06:17
  • And don't forget, `input.onstoopidallchange = e => input.value = input.value.toUpperCase();` – Kaiido Mar 25 '19 at 06:24
  • What's the issue with `onstoopidallchange`? If such a function is called on a monkeypatched input, it looks to work as desired, the setter and getter is intercepted properly, without `onstoopidallchange` being any wiser about it – CertainPerformance Mar 25 '19 at 06:29
  • ;-) try it (from a tab you don't like too much) – Kaiido Mar 25 '19 at 06:31
  • It seems to work in the answer snippet and https://jsfiddle.net/bg6102kv/ as well – CertainPerformance Mar 25 '19 at 06:39
  • No, you didn't get me. OP wants to fire a callback everytime they catch such a change. If you do set the value of the input in one of these callbacks then you kill the tab. See the snippet in my answer, or this updated fiddle https://jsfiddle.net/kd30j8cp/1/ – Kaiido Mar 25 '19 at 06:55
  • Sure, if you try to set the value *inside something that listens for when you set the value*, you'll crash the page, but that's easy to avoid – CertainPerformance Mar 25 '19 at 06:56
  • That's exactly why standards forbid to fire Events on other changes than the ones made by users. And that would be easy to avoid from outside, but then it's probably just easier to fire the callback directly. – Kaiido Mar 25 '19 at 06:57
  • The first snippet doesn't work for me, but the second snippet works nicely, and it's at least a much better alternative to the polling timer thing I guess. – hellopeach Mar 25 '19 at 07:16
  • @hellopeach What's the problem with the first snippet? Works fine on my end, what errors are you getting? – CertainPerformance Mar 25 '19 at 07:19
  • @Kaiido, well, since JS added this getter/setter functionality, I guess they see some use case for it, as any setter can cause an infinite loop/recursion if you assign to the same property in the setter function that the setter is defined for. Also the "explicitly fire the callback" method can also cause infinite loop/recursion if they somehow fire the callback function in another function triggered by the callback function anyway... – hellopeach Mar 25 '19 at 07:22
  • 1
    @hellopeach Honestly? I think they just got lucky it didn't crash yet. But in the mean time, their code would only have failed with a kind of jitter function (`inp.value = inp.value + Math.random()-0.5`) since they are checking for an actual change. Seriously all this is a bad idea, you should let them know and try to understand the X of this X-Y problem. And yes.. firing the callback from inside the callback can also cause infinite loop, but that would be made from the one contractor that wrote/edited the callback. Not from the one that implemented an all-caps policy. – Kaiido Mar 25 '19 at 07:29
  • @Kaiido, well, I guess if they enforce a policy of writing inp.onchange() after every inp.value assignment, that's as likely to cause the infinite loop/recursion situation anyway, for example one wrote an inp.onchange function to call another function, while the other function made an inp.value= "..." assignment then called inp.onchange() according to the rulebook... and as you said, this kind of things may be from multiple different contractors, instead of a single contractor that you can point your finger at (which is something clients around here generally like) – hellopeach Mar 25 '19 at 08:46
  • @hellopeach but that problem would be introduced by the very person that will add **both** `input.value = foo` and `input.onchange()`. For them, it should be clear that they are inside such an onchange already and thus should not fire it again. But if you have someone else that only come in a function where they see `function handleChange(evt) { ... }` and all they add is `input.value = foo;` then they may have no idea that this very value setting does fire the event in which they are, because it is not standard behavior. – Kaiido Mar 25 '19 at 08:49
  • @CertainPerformance, the issue with the first snippet is that it works well if you stick to calling the test.setAttribute(...) to set the value, but once you do a test.value = "..." it seems to break as first it won't trigger the setter and secondly all following test.setAttribute(...) calls won't have any effect on the displayed HTML element. That's what I tested on Chrome 72.0.3626.121... On another note, it seems test.value = "..." assignment just breaks the test.setAttribute(...) functionality for HTML display... – hellopeach Mar 25 '19 at 09:06
2

The standard way is to not fire a change event when the value has been changed programmatically.
Not only are there too many ways to set the value of an input programmatically, but moreover, that's just a call for endless loop.

If in any of your callbacks your input's value is set, then you'll crash the page.

Wanna try?

let called = 0; // to avoid blocking this page
const { set, get } = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
Object.defineProperty(inp, 'value', {
  set(v) {
    const ret = set.call(this, v);
    if(++called < 20) // I limit it to 20 calls to not kill this page
      this.dispatchEvent(new Event('all-changes'));
    return ret;
  },
  get() { return get.call(this); }
});

inp.addEventListener('all-changes', e => {
  inp.value = inp.value.toUpperCase();
  console.log('changed');
});

btn.onclick = e => inp.value = 'foo';
<input id="inp">
<button id="btn">set value</button>

So the best is still to only call whatever callback directly from the code responsible of the change.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • well, as I said, the client just has this kind requirement, and they'd rather use the ancient polling timer thing than telling their contractors/sub-contractors to obey a rule to explicitly call the right function after assigning an input value programmatically. – hellopeach Mar 25 '19 at 06:56
  • Then you are in the uncomfortable position of having to explain to your client that their request is not a good idea, and that they should consider review it. Of course, as clients they have the final word, but since you asked for the *standard* way, here it is. – Kaiido Mar 25 '19 at 07:00