MutationObserver API
One of my favorite web tricks was using CSS and JavaScript to detect insertion and removal of a DOM node, detailed in Detect DOM Node Insertions with JavaScript and CSS Animations. The technique and blog post were published during a time when we didn't have a reasonable API for detecting such events. Nowadays we have MutationObserver
, an API made to efficiently detect loads of node operations. Let's have a look!
Basic MutationObserver
API
The MutationObserver
API is a bit complicated for me but here's the basic setup:
var observer = new MutationObserver(function(mutations) { // For the sake of...observation...let's output the mutation to console to see how this all works mutations.forEach(function(mutation) { console.log(mutation.type); }); }); // Notify me of everything! var observerConfig = { attributes: true, childList: true, characterData: true }; // Node, config // In this case we'll listen to all changes to body and child nodes var targetNode = document.body; observer.observe(targetNode, observerConfig);
There's a lot to using the MutationObserver
, but the breakdown is:
- Create an instance of
MutationObserver
with a callback to handle any event thrown its way - Create a set of options for the
MutationObserver
- Call the
observe
method of theMutationObserver
instance, passing it the node to listen to (..and its children) and the option list. - At the time you want to stop observing, call
disconnect
MutationObserver
Options
MDN provides detail on the options for MutationObserver
:
childList
: Set to true if additions and removals of the target node's child elements (including text nodes) 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 an array of attribute local names (without namespace) if not all attribute mutations need to be observed.
That's a lot to be aware of when listening to one node and/or child nodes!
MutationRecord: MutationObserver
Handler Results
The resulting object when a mutation is observed is detailed as well:
type (String)
: Returns attributes if the mutation was an attribute mutation, characterData if it was a mutation to a CharacterData node, and childList if it was a mutation to the tree of nodes.target (Node)
: Returns the node the mutation affected, depending on the type. For attributes, it is the element whose attribute changed. For characterData, it is the CharacterData node. For childList, it is the node whose children changed.addedNodes (NodeList)
: Return the nodes added. Will be an empty NodeList if no nodes were added.removedNodes (NodeList)
: Return the nodes removed. Will be an empty NodeList if no nodes were removed.previousSibling (Node)
: Return the previous sibling of the added or removed nodes, or null.nextSibling (Node)
: Return the next sibling of the added or removed nodes, or null.attributeName (String)
: Returns the local name of the changed attribute, or null.attributeNamespace (String)
: Returns the namespace of the changed attribute, or null.oldValue (String)
: The return value depends on the type. For attributes, it is the value of the changed attribute before the change. For characterData, it is the data of the changed node before the change. For childList, it is null.
Whew. So let's take look at some realistic use cases of MutationObserver
.
Detect When a Node is Inserted
The use case in my Detect DOM Node Insertions with JavaScript and CSS Animations post was detecting node insertion, so let's create a snippet which detects node insertion:
// Let's add a sample node to see what the MutationRecord looks like // document.body.appendChild(document.createElement('li')); { addedNodes: NodeList[1], // The added node is in this NodeList attributeName: null, attributeNamespace: null, nextSibling: null, oldValue: null, previousSibling: text, removedNodes: NodeList[0], target: body.document, type: "childList" }
The resulting MutationRecord shows addedNodes: NodeList[1]
, meaning that a node has been added somewhere lower in the tree. The type
is childList
.
Detect When a Node is Removed
Removing a node shows the following MutationRecord:
// Now let's explore the MutationRecord when a node is removed // document.body.removeChild(document.querySelector('div')) { addedNodes: NodeList[0], attributeName: null, attributeNamespace: null, nextSibling: text, oldValue: null, previousSibling: null, removedNodes: NodeList[1], // The removed node is in this NodeList target: body.document, type: "childList" }
This action also shows a type
of childList
but now removeNodes
now has the NodeList[1]
, a NodeList
with the removed node.
Detect Attribute Changes
If an attribute is changed on any element, you'll be quick to know about it; the MutationRecord will show:
// What do attribute changes look like? // document.body.setAttribute('id', 'booooody'); { addedNodes: NodeList[0], attributeName: "id", attributeNamespace: null, nextSibling: null, oldValue: null, previousSibling: null, removedNodes: NodeList[0], target: body#booooody.document, type: "attributes" }
Also note that the target
will show the node for which the attributes was changed. The oldValue
will display its former value and while a normal getAttribute
check give you the new attributes value.
Stop Listening!
If you're looking to write the ultimate efficient app, you'll only be adding listeners for the period you need them and then removing them when you're done:
observer.disconnect();
The MutationObserver
instance has a disconnect
method to stop listening. Since your app may have many, many DOM operations, you may want the power to disconnect your listener for the duration of the time your user interacts with the page.
The MutationObserver
API seems a tad verbose but it's powerful, informative, and ultimately hack free. Daniel Buchner's brilliant original "hack" provides better support for node addition and removal but MutationObserver
should probably be used if possible.
Do you know of a way to use MutationObserver to detect when an elementโs dimension (width or height) changes? The best cross-browser solution Iโve seen so far is Cross-Browser, Event-based, Element Resize Detection, but that was published two years ago and I wonder if someone has come up with a better solution since.
Hi,
I am using mutation observer on a target node. It fires if any of the child nodes or properties changes.But, I have a case where i had to handle if the target node itself is removed. This is not working.Is there a way to handle this?
You can observe the parent of your โtarget nodeโ. Then, in the
MutationRecord
that is passed to the callback, check theremovedNodes
to see if your โtarget nodeโ is there.I wondered what the performance difference would be between the CSS animation trick and the MutationObserver API.
So I created a rough jsbin to test the two approaches:
http://jsbin.com/hibeji
MutationObserver is consistently faster.
your overview regarding MutationObserver is pretty neat, thanks for that!
one thing though bugs me: why does the property change โcheckedโ on an input field not get tracked?
iโve created a fiddle to illustrate this: enabling/disabling the input field gets tracked fine but checking/unchecking the input field does not.
https://jsfiddle.net/nerdess/5pc5jLxm/2/
this makes no sense to me, maybe someone can explain what is going on hereโฆthanks!
You donโt use MutationObservers for that purpose. You use the
onchange
event.https://www.w3schools.com/jsref/event_onchange.asp
the
onchange
event does not get triggered though when the state of the input (checked/unchecked) changes programmatically. it only works if a user clicks on the inputโฆNice example code, just about the only example online I could find that actually worked correctly. Thanks.
Thanks so much for this post!
Very helpful post that actually helps me understand whatโs going on with MutationObserver! The only hiccup is that after executing the code provided, I was only seeing the text โchildListโ in the console. It didnโt take any effort at all to realize that the sample was logging โmutation.typeโ instead of โmutationโ. Made the quick change (in order to observe the records!) and I was off to the races.
Thanks for the article, David!
Maybe iโm being denseโฆbut in what sort of situations would you actually need to use this? Wonโt it be my own functions that are changing the DOM in the first place?
A common use case for this would be for a
contenteditable
element, where the user can directly alter the DOM tree.**Oliver** , you would use this constructor if you are adding your project to some 3rd party library or some already made site/app and you want to know when something changes in DOM. Knowing this can help you to trigger your functions and add your changes.
Example:
you are waiting for clients page to make some change (lets say function that waits for XHR response in form of JSON that adds some new piece to DOM structure) , by knowing when it was triggered you are able to append your changes after knowing it will not be overwritten.
Lets, say you want to add a โclickโ event to some button, but that button gets changed in process of loading because some other library is making changes after page load, so you can wait for this change and on it add your code.
P.S. this is how I see usage of observer
Hello, thanks for the informative post! Unfortunately,
Object.observe
has now been deprecated in favor of usingProxy
which makes this a bit obsolete. I am only just coming to grips with this change myself, but Iโd very much be interested in your ideas on how best to use Proxy to listen for DOM node elements being added/removed and/or modified/changed. Maybe an idea for a future article? :) Thanks again!My mistake. On further investigation it seems I am talking about two different things. In which case I believe this article is still totally valid. Apologies if I cause anyone confusion.
can we detect some how the change in the css properties of the dom elements? could that be added in MutationObserver API?
some how to detect change in height/width of the element, whether its due to css change or something due to implicit reason while adjusting another new element being added.
Can I observe
this.shadowRoot
withmututionObserver
?What I read helps me to find out *that* something has changed, but in order to find out *when* if has changed, Iโd like to see the trace-stack that led to this mutation. Unfortunately when I print โnew Error().stackโ in my mutattion-observer, it always shows the same stack. Is there some way (perhaps through a different route) to get this info?
Ok, I should have aslked Google before, sorry. Turns out [its not possible](https://stackoverflow.com/questions/53656494/how-to-get-the-function-which-caused-a-dom-mutation-with-a-mutationobserver).