33

I want to detect when text/value change in input field. Even if I change the value with js, I want to detect that changes.

Here's what I've tried so far in demo in fiddle.

HTML:

<input type="text" id="exNumber"/>

JavaScript:

var observer = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    // console.log('Mutation type: ' + mutation.type);
    if ( mutation.type == 'childList' ) {
      if (mutation.addedNodes.length >= 1) {
        if (mutation.addedNodes[0].nodeName != '#text') {
           // console.log('Added ' + mutation.addedNodes[0].tagName + ' tag.');
        }
      }
      else if (mutation.removedNodes.length >= 1) {
         // console.log('Removed ' + mutation.removedNodes[0].tagName + ' tag.')
      }
    }
     if (mutation.type == 'attributes') {
      console.log('Modified ' + mutation.attributeName + ' attribute.')
    }
  });   
});

var observerConfig = {
        attributes: true,
        childList: false,
        characterData: false
};

// Listen to all changes to body and child nodes
var targetNode = document.getElementById("exNumber");
observer.observe(targetNode, observerConfig);
akinuri
  • 10,690
  • 10
  • 65
  • 102
while true
  • 816
  • 1
  • 11
  • 26
  • 1
    If you are impatient and want a terrible awful no-good really-bad fix right now, then I made just the thing for you: [IDL-Property-Observe](https://github.com/anonyco/IDL-Property-Observer). After running this library, your code above will run just fine at the cost of sacrificing best-practices with native prototypes. Cheers! – Jack G Aug 19 '19 at 01:43

5 Answers5

40

To understand what is going on is necessary to clear up the difference between attribute (content attribute) and property (IDL attribute). I won't expand on this as in SO there are already excellent answers covering the topic:

When you change the content of a input element, by typing in or by JS:

targetNode.value="foo";

the browser updates the value property but not the value attribute (which reflects the defaultValue property instead).

Then, if we look at the spec of MutationObserver, we will see that attributes is one of the object members that can be used. So if you explicitly set the value attribute:

targetNode.setAttribute("value", "foo");

MutationObserver will notify an attribute modification. But there is nothing like properties in the list of the spec: the value property can not be observed.

If you want to detect when an user alters the content of your input element, the input event is the most straightforward way. If you need to catch JS modifications, go for setInterval and compare the new value with the old one.

Check this SO question to know about different alternatives and its limitations.

David
  • 6,695
  • 3
  • 29
  • 46
  • `onkeyup` doesn't useful for my case.actually the external script i can't modify for some reason. script change value of input field .so i want to detect when text change – while true Sep 03 '15 at 19:21
  • In that case, check [this other one](http://stackoverflow.com/q/1948332/5247200). – David Sep 03 '15 at 19:27
  • tnx david so the only ugly solution is using a timer .i didn't want that that's why i tried observer .but unfortunately there may be no other way to do that – while true Sep 03 '15 at 19:34
  • I don't think so (and I agree that a timer is ugly) if you don't have access to the external script. Anyway, my answer cover the original question about how _MutationObserver_ behave, so it would be nice if you mark it as correct :) – David Sep 03 '15 at 19:44
  • @David Thanks. This was killing me for a long time. – Soham Dasgupta Feb 17 '16 at 19:00
  • 3
    Do you have any details on why the dom is not changed? After all I can see the change in my browser window. – Mene Oct 23 '17 at 13:23
  • 1
    @Mene Thanks for pointing that out: it is wrong. I guess I wanted to say HTML (which defines attributes) instead of DOM (which defines properties). It is common to refer to a change in an attribute as a change in the HTML. Anyway, being strict, HTML is the static text that resides in the server (it doesn't change) while the DOM is the in-memory object resulting from parsing the markup (it changes whenever the user or a script modifies it) so I just deleted that sentence. Actually, I rewrote the whole thing to be more specific. – David Oct 27 '17 at 11:10
  • Excellent! I was having trouble understand what's going on, or even find references to it, your edits help a lot! – Mene Oct 27 '17 at 11:15
  • I used the JSFiddle link provided by op and it's working. I mean it's capturing all the changes whenever I enter an input. But the same code is not working outside of the JSFiddle, do you have any idea? – Foo Bar Zoo Feb 17 '19 at 17:49
  • @FooBarZoo - It shouldn't work if we follow the specification. JSFiddle is not open source so the only way to know it they are altering the normal behavior of `Mutationbserver` is to [ask them](https://github.com/jsfiddle/jsfiddle-issues) (but it sounds weird to me). It can be also related to a browser implementation. I just try out the fiddle again on FF 64 on Linux and it behaves as expected (no notifications at least the attribute changes). – David Feb 17 '19 at 19:27
  • @David thanks for quick response. The thing is, the code in JSFiddle is working on Firefox on OSX even outside of the JSFiddle editor. The only difference is, it's working on Chrome on OSX with JSFiddle editor and not working otherwise. Maybe JSFiddle is adding an extra JS file which adds some event listeners for input fields to make it working for `MutationObserver` but another mind blowing thing is, the behaviour of Chrome and Firefox is completely different, one is capturing the input changes and the other one does not(I mean outside of the JSFiddle editor). – Foo Bar Zoo Feb 17 '19 at 21:57
20

I've modified Shawn's method a little and wanted to share it. Can't believe there's actually a solution to this.

Type into the input box to see the default behavior. Now, open the DevTools and select the input element, then change its value, e.g. $0.value = "hello". Examine the UI vs. API difference. It seems UI interactions do not modify value property directly. If it were, it would also log "...changed via API...".

let inputBox = document.querySelector("#inputBox");

inputBox.addEventListener("input", function () {
    console.log("Input value changed via UI. New value: '%s'", this.value);
});

observeElement(inputBox, "value", function (oldValue, newValue) {
    console.log("Input value changed via API. Value changed from '%s' to '%s'", oldValue, newValue);
});

function observeElement(element, property, callback, delay = 0) {
    let elementPrototype = Object.getPrototypeOf(element);
    if (elementPrototype.hasOwnProperty(property)) {
        let descriptor = Object.getOwnPropertyDescriptor(elementPrototype, property);
        Object.defineProperty(element, property, {
            get: function() {
                return descriptor.get.apply(this, arguments);
            },
            set: function () {
                let oldValue = this[property];
                descriptor.set.apply(this, arguments);
                let newValue = this[property];
                if (typeof callback == "function") {
                    setTimeout(callback.bind(this, oldValue, newValue), delay);
                }
                return newValue;
            }
        });
    }
}
<input type="text" id="inputBox" placeholder="Enter something" />
akinuri
  • 10,690
  • 10
  • 65
  • 102
9

the value property can be observed, Don't waste your time.

function changeValue (event, target) {
    document.querySelector("#" + target).value = new Date().getTime();
}
 
function changeContentValue () {
    document.querySelector("#content").value = new Date().getTime();
}
 
Object.defineProperty(document.querySelector("#content"), "value", {
    set:  function (t) {
        alert('#changed content value');
        var caller = arguments.callee
            ? (arguments.callee.caller ? arguments.callee.caller : arguments.callee)
            : ''
 
        console.log('this =>', this);
        console.log('event => ', event || window.event);
        console.log('caller => ', caller);
        return this.textContent = t;
    }
});
<form id="form" name="form" action="test.php" method="post">
        <input id="writer" type="text" name="writer" value="" placeholder="writer" /> <br />
        <textarea id="content" name="content" placeholder="content" ></textarea> <br />
        <button type="button" >Submit (no action)</button>
</form>
<button type="button" onClick="changeValue(this, 'content')">Change Content</button>
pixelDino
  • 105
  • 2
  • 9
GracefulLight
  • 105
  • 1
  • 5
  • 1
    I'm working on a similar issue, and I'm interested in this answer, but I'm struggling a little bit. This only works for textarea's right? Since you set the textContent which appears inside the textarea field, but textContent does not appear inside the input field in HTML. – Josh Oct 03 '18 at 14:08
  • 1
    @Josh Input field uses `return this.defaultValue = t;` : ) – GracefulLight Oct 03 '18 at 14:51
  • 3
    Could you give more details on how your code works please? – Polyducks Apr 29 '19 at 09:53
  • You have to define the `get` method if you want it to be read from JS – Madacol Feb 24 '22 at 22:35
  • Sadly, this doesn't work when developing extensions. The page's code runs in a different environment, so this setter doesn't exist from the JS in the page that sets the `value` property – Madacol Feb 24 '22 at 22:40
9

This works and preserves and chains the original setter and getter so everything else about your field still works.

var registered = [];
var setDetectChangeHandler = function(field) {
  if (!registered.includes(field)) {
    var superProps = Object.getPrototypeOf(field);
    var superSet = Object.getOwnPropertyDescriptor(superProps, "value").set;
    var superGet = Object.getOwnPropertyDescriptor(superProps, "value").get;
    var newProps = {
      get: function() {
        return superGet.apply(this, arguments);
      },
      set: function (t) {
        var _this = this;
        setTimeout( function() { _this.dispatchEvent(new Event("change")); }, 50);
        return superSet.apply(this, arguments);
      }
    };
    Object.defineProperty(field, "value", newProps);
    registered.push(field);
  }
}
Shawn Regan
  • 91
  • 1
  • 2
  • 2
    Can you explain how your solution works? What is it doing? – Polyducks Apr 29 '19 at 09:53
  • 2
    I think this is the best out of the box solution. Worked perfectly in my application and I love it because it leaves the original setter alone. – Xandor Oct 25 '19 at 12:14
  • For each field on a form call: setDetectChangeHandler(document.querySelector('.myFrom .fieldN')); Then to catch all changes add a listener to the form document.querySelector('.myForm').addEventListener('change', event => { // process the field changes here }); – Systemsplanet Jan 14 '20 at 05:50
  • I like this solution. Though this causes a tiny problem in a particular situation. I have added an event listener (for input event) to the field with debounce behaviour. The value I enter to the field gets updated (correcting currency format) after 500ms. So I update the field, and field updates itself (its value). If I use this (`setDetectChangeHandler`) method, I end up with a loop. `"input"` event fires continuously. So instead of firing an event in `set`, I pass a callback to the `setDetectChangeHandler` and call it in the `set`. This gives more control. – akinuri May 23 '20 at 15:48
  • How do I use this? `field` is what? – Jarad Feb 19 '21 at 05:59
0

A little manager to monitor input properties:

var observerList = [];
var observePropertyChangeStarted = false;
function observePropertyChange(input, propertyName, callback) {
    observerList.push({
         input: input,
         propertyName: propertyName,
         state: input[propertyName],
         callback: callback
    });
    if (!observePropertyChangeStarted) {
        observePropertyChangeStarted = true;
        setInterval(_ => {
            for (const obj of observerList) {
                const state = obj.input[obj.propertyName];
                if (state !== obj.state) {
                    obj.state = state;
                    obj.callback(obj.input);
                }
            }
        }, 100);
    }
}

Then simply listen to any input property:

const checkBoxInput = document.querySelector('input[type="checkbox"]');
observePropertyChange(checkBoxInput, 'checked', (target) => { 
    // myActions();
}); 
checkBoxInput.checked = false;

const rangeInput = document.querySelector('input[type="range"]');
observePropertyChange(rangeInput , 'value', (target) => { 
    // myActions();
}); 
rangeInput.value = 10;

The function calls only one "setInterval" to limit the use of resources.

Dady
  • 71
  • 8