Detecting DOM change using Mutation observer

- 11 mins
JSMutationObserver

Index


Introduction:

Detecting and get notified when dom changes is a common scenario for developers. Previously it could be done using Mutation events. But it had a huge performance degradation and stability issue (see here). That’s why it has announced as deprecated and recommended to avoid. In future release of the browsers, this might be removed from their engines too.
On 2011 a new proposal for MUTATION OBSERVER had announced. It resolved the performance issue at the same time keeping the same functionality.

Today we’ll implement a basic mutation observer and will discuss about its structure.


Preview

What will we do?


Do it yourself - Step 1

Let’s try it out.

Initially create a html boilerplate. Create a css file style.css and a js file mutation.js on same directory.

	<!DOCTYPE html>
	<html>
	<head>
		<meta charset="utf-8">
		<title>Mutation Observer 101</title>
		<link rel="stylesheet" type="text/css" href="style.css">
	</head>
	<body>
	</body>
	<script type="text/javascript" src='mutation.js'></script>
	</html>

Now update html markup within body. Here div with the id changeMe contains actual data we will mutate. Next div with id log will show the log data whenever data changes on upper div.

	<div class="container">
		<div id="changeMe">
			<h3>TARGET</h3>
			<ol>
				<li data-id="1">Hello, My name is Nabil Ahmad.</li>
				<li id='oldId' data-id="2">Nice to meet you.</li>
				<li data-id="3">Thank you.</li>
			</ol>
		</div>
		<div id="log">
			<h3>LOG</h3>
			<ul></ul>
		</div>
	</div>

Next add some styling code. Add following css on style.css

	ul, ol{line-height: 2em;}
	#log{margin-top: 1%;}
	h3{
		background: purple;
		color: #fff;
		padding: 10px;
	}
	.type{background-color: chartreuse;}
	.change{background-color: antiquewhite;}

So our basic boilerplate is ready. Next we will start with mutation.js file.


Do it yourself - Step 2

Add an array of corresponding spanish translation for each list text.

	const translatedText = ["Hola, Me llamo Nabil Ahmad.", "Mucho gusto.", "Gracias."],

Set two variable targetNode as text selector and logNode for log destination.

	const translatedText = ["Hola, Me llamo Nabil Ahmad.", "Mucho gusto.", "Gracias."],
	targetNode = document.querySelectorAll("#changeMe ol li"),
	logNode = document.querySelector("#log ul")

Let’s add some variable which we will use later.

	let translationOp, loop, index = 0;

Do it yourself - Step 3

The function changeText will pick the text from list using targetNode selector and current index. Index is initialized with 0. So index will be incremented by 1 when call next and upon calling the same function whenever it become eqal to loop (wait… what is it???!!!) we call translationOp to stop.
First if condition states that when it is the second item from list (remember list index start from 0. So 1 means second list item) we change it’s id to ‘newId’

	function changeText(){
		targetNode[index].innerText = translatedText[index];
		if(index === 1){
			targetNode[index].setAttribute('id', 'newId');
		}

		index += 1;
		if(index === loop){
			window.clearInterval(translationOp);
		}
	}

If you follow along you’ve already recognized that a setInterval method is calling the function and translationOp variable is keeping the setInterval as a reference. and if it equals to loop (again !!!) it will stop.

Until now if it looks confusing, don’t stop. Continue the reading. I’m sure you will catch up within a minute.

Here are the initialization function init and the mysterious loop which is actually a normal variable has the length of our list of text. We’re creating a setInterval method which is calling our changeText each 3s until stopped.

	function init(){
		loop = targetNode.length;
		translationOp = window.setInterval(changeText, 3000);
	}
	// initialize it
	init();

I hope this section is clear. If not run the code. Revise whole code top to bottom carefully. For reference here’s the final version on codepen


Do it yourself - Step 4

So far so good!

Now we will create the mutation observer.

First add observerTarget selector and a variable called observer on top of the script.

	//...rest of the const variables
	observerTarget = document.querySelector("#changeMe");

	//...rest of the initializer variables
	observer;

Now implement observeChange function. The whole code is already documented. So I am giving a short brief on that.-

	function observeChange(){
		//callback function
		var mutationNotifier = function(mutationsList) {
		    mutationsList.forEach(function(mutation, index){
		    	// log mutation
		    });

		    if(index === loop) stopObserver();
		};

		//create an instance with callback function
		observer = new MutationObserver(mutationNotifier);

		//create options
		var config = {
			attributes: true, // observe attributes (ex. id, class)
			attributeOldValue: true, // record value before mutation
			attributeFilter: ["id"], // only observe this list of attributes
			characterData: true, // observe target node's data
			characterDataOldValue: true, // record target node's data before mutation
			childList: true, // observe all child elements including text nodes
			subtree: true // target node & all of its descendants, false = only target node
		};

		//call mutationobserver
		observer.observe(observerTarget, config);

		//stop observer when done
		var stopObserver = function(){
			observer.disconnect();
		};
	}

Let’s initialize the observeChange from main function init.

	function init(){
		//...rest code
		observeChange();
	}

Do it yourself - Step 5

Now observer is watching. But how do we know it’s working?

Let’s show some log output to watch if it’s working or not.

Create a addLog function which will add our log item on logNode (previously declared) target.

	function addLog(logItem){
		logNode.insertAdjacentHTML('beforeEnd', logItem);
	}

Now add a log after calling mutation observer.

	// call mutation observer
	addLog("<li class='log'>Observer started!</li>");

Also, add a log within stopObserver function when observer disconnect.

	var stopObserver = function(){
		addLog("<li class='log'>Observer stopped!</li>");
		// rest code
	};

The only place left is within mutation observer callback function mutationNotifier. Each mutation object has a set of value which is return after mutation/change happens. Detail on each key has been given below.

Within switch method, I am checking for type of mutation and based on that adding to log.

	var mutationNotifier = function(mutationsList) {
	    mutationsList.forEach(function(mutation, index){

	    	// newly added

	    	/************************************
	    	* Mutation record/result (mutation) *
			*************************************
			* addedNodes: Return the added nodes [NodeList, default: []]
			* attributeName: Return the attribute name that changed [String, default: null]
			* attributeNamespace: Return the namespace of the changed attribute [String, default: null]
			* nextSibling: Return the nextSibling of changed node [Node, default: null]
			* oldValue: Return the nextSibling of changed node [Node, default: null]
			* previousSibling: Return the nextSibling of changed node [Node, default: null]
			* removedNodes: Return the removed nodes [NodeList, default: []]
			* target: Return the target node where mutation happens [Node, default: null]
			* type: Return the type of mutatation (ex. 'attributes', 'childList') [String]
	    	*/

	    	switch(mutation.type){
	    		case "attributes":
	    			addLog("<li class='log'><span class='type'>"+ mutation.type +"</span>List <span class='change'>"+ mutation.target.dataset.id + "</span> Attribute <span class='change'>" + mutation.attributeName + "</span> changed to <span class='change'>" + mutation.target[mutation.attributeName] + "</span> (was <span class='change'>" + mutation.oldValue + "</span>) </li>");
	    			break;
	    		case "childList":
	    			addLog("<li class='log'><span class='type'>"+ mutation.type +"</span> List <span class='change'>"+ mutation.target.dataset.id + "</span> changed</li>");
	    			break;
	    		default:
	    			break;
	    	}
	    });

	    //...rest code
	};

That’s it. We’ve implemented a basic mutation observer and observe the text change. Please check the resources section for complete code link and other few resources I recommend.


Resources


Conclusion

This mutation observer is really helpful for javascript developers as well as for a/b test developers.


comments powered by Disqus
rss facebook twitter github youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora quora