Mutation Observer

A built-in object that observes a DOM element, firing a callback in case of modifications is known as MutationObserver.

Let’s start at the syntax and then cover use cases to show where it can be especially handy.

The Mutation Observer Syntax

MutationObserveris simple in usage.

The first step should be creating an observer using a callback-function, like this:

let observer = new MutationObserver(callback);

The next step is attaching it to a DOM node, as follows:

observer.observe(node, config);

Config is an object that has options specifying the changes to respond to:

  • childList: modifications in the direct children of the node.
  • subtree: inside all the node descendants.
  • attributes: the node attributes.
  • attributeFilter: an array of attribute names for observing only the selected ones.
  • characterData: to observe the node.data or not.

The callback is run after any changes. The changes are transferred to the first argument as a list of MutationRecord objects, and the observer becomes the second object.

The MutationRecord object includes the following properties:

  • type: the type of mutation. It is one of "attributes", "characterData", and "childList".
  • target: that’s where the change happens.
  • addedNodes/removedNodes: the added or removed nodes.
  • previousSibling/nextSibling: the previous/next sibling to added or removed nodes.
  • attributeName/attributeNamespace: the changed attribute name or namespace.
  • oldValue: the previous value merely for text or attribute changes, in case the matching option is set attributeOldValue/characterDataOldValue.

Let’s check out an example, including <div> with the contentEditable attribute, which allows focusing on it and editing:

<!DOCTYPE html>
<html>
  <body>
    <div contentEditable id="elemId">Click and <b>edit</b>...</div>
    <script>
      let observer = new MutationObserver(mutationRecords => {
        alert(mutationRecords); // alert(the changes)
      });
      
      // observe everything except attributes
      observer.observe(elemId, {
        childList: true, // observe direct children
        subtree: true, // lower descendants too
        characterDataOldValue: true, // pass old data to callback
      });
      
    </script>
  </body>
</html>

Running this code in the browser will demonstrate one mutation, like here:

mutationRecords = [{

  type: "characterData",

  oldValue: "edit",

  target: <textNode> ,
  // other properties empty
}];

After making more complicated editing operations, the mutation event can contain several mutation records, like this:

mutationRecords = [{

  type: "childList",

  target: <div# elem> ,

  removedNodes: [ <b> ],

  nextSibling: <textNode> ,

  previousSibling: <textNode>
    // other properties empty
}, {

  type: "characterData"

  target: <textNode>
    // the mutation details depend on how the browser handles such deletion
    // it can combine two adjacent text nodes "edit " and ", please" into one node
    //... or it may leave them separate text nodes
}];

So, with MutationObserver, you can respond to any changes inside the DOM tree.

Using for Integration

Now, let’s see when it can be especially useful.

MutationObserver can help you detect the unwanted element in the DOM and remove it.

In other situations when a third-party script adds something in the document, and you want to detect it for adapting the page.

With MutationObserver, you can do that.

Using for Architecture

In some circumstances, MutationObserver is handy from architectural standpoint.

Imagine creating a programming website.

A snippet in an HTML markup will look as follows:

...
<pre class="language-javascript"><code>
  // here's the code
  let welcome = "W3Docs";
</code></pre>
...

There is a method of Prism.highlightElem(pre) that examines the contents of pre elements, adding into them particular tags and styles for colored syntax highlighting. This method can be run on DOMContentLoaded or at the bottom of the page. At that moment, the DOM is ready, and you can look for elements pre[class*="language"] and call sm.highlightElem like this:

// highlight the all code snippets on the page
document.querySelectorAll('pre[class*="language"]').forEach(Prism.highlightEl);

Now, imagine that you need to dynamically fetch materials from a server:

let article = /* fetch a new content from server */
articleEl.innerHTML = article;

The new article HTML may include code snippets. The Prism.highlightEl should be called to highlight them.

That call can be appended to the code that loads an article, as follows:

let article = /* fetch new content from server */
articleEl.innerHTML = article;
let snippets = articleEl.querySelectorAll('pre[class*="language-"]');
snippets.forEach(Prism.highlightEl);

However, for the places in the code where contents such as quizzes, articles, it’s not convenient and can be forgotten.

There is another option, as well.

The MutationObserver can be used to automatically detect once code snippets are placed in the page highlighting them.

Dynamic Highlight

Now let’s get to a working example.

Running the code below will invoke observing the element below, as well as highlighting the code snippets emerging there, like this:

<!DOCTYPE html>
<html>
  <body>
    <div id="highlightDemo">Example</div>
    <script>
      let observer = new MutationObserver(mutations => {
        for(let mutation of mutations) {
          // examine new nodes, there is something to highlight
          for(let node of mutation.addedNodes) {
            // we only track elements, skip other nodes
            if (!(node instanceof HTMLElement)) continue;
            // check the inserted element for being a code snippets
            if (node.matches('pre[class*="language-"]')) {
              Prism.highlightElement(node);
            }
            // or maybe there is a code snippet somewhere in its subtree 
            for(let elem of node.querySelectorAll('pre[class*="language-"]')) {
              Prism.highlightElement(elem);
            }
          }
        }
      });
      let demoEl = document.getElementById('highlightDemo');
      observer.observe(demoEl, {childList: true, subtree: true});
           
    </script>
  </body>
</html>

Below, you can find an HTML-element and JavaScript that can dynamically fill it with innerHTML.

If you execute the code above, then the one below, MutationObserver will find and highlight it.

<!DOCTYPE html>
<html>
  <body>
    <div id="highlight-demo">Example</div>
    <script>
      let demoElem = document.getElementById('highlight-demo');
      // dynamically insert content with code snippets
      demoElem.innerHTML = `A code snippet is below:
        <pre class="language-javascript"><code> let welcome= "W3Docs"; </code></pre>
        <div>Another one:</div>
        <div>
          <pre class="language-css"><code>.class { margin: 25px; } </code></pre>
        </div>
      `;
          
    </script>
  </body>
</html>

Summary

MutationObserver can respond to changes inside DOM: added and removed elements, text content, and attributes.

It can be used for tracking changes represented by other parts of the code, as well as for integrating with third-party scripts.

MutationObserver is capable of tracking any changes.




Do you find this helpful?

Related articles