Wednesday, October 9, 2024

Respond to DOM Changes with Mutation Observers

Respond to DOM Changes with Mutation Observers

Earlier this year, the World Wide Web Consortium (W3C) introduced the DOM4 specification, which includes DOM Mutation Observers. These replace the long-standing DOM Mutation Events, which never really caught on due to their slowness, verbosity, and propensity to crash the browser! Unlike Mutation Events, which fired an event for every change to the DOM, the new Mutation Observers utilize a far superior asynchronous callback mechanism that can be invoked after multiple changes in the DOM. In today’s article, we’ll learn how to create and configure DOM Mutation Observers as well as interpret their results.

What Mutation Events/Observers Do

Both Mutation Events and Mutation Observers serve the same purpose, which is to inform your script of changes to the DOM structure. In fact, the word “mutation” is a synonym for “change”. As such, it closely resembles the “mutator” moniker ascribed to setter functions.

Mutation Events relied on special DOM-specific event identifiers that you could attach an event listener to:

var insertedNodes = [];
document.addEventListener("DOMNodeInserted", function(e) {
        insertedNodes.push(e.target);
    },
    false);
console.log(insertedNodes);

Mutation Observers have their own constructor that accepts a callback function. Once instantiated, the MutationObserver instance’s observe() method can be invoked a specific element, or on the entire document, as in the example below:

var insertedNodes = [];
var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
        for (var i = 0; i < mutation.addedNodes.length; i++) {
            insertedNodes.push(mutation.addedNodes[i]);
        })
});
observer.observe(document, { childList: true });
console.log(insertedNodes);

Mutation Event Types

The mutation event types of Mutation Events are conspicuously absent from the MutationObserver constructor. Instead of receiving the mutation event types up-front, the MutationObserver monitors a given node for all mutator events and passes them to the callback function via a property of the function argument called “type”. More on that in a bit…

The second argument is an options object of boolean attributes. Here is a list and description of each:

  • 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.

MutationObserver Results

Each MutationObserver instance maintains an array of MutationRecords which is passed to the observe() method’s callback function as the first parameter. Since the structure of the MutationRecord remains constant regardless of the mutation type reported, not all attributes apply to every event. In fact, the majority of them will be null or contain an empty object. For now, let’s take a look at each attribute and what kind of information they contain. In the next section, we’ll look at a real-life example of MutationRecords:

  • 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.

MutationRecord Type Attribute

The first attribute that you’ll want to check is the type since it’s the one that determines what other attributes may be useful to you. There are three types to look for:

  • “attributes”: An attribute changed on the target node.
  • “characterData”: The text content (a child text node) value of the target node changed.
  • “childList”: A child (or subtree descendent) of the target node was added or removed.

A Working Example

Here is some code to help understand how all of the above components fit together. This MutationObserver monitors the entire document for mutations and outputs their details to console:

var observer = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    console.log('Mutation type: ' + mutation.type);
    if ( mutation.type == 'childList' ) {
      if (mutation.addedNodes.length >= 1) {
        if (mutation.addedNodes[0].nodeName != '#text') {
           console.log('Added ' + mutation.addedNodes[0].tagName + ' tag.');
        }
      }
      else if (mutation.removedNodes.length >= 1) {
        console.log('Removed ' + mutation.removedNodes[0].tagName + ' tag.')
      }
    }
    else if (mutation.type == 'attributes') {
      console.log('Modified ' + mutation.attributeName + ' attribute.')
    }
        });   
});
 
var observerConfig = {
        attributes: true,
        childList: true,
        characterData: true
};
 
// Listen to all changes to body and child nodes
var targetNode = document.body;
observer.observe(targetNode, observerConfig);

// Let's add a sample node to see what the MutationRecord looks like
document.body.appendChild(document.createElement('ol'));
document.body.removeChild(document.querySelector('ol'));
document.body.setAttribute('id', 'booooody');

Upon running the above code, you should see something like the following in the browser console:

Mutation type: childList
Added OL tag.
Mutation type: childList
Removed OL tag.
Mutation type: attributes
Modified id attribute.
Mutation type: childList

Conclusion

According to caniuse.com, the MutationObserver is supported by most major browsers, with the exception of Opera Mini. A couple of points to bear in mind: the WebKitMutationObserver of iOS 6 has a known bug that it doesn’t trigger “childList” changes until you bind one of the deprecated mutation events such as “DOMNodeInserted” to the node which is watched by observer. Also, IE 11 does not include the child nodes when they are removed by setting the innerHTML property of an element.

Rob Gravelle
Rob Gravelle
Rob Gravelle resides in Ottawa, Canada, and has been an IT guru for over 20 years. In that time, Rob has built systems for intelligence-related organizations such as Canada Border Services and various commercial businesses. In his spare time, Rob has become an accomplished music artist with several CDs and digital releases to his credit.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Popular Articles

Featured