Detect Changes in the Dom

Detect changes in the DOM

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+

Efficiently detect DOM change in single page application

MutationObserver is probably your best bet. Before MutationObserver, there were Mutation events, but these are now deprecated and not recommended.

Another (arguably poor) option would be to use a setTimeout and periodically check the DOM for changes. But this would require you to write a function to get the nodes you want to check and compare the current value with the old value. This wouldn't be as efficient, since you'd be checking the nodes every X milliseconds, even if there are no changes.

You should be able to write your code in such a way that an infinite loop does not occur. If you are getting an infinite loop, you should add that code to your question above.

As far as performance goes, since MutationObserver will give you old and new values, you could compare these values instead of traversing the entire DOM. For example, something like this:

let observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
let oldValue = mutation.oldValue;
let newValue = mutation.target.textContent;
if (oldValue !== newValue) {
// do something
}
});
});

observer.observe(document.body, {
characterDataOldValue: true,
subtree: true,
childList: true,
characterData: true
});

Detect changes in DOM elements order

If you want to check for changes in the DOM you can use the MutationObserver:

$(document).ready(function() {

var initial = $("div"); //This might not be ok

if (new Date() % 2) {

randomizeOrder();

}



});

function randomizeOrder() {

var lastElement = $("div span").last();

$(lastElement).insertBefore($("div span").first());

}

var target = document.getElementById('id1');

// create an observer instance

var observer = new MutationObserver(function(mutations) {

var changed = false;

mutations.forEach(function(mutation) {

// You can check the actual changes here

});

console.log('Dom Changed');

});

// configuration of the observer:

var config = { attributes: true, childList: true, characterData: true };

// pass in the target node, as well as the observer options

observer.observe(target, config);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div id="id1">

<span>Random text</span>

<span style="color: red">Random text</span>

</div>

Is there a JavaScript / jQuery DOM change listener?

For a long time, DOM3 mutation events were the best available solution, but they have been deprecated for performance reasons. DOM4 Mutation Observers are the replacement for deprecated DOM3 mutation events. They are currently implemented in modern browsers as MutationObserver (or as the vendor-prefixed WebKitMutationObserver in old versions of Chrome):

MutationObserver = window.MutationObserver || window.WebKitMutationObserver;

var observer = new MutationObserver(function(mutations, observer) {
// fired when a mutation occurs
console.log(mutations, observer);
// ...
});

// define what element should be observed by the observer
// and what types of mutations trigger the callback
observer.observe(document, {
subtree: true,
attributes: true
//...
});

This example listens for DOM changes on document and its entire subtree, and it will fire on changes to element attributes as well as structural changes. The draft spec has a full list of valid mutation listener properties:

childList

  • Set to true if mutations to target's children are to be observed.

attributes

  • Set to true if mutations to target's attributes are to be observed.

characterData

  • Set to true if mutations to target's data are to be observed.

subtree

  • Set to true if mutations to not just target, but also target's descendants are to be observed.

attributeOldValue

  • Set to true if attributes is set to true and target's attribute value before the mutation needs to be recorded.

characterDataOldValue

  • Set to true if characterData is set to true and target's data before the mutation needs to be recorded.

attributeFilter

  • Set to a list of attribute local names (without namespace) if not all attribute mutations need to be observed.

(This list is current as of April 2014; you may check the specification for any changes.)

JavaScript/DOM: MutationObserver to detect changes on `lang` attributes anywhere in the DOM tree

The following code (same as in the question) is indeed working and will detect lang attribute changes anywhere in the document.
But it is important to know that only lang attribute changes in the light DOM will be detected, changes in the shadow DOM will NOT be observed.

const observer = new MutationObserver(() => {
console.log('lang attribute has changed');
});

observer.observe(document, {
attributes: true,
attributeFilter: ['lang'],
subtree: true
});

Angular2 Directive: How to detect DOM changes

Angular does not provide something built-in for that purpose. You can use MutationObserver to detect DOM changes.

@Directive({
selector: '[my-skrollr]',
...
})
class MyComponent {
constructor(private elRef:ElementRef) {}

ngAfterViewInit() {
this.observer = new MutationObserver(mutations => {
mutations.forEach(function(mutation) {
console.log(mutation.type);
});
});
var config = { attributes: true, childList: true, characterData: true };

this.observer.observe(this.elRef.nativeElement, config);
}
}

Detecting DOM changes with puppeteer and MutationObserver

Maybe page.exposeFunction() is the simplest way:

'use strict';

const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://www.example.com/');

await page.exposeFunction('puppeteerLogMutation', () => {
console.log('Mutation Detected: A child node has been added or removed.');
});

await page.evaluate(() => {
const target = document.querySelector('body');
const observer = new MutationObserver( mutations => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
puppeteerLogMutation();
}
}
});
observer.observe(target, { childList: true });
});

await page.evaluate(() => {
document.querySelector('body').appendChild(document.createElement('br'));
});

})();

But page.waitForSelector() (or some other waitFor... methods) in some cases can also suffice:

'use strict';

const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://www.example.com/');

await page.evaluate(() => {
setTimeout(() => {
document.querySelector('body')
.appendChild(document.createElement('br')).className = 'puppeteer-test';
}, 5000);
});

await page.waitForSelector('body br.puppeteer-test');

console.log('Mutation Detected: A child node has been added or removed.');
})();


Related Topics



Leave a reply



Submit