Detect changes in the DOM
Solution 1
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+
Solution 2
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>…More will be added after 3 seconds…</em></li>
</ol>
Solution 3
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+
Solution 4
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.
Solution 5
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/
Related videos on Youtube
esafwan
I Started with DOS, grew up with Windows and Matured with GNU/Linux. Was a Hardcore gamer, later interest shifted to programing and graphic designing. Is passionate when it comes to debating. Hates working under someone and has never so far. Currently busy building a career that is free from boss and hunger for money!
Updated on October 30, 2021Comments
-
esafwan over 2 years
I want to execute a function when some div or input are added to the html. Is this possible?
For example, a text input is added, then the function should be called.
-
Justin Johnson almost 14 yearsUnless some third party script is adding the nodes to the DOM, this isn't necessary.
-
Gabriele Petrioli almost 14 yearsduplicate of stackoverflow.com/questions/2457043/…
-
Makyen over 6 yearsPossible duplicate of Is there a JavaScript/jQuery DOM change listener?
-
FluorescentGreen5 about 6 years@JustinJohnson if you're making a chrome extension which injects JS code, it's useful.
-
Maykonn almost 6 years
-
Christoffer Bubach about 3 yearsI really don't get it - it's like... eh.. requesting a hotel wake up call at their breakfast buffet... You change the DOM with JS and then want JS-events to notify you of what you just did? Sounds like an awesome code-base. If you're just waiting for a specific css selector to load there's older methods like onload="", jquery .ready() or a manual setInterval checks that solves it without an extra 50kb blob as the "solutions" below. :P
-
esafwan about 3 yearsYou are assuming a scenario where just your own code will change your dom. Imagine a scenario where you are using 3rd party library a day you want to know when they change the dom. There many other use cases too, that's the reason now we have MutationObserver support in all modern browsers.
-
esafwan about 3 years
-
-
esafwan almost 14 yearscant we add a active listener(dont knw wht to say!) or something that check in interval and check the dom?
-
Kees C. Bakker over 13 yearsWondering: how does jQuery live() solve this problem if they can't detect a DOM change?
-
Bojangles over 11 years@JoshStodola The bold was annoying me too. I decided to fix it.
-
Sebastien Lorber about 10 yearsit seems to work pretty nicely for new DOM nodes. Can we adapt it to also handle dom node changes (at least the DOM node values/text?)
-
A1rPun almost 10 yearsPass the
mutations, observer
parameters to the callback function for more control. -
stiller_leser over 9 yearsThis helped me a lot, but how do I "unbind" this? Say I want to watch for a change only once, but do this on multiple occasions? oberserveDOM = null obviously won't work...
-
Adam Pietrasiak over 9 yearsMutations events are deprecated. You should use MutationObserver. I've written my plugin for problems like this - github.com/AdamPietrasiak/jquery.initialize
-
SuperUberDuper about 9 yearsHow can I get jquery onClick to fire before a mutation observer, that fires when a button is clicked with a ember action? stackoverflow.com/questions/29216434/…
-
f0ster almost 9 yearsWhy would this only work for appended/removed? It looks like the mutation events cover more than that.. developer.mozilla.org/en-US/docs/Web/Guide/Events/…
-
RobG over 8 yearsBTW, passing window to window in
(function(window){...}(window))
is pointless. If the intention is to get the global/window object safely, pass in this:(function(window){...}(this))
since in global code, this always points to the global/window object. -
RobG over 8 yearsI can't see how the naïve function can work. getElementsByTagName returns a live node list, so last and current will always contain exactly the same elements. last should be static (e.g. use querySelectorAll instead). Perhaps that section of the code hasn't been tested? Also, even if converted to a static array, if the same number of nodes is added as removed, then the length test will exit early when there have been changes made.
-
lefoy over 7 yearsthis is not the same... the method described above is still valid api.jquery.com/trigger
-
thexpand almost 6 yearsPlease, add to npm.
-
Niloct over 5 yearsThis is a good start but failed to catch when are multiple mutations at once and so the
childList
mutation is not onmutations[0]
. You should check every item ofmutations
. -
BenMorel over 5 yearsMutationObserver is native JavaScript, not jQuery.
-
stldoug almost 4 yearsJSHint does not like
!obj.nodeType === 1
. It's just checking to make sure the nodeType is an element, so you can use,obj.nodeType !== 1
. -
Samuel Nwaokoro over 3 yearsThanks for the warning about dragons 😅
-
Altimus Prime about 3 yearsWhen I try this nothing logs to the console on any of the mutations to the DOM. I must not be doing it right.
-
Vadorequest about 2 yearsTo "unwatch", use the
observer.disconnect();
see the documentation at developer.mozilla.org/en-US/docs/Web/API/MutationObserver