javascript - Detect changes in the DOM

ID : 10286

viewed : 16

Tags : javascriptdommutation-eventsmutation-observersjavascript

Top 5 Answer for javascript - Detect changes in the DOM

vote vote

100

Ultimate approach so far, with smallest code:

(IE11+, FF, Webkit)

Using MutationObserver and falling back to the deprecated Mutation events if needed:
(Example below if only for DOM changes concerning nodes appended or removed)

var observeDOM = (function(){   var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;    return function( obj, callback ){     if( !obj || obj.nodeType !== 1 ) return;       if( MutationObserver ){       // define a new observer       var mutationObserver = new MutationObserver(callback)        // have the observer observe foo for changes in children       mutationObserver.observe( obj, { childList:true, subtree:true })       return mutationObserver     }          // browser support fallback     else if( window.addEventListener ){       obj.addEventListener('DOMNodeInserted', callback, false)       obj.addEventListener('DOMNodeRemoved', callback, false)     }   } })()   //------------< DEMO BELOW >----------------  // add item var itemHTML = "<li><button>list item (click to delete)</button></li>",     listElm = document.querySelector('ol');  document.querySelector('body > button').onclick = function(e){   listElm.insertAdjacentHTML("beforeend", itemHTML); }  // delete item listElm.onclick = function(e){   if( e.target.nodeName == "BUTTON" )     e.target.parentNode.parentNode.removeChild(e.target.parentNode); }      // Observe a specific DOM element: observeDOM( listElm, function(m){     var addedNodes = [], removedNodes = [];     m.forEach(record => record.addedNodes.length & addedNodes.push(...record.addedNodes))        m.forEach(record => record.removedNodes.length & removedNodes.push(...record.removedNodes))    console.clear();   console.log('Added:', addedNodes, 'Removed:', removedNodes); });   // Insert 3 DOM nodes at once after 3 seconds setTimeout(function(){    listElm.removeChild(listElm.lastElementChild);    listElm.insertAdjacentHTML("beforeend", Array(4).join(itemHTML)); }, 3000);
<button>Add Item</button> <ol>   <li><button>list item (click to delete)</button></li>   <li><button>list item (click to delete)</button></li>   <li><button>list item (click to delete)</button></li>   <li><button>list item (click to delete)</button></li>   <li><em>&hellip;More will be added after 3 seconds&hellip;</em></li> </ol>

vote vote

86

2015 update, new MutationObserver is supported by modern browsers:

Chrome 18+, Firefox 14+, IE 11+, Safari 6+

If you need to support older ones, you may try to fall back to other approaches like the ones mentioned in this 5 (!) year old answer below. There be dragons. Enjoy :)


Someone else is changing the document? Because if you have full control over the changes you just need to create your own domChanged API - with a function or custom event - and trigger/call it everywhere you modify things.

The DOM Level-2 has Mutation event types, but older version of IE don't support it. Note that the mutation events are deprecated in the DOM3 Events spec and have a performance penalty.

You can try to emulate mutation event with onpropertychange in IE (and fall back to the brute-force approach if non of them is available).

For a full domChange an interval could be an over-kill. Imagine that you need to store the current state of the whole document, and examine every element's every property to be the same.

Maybe if you're only interested in the elements and their order (as you mentioned in your question), a getElementsByTagName("*") can work. This will fire automatically if you add an element, remove an element, replace elements or change the structure of the document.

I wrote a proof of concept:

(function (window) {     var last = +new Date();     var delay = 100; // default delay      // Manage event queue     var stack = [];      function callback() {         var now = +new Date();         if (now - last > delay) {             for (var i = 0; i < stack.length; i++) {                 stack[i]();             }             last = now;         }     }      // Public interface     var onDomChange = function (fn, newdelay) {         if (newdelay) delay = newdelay;         stack.push(fn);     };      // Naive approach for compatibility     function naive() {          var last = document.getElementsByTagName('*');         var lastlen = last.length;         var timer = setTimeout(function check() {              // get current state of the document             var current = document.getElementsByTagName('*');             var len = current.length;              // if the length is different             // it's fairly obvious             if (len != lastlen) {                 // just make sure the loop finishes early                 last = [];             }              // go check every element in order             for (var i = 0; i < len; i++) {                 if (current[i] !== last[i]) {                     callback();                     last = current;                     lastlen = len;                     break;                 }             }              // over, and over, and over again             setTimeout(check, delay);          }, delay);     }      //     //  Check for mutation events support     //      var support = {};      var el = document.documentElement;     var remain = 3;      // callback for the tests     function decide() {         if (support.DOMNodeInserted) {             window.addEventListener("DOMContentLoaded", function () {                 if (support.DOMSubtreeModified) { // for FF 3+, Chrome                     el.addEventListener('DOMSubtreeModified', callback, false);                 } else { // for FF 2, Safari, Opera 9.6+                     el.addEventListener('DOMNodeInserted', callback, false);                     el.addEventListener('DOMNodeRemoved', callback, false);                 }             }, false);         } else if (document.onpropertychange) { // for IE 5.5+             document.onpropertychange = callback;         } else { // fallback             naive();         }     }      // checks a particular event     function test(event) {         el.addEventListener(event, function fn() {             support[event] = true;             el.removeEventListener(event, fn, false);             if (--remain === 0) decide();         }, false);     }      // attach test events     if (window.addEventListener) {         test('DOMSubtreeModified');         test('DOMNodeInserted');         test('DOMNodeRemoved');     } else {         decide();     }      // do the dummy test     var dummy = document.createElement("div");     el.appendChild(dummy);     el.removeChild(dummy);      // expose     window.onDomChange = onDomChange; })(window); 

Usage:

onDomChange(function(){      alert("The Times They Are a-Changin'"); }); 

This works on IE 5.5+, FF 2+, Chrome, Safari 3+ and Opera 9.6+

vote vote

71

The following example was adapted from Mozilla Hacks' blog post and is using MutationObserver.

// Select the node that will be observed for mutations var targetNode = document.getElementById('some-id');  // Options for the observer (which mutations to observe) var config = { attributes: true, childList: true };  // Callback function to execute when mutations are observed var callback = function(mutationsList) {     for(var mutation of mutationsList) {         if (mutation.type == 'childList') {             console.log('A child node has been added or removed.');         }         else if (mutation.type == 'attributes') {             console.log('The ' + mutation.attributeName + ' attribute was modified.');         }     } };  // Create an observer instance linked to the callback function var observer = new MutationObserver(callback);  // Start observing the target node for configured mutations observer.observe(targetNode, config);  // Later, you can stop observing observer.disconnect(); 

Browser support: Chrome 18+, Firefox 14+, IE 11+, Safari 6+

vote vote

67

or you can simply Create your own event, that run everywhere

 $("body").on("domChanged", function () {                 //dom is changed              });    $(".button").click(function () {            //do some change           $("button").append("<span>i am the new change</span>");            //fire event           $("body").trigger("domChanged");          }); 

Full example http://jsfiddle.net/hbmaam/Mq7NX/

vote vote

50

I have recently written a plugin that does exactly that - jquery.initialize

You use it the same way as .each function

$(".some-element").initialize( function(){     $(this).css("color", "blue");  }); 

The difference from .each is - it takes your selector, in this case .some-element and wait for new elements with this selector in the future, if such element will be added, it will be initialized too.

In our case initialize function just change element color to blue. So if we'll add new element (no matter if with ajax or even F12 inspector or anything) like:

$("<div/>").addClass('some-element').appendTo("body"); //new element will have blue color! 

Plugin will init it instantly. Also plugin makes sure one element is initialized only once. So if you add element, then .detach() it from body and then add it again, it will not be initialized again.

$("<div/>").addClass('some-element').appendTo("body").detach()     .appendTo(".some-container"); //initialized only once 

Plugin is based on MutationObserver - it will work on IE9 and 10 with dependencies as detailed on the readme page.

Top 3 video Explaining javascript - Detect changes in the DOM

Related QUESTION?