Mutation Observers to The Rescue
No, not X-Men superfans, but Javascript’s MutationObserver
interface!
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:
- create a new observer
- tell it what to do when mutations happen
- 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
orcharacterDataOldValue
options totrue
, respectively. - By default, a mutation observer will only observe the node you specify; to watch the entire subtree it contains, set the
subtree
option totrue
.
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
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.
Each mutation record has a number of useful properties, including:
- The
type
:childList
,attributes
, orcharacterData
, 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 aCharacterData
node. Note, this may or may not be the originaltargetNode
you’re observing — ifsubtree: true
it could be any descendant of thetargetNode
. 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 isattributes
. 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. Forattributes
, it’s the value before the mutation, forcharacterData
, 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.
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.