Introduction
Have you ever integrated a third-party widget, only to find that it unexpectedly disrupts your carefully crafted layout or breaks key functionality? You're not alone. The unpredictable nature of these widgets can cause headaches for even the most experienced developers. But there's a way to regain control—MutationObserver is your secret weapon for monitoring and managing these hidden DOM changes before they wreak havoc.
Problem
Let's say you want to make some layout decisions based on the presence of some third party widget and that third party data is injected directly into a wrapper element in your code. Now it can take some time to load that widget or in worst case it doesn't load successfully. How will you detect the required widget is loaded and you can make the required layout changes.
Solution
One solution is polling. You use setInterval()
or setTimeout()
to check the presence of the widget but it may be possible that the widget never loads. Running polling again and again will effect the performance of the webpage.
The another more efficient solution is MutationObserver
So what is a Mutation Observer in JS?
Mutation Observer
The MutationObserver API in JavaScript allows you to monitor changes to the DOM (Document Object Model). It’s a powerful tool for detecting and reacting to changes such as adding, removing, or modifying elements on a webpage.
How it works
The instance
Let's take a look at MutationObserver
instance:
//create instannce for observer
let observer = new MutationObserver(callback);
//start observing
observer.observe(targetNode, config);
We instantiate an observer with a callback by using MutationObserver
instance. This callback will be executed when the observer notices a mutation in the DOM element which we set the observer on.
.observe()
is used to attach the observer to a DOM Node. It expects two inputs:
first the targetNode
, the element which we want the observer to attach to, and second the config
Config
Config in an object of boolean options which describes the types of change we want the observer to react to. The most common options are:
childList
– changes in the direct children ofnode
subtree
– in all descendants ofnode
,attributes
– attributes ofnode
,attributeFilter
– an array of attribute names, to observe only selected ones.characterData
– whether to observenode.data
(text content)
The Callback
Below is the sample code for the callback.
//callback that will run when mutation is observed
let mutationObserverCallback = function (mutationlist) {
console.log("# mutation list", mutationlist);
for (let record of mutationlist) {
console.log("# mutation record", record);
}
};
When a callback is executed it receives the changes as the first argument. This argument is a list of MutationRecords
.
Mutation Record contains:
type
– mutation type, one of"attributes"
: attribute modified"characterData"
: data modified, used for text nodes,"childList"
: child elements added/removed,
target
– where the change occurred: an element for"attributes"
, or text node for"characterData"
, or an element for a"childList"
mutation,addedNodes/removedNodes
– nodes that were added/removed,previousSibling/nextSibling
– the previous and next sibling to added/removed nodes,attributeName/attributeNamespace
– the name/namespace (for XML) of the changed attribute,oldValue
– the previous value, only for attribute or text changes, if the corresponding option is setattributeOldValue
/characterDataOldValue
Example Code:
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="index.css" />
<title>Mutation Observer</title>
</head>
<body>
<div class="my-content">
<h1 class="my-content-header">Mutation Observer</h1>
<p class="my-content-body">
Let's say below is the sample third party widget that is being populated
after page load, that(widget's DOM) we don't have control on
</p>
</div>
<div class="my-third-party-wrapper">
<div class="my-third-party-header">Third Party Simulated Widget</div>
<!-- Widget content will be injected in below div so we want to observe this div for mutation -->
<div id="my-third-party-data"></div>
</div>
</body>
<script src="../../src/simulateThirdParty.js"></script>
<script src="../../src/index.js"></script>
</html>
index.js
//target node which we want to observe for change
let thirdPartyWidget = document.getElementById("my-third-party-data");
//configuration for observer
let config = {
childList: true,
subtree: true,
attributes: true,
};
//callback that will run when mutation is observed
let mutationObserverCallback = function (mutationlist) {
console.log("# mutation list", mutationlist);
for (let record of mutationlist) {
console.log("# mutation record", record);
}
};
//create instannce for observer
let observer = new MutationObserver(mutationObserverCallback);
//start observing
observer.observe(thirdPartyWidget, config);
See the whole code here
Best Practices
Make sure to disconnect the MutationObserver once you are done and you longer need to track for changes in DOM as keeping the observer on, it can lead to memory leaks and performance issue.
To disconnect the mutation observer, you can use .disconnect()
method.
observer.disconnect()
disconnect()
make sure that we are no longer listening to mutations and hence the garbage collection for observer is done successfully not allowing memory leakages.