33Days

33 days since this post was last revised. It's possible some minor details may have changed.

I had a play with the JavaScript MutationObserver API recently and came away very impressed. I’m already considering all the places I could probably tidy up code by making use of them. In case you haven’t heard about them before, here’s a little primer.

MDN describes the MutationObserver interface as:

The MutationObserver interface provides the ability to watch for changes being made to the DOM tree.

Think if it like an Event Listener for changes to DOM elements and I don’t think you’re far off.

Support is also good. Back to IE11 plus all the ever-green browsers on desktop. On mobile, it’s Android 4.4 > and iOS6.

A basic example

At this point let me demonstrate a quick example. Suppose we have a contenteditable piece of text and we want to do something when a user edits that text. For this example, we’d like to know what the text was before the user pressed a key.

See the Pen MutationObserver by Ben Frain (@benfrain) on CodePen.

So, given this markup:

<div class="container">
    <div class="value" contenteditable="true">type something here</div>
</div>

<div class="previous"></div>

We can use this JavaScript code:

const container = document.querySelector(".container");
const previous = document.querySelector(".previous");

const mutationConfig = { attributes: true, childList: true, subtree: true, characterData: true,
    characterDataOldValue: true };

var onMutate = function(mutationsList) {
    mutationsList.forEach(mutation => {
        previous.textContent = mutation.oldValue;
    });
};

var observer = new MutationObserver(onMutate);
observer.observe(container, mutationConfig);

On each keypress we can see what the prior string of text was. We don’t have to store off the existing value to a variable or worry about listening to keyup events, it’s just there in the mutation MutationRecord that gets provided with each Mutation. If you log out the mutation inside the forEach above you can see the MutationRecord in the console. I’ve listened to characterData but if you were inserting/removing DOM nodes you could see that detailed too.

Anatomy of writing a MutationObserver

Right, now we get what they can do for us, what’s the code above actually doing.

First off, we’re just grabbing the container element. Notice that we are grabbing the parent of the element where the changes happen? That’s because you can set the scope of an MutationObserver to be as tight or as wide as you need. We are also grabbing the `previous` element where we write in what the text was previously.

const container = document.querySelector(".container");
const previous = document.querySelector(".previous");

Next is the configuration that I’ll want to pass to the MutationObserver presently:

const mutationConfig = { attributes: true, childList: true, subtree: true, characterData: true,
    characterDataOldValue: true };

I don’t need to separate that into a separate `const`, I could just easily pass it in when I call the method like this instead:

observer.observe(container, { 
    attributes: true,
    childList: true,
    subtree: true,
    characterData: true,
    characterDataOldValue: true 
});

Next up is the function I want to run when any mutations are observed. I have inventively called this onMutate:

var onMutate = function(mutationsList) {
    mutationsList.forEach(mutation => {
        previous.textContent = mutation.oldValue;
    });
};

This is passed the mutation list and for each of those (the mutation) I am writing in the oldValue from the mutation into the DOM. At the risk of stating the obvious, you can whatever you want here give the enormity of what’s available in the MutationRecord.

You actually create a MutationObsever with the new keyword and name the callback you want to run when a mutation is observed:

var observer = new MutationObserver(onMutate);

Now we have our observer we can observe it like this:

observer.observe(container, mutationConfig);

We pass the element we want to observe and the configuration by which we should process any mutations.

Options for MutationObserver

It is worth knowing that the MDN page currently omits details of the options available in the MutationObserver config. This is detailed in the specification at https://dom.spec.whatwg.org/#mutationobserver.

For completness they are:

  • childList
  • attributes
  • characterData
  • subtree
  • attributeOldValue
  • characterDataOldValue
  • attributeFilter

Of interest for our little demo are the characterData and characterDataOldValue options. Without these we wouldn’t see anything. The options are a great way to tune out some of the noise based upon your requirements.

The MutationObserver also has a takeRecords() method that returns whatever is in the record queue and a disconnect method which stops the observer.

Summary

The MutationObserver API seems to provide a very clean way to deal with DOM changes outside of the more usual input/form element handlers. Support is excellent and the API is mercifully simple and, for me at least, very logical.

If, like me, you hadn’t tried them out previously, I’d encourage you to give them a whirl.

Update

Robert Smith pointed out (via Twitter) that Kent C. Dodds has a DOM Testing Library that makes good use of MutationObserver. This via Kent on Twitter:

dom-testing-library’s waitForElement uses MutationObserver to know as soon as possible when to call your callback and check whether the element you’re waiting for is available! Very interesting API!

Examples are definitely out there in the wild although it seems there isn’t as much take-up as there perhaps should be.

Ben Frain Developer, Author: 'Enduring CSS', 'Responsive Web Design with HTML5 & CSS3'.