Mutation Observers to The Rescue

Nora Brown
ITNEXT
Published in
5 min readSep 12, 2018

--

No, not X-Men superfans, but Javascript’s MutationObserver interface!

A red eft — the terrestrial juvenile stage of the aquatic eastern newt. ©Nora Brown

Sometimes, you don’t have much control over a site’s code. Maybe you’re working on a client site built with Squarespace, or with Wordpress + plugins + more plugins. When you want to make an enhancement or change a behavior in these situations, Javascript is usually the tool we turn to. But sometimes, even the usual Javascript hooks aren’t available.

For example, one common pattern is to wait for a page’s DOM content to be loaded, then access some element on the page, and make a change to it. But what if the element you want to access is loaded dynamically, after the page’s load event has fired?

Or perhaps you want to react to something that happens on a page, but the developer of whatever code is triggering that something hasn’t published a friendly list of events you can listen to. Maybe there isn’t even a proper event fired (shocking!).

Enter MutationObserver, stage left

We can turn to the well-supported MutationObserver for a little help. It’s easy to understand and implement, and is supported by modern browsers post-IE10. It’s pretty awesome, really.

The general pattern is to:

  1. create a new observer
  2. tell it what to do when mutations happen
  3. direct it to observe the element you want, with certain options:
// DOM element we want to observe
var targetNode = ...;
// Options for the observer
var config = { ...options... };
// Callback will execute when mutations are observed
var callback = function(mutationsList){ ...do stuff... };
// Create a new observer, passing in the callback function
var observer = new MutationObserver(callback);
// Start observing the targetNode with the given configuration
observer.observe(targetNode, config);

The observer configuration has several options:

  • Most importantly, it describes what to observe: childList, attributes, characterData, or any combination.
  • If you’re monitoring attributes, you can additionally specify an attributeFilter, which limits the observed attributes to those in the array you provide.
  • If you’re watching attributes or character data, you can record the pre-mutation values by setting attributeOldValue or characterDataOldValue options to true, respectively.
  • By default, a mutation observer will only observe the node you specify; to watch the entire subtree it contains, set the subtree option to true.

So a configuration might look something like:

var config = {
attributes: true,
attributeOldValue: true,
attributeFilter: ['class'],
subtree: true
}

The above would configure our observer to watch for changes to the class attribute on our target node and any descendant nodes, and keep track of the pre-mutation value.

Manipulating dynamically-loaded DOM content

Adding a ‘Continue Shopping’ link to a Squarespace cart page

Let’s put a mutation observer to work on our first use-case. We’d like to add a prominent (but also subtle) “Continue Shopping” link on a Squarespace cart page. Easy-peasy, right? Just grab that nice headline, append a link, and Bob’s your uncle (jQuery used here, for brevity):

$('.cart-title').append('<br><a href="/shop-home" style="font-size: .5em">Continue Shopping</a>');

If this is inside a $(document).ready callback, or included before the closing </body> tag, it should work perfectly. Wah-wahh…Unless, of course, the entire cart, including the headline, is added to the DOM after the page has loaded.

No probs. We can use a MutationObserver to watch for changes to the DOM, or a specific part of the DOM, and react when we see our cart-title has been added:

As you can see, the callback for a mutation observer can accept a parameter (mutationsList in the code above), that is an array of MutationRecord objects.

A MutationRecord in Chrome DevTools

Each mutation record has a number of useful properties, including:

  • The type: childList, attributes, or characterData, which is useful if you are observing multiple mutation types, and want to take a different action based on the type.
  • The target: the element whose child nodes or attributes have changed, or a CharacterData node. Note, this may or may not be the original targetNode you’re observing — if subtree: true it could be any descendant of the targetNode.
  • addedNodes, removedNodes, previousSibling, nextSibling: The first two are as advertised, previous and next are relative to the added or removed nodes.
  • attributeName: The name of the attribute that was updated, if the mutation type is attributes. Handy if you’re interested in changes to multiple attributes.
  • oldValue: If you’ve configured the observer to record the pre-mutation values, this is where they’ll be. For attributes, it’s the value before the mutation, for characterData, it’s the character data before the mutation.

Responding to an attribute change

In our second scenario, we want to respond to something that happens on the page — something we don’t control and whose events we don’t have access to. For this example, let’s imagine an “Add to Cart” action: the user clicks the Add to Cart button, the site’s code makes an Ajax call, the item is added to the user’s cart. At that point, you’d like to take the user to the Cart page.

Classes and other attributes change throughout add-to-cart process

Throughout this add-to-cart process, many attributes on the button and surrounding elements are changed, including classes. We can set up a mutation observer to watch them and respond:

Here, since we are not watching the subtree, we know the mutation.target will be our original targetNode. So we can simply wait for the cart-added class to be present in the class list, indicating the add-to-cart process is complete, and then take the user to the cart page.

Other use-cases for Mutation Observers

In both the scenarios above, the Mutation Observer API came to our rescue when we had to resort to watching the DOM for changes, because we had no control over the code making those changes. But there may be other scenarios where the MutationObserver pattern is useful. Imagine you want to react to a new element being added to a page, and there are several ways it may happen (clicking a button, hitting return, an ajax call). Rather than triggering on all of those ways, or using a publish/subscribe pattern, maybe you just watch the DOM for the end result — the new element being added.

Have you used MutationObserver? Leave a comment below! Thanks for reading.

More Info on Mutation Observers

--

--

I design, build, and optimize websites. Currently loving CSS Grid and Vue.js.