Three Real-World Uses for Mutation Observer

MutationObserver is a lesser known JavaScript feature which allows you to detect when elements in a web page are inserted, changed or removed. It is still relatively new, but it is supported by every modern browser.

The web is full of demos and tutorials of MutationObserver, but it’s pretty hard to find examples of it actually being used in practice. Even a search of Github is almost all libraries and test cases. We’ve had a couple occasions to use it at Eager however, which I now have the opportunity to share.

Client-side Image Optimization

Believe it or not, it’s actually possible to swap the src’s of img tags before the browser begins to load them. We can use that to optimize our images without changing the HTML source of our page. This code uses a FireSize service to handle the actual optimization.

We start by setting up a MutationObserver which will call our checkNode function with any new nodes which are added to the DOM:

var observer = new MutationObserver(function(mutations){
  for (var i=0; i < mutations.length; i++){
    for (var j=0; j < mutations[i].addedNodes.length; j++){
      checkNode(mutations[i].addedNodes[j]);
    }
  }
});

observer.observe(document.documentElement, {
  childList: true,
  subtree: true
});

If we run this code early in the head of the page, it will call our checkNode function with each DOM node as the browser parses the page’s HTML. This gives us the ability to check or mutate these nodes before they’ve ever been rendered.

We can define our checkNode function to decide if this is an image for us to optimize.

checkNode = function(addedNode) {
  if (addedNode.nodeType === 1 && addedNode.tagName === 'IMG'){
    addedNode.src = optimizeSrc(addedNode.src)
  }
}

Finally, we can define optimizeSrc to switch out our image’s src for an optimized one:

optimizeSrc = function(src) {
  return "//firesize.com/" + src;
}

For a complete implementation, take a look at our FireSize app source code.

Initializing When An Element Becomes Available on the Page

It’s a common pattern to wait for jQuery.ready or DOMContentLoaded to initialize code which depends on elements on the page. Those events don’t fire until the entire DOM has loaded however, meaning the page will start to be rendered before you have a chance to change or add to its content.

Our pattern from the image optimization solution also works for detecting when any element becomes available, allowing you to initialize code which depends on that element at the exact first moment it’s possible. We can redefine checkNode to instead check if our element matches an arbitrary selector:

checkNode = function(addedNode) {
  if (addedNode.nodeType === 1){
    if (addedNode.matches('.should-underline')){
      SmartUnderline.init(addedNode);
    }
  }
}

Managing ContentEditable Regions

As you may know, the contenteditable attribute can be used to make any HTML element editable by the user. For example:

<div contenteditable>
  <h1>My awesome content!</h1>
  <p>You can edit this</p>
</div>

Will produce this editable element:

My awesome content!

You can edit this

If you are truely building an editor, it’s common to want some control over what the user can enter. You can use MutationObservers to prevent certain modifications, or take action when they occur. For example, lets say we want some (very basic) support for ‘markdown-style’ links in our editable area.

We begin with the standard MutationObserver binding, this time paying attention to changes in the characterData of our fields, and calling a function which we will later define called replaceLinks:

var observer = new MutationObserver(function(mutations){
  for (var i=0; i < mutations.length; i++){
    replaceLinks(mutations[i].target);
  }
})

observer.observe(document.querySelector('[contenteditable]'), {
  characterData: true,
  subtree: true
})

The replaceLinks function itself uses a regular expression to find markdown-style links and swap them out for HTML links:

function replaceLinks(target){
  // Replace markdown encoded links
  // a la [Text](URL)
  // with <a href='URL'>Text</a>
  var content = target.textContent.replace(
    /(.*)\[([^\]]+)\]\(([^\]]+)\)(.*)/g,
    "$1<a href='$3'>$2</a>$4"
  )

  if (content !== target.textContent){
    var newNode = document.createElement('template')
    newNode.innerHTML = content
    target.parentElement.replaceChild(newNode.content, target)
  }
}

This is, by necessity, a simplified example. I know of at least one markdown editor which does try to do this, with pretty user-hostile consequences. Please don’t actually do this type of dynamic replacement of text without careful consideration. You could, however, insert a new element to show a menu, or something similar.

Here’s a full demo of the working code. Try adding a [markdown-style](link):

My awesome content!

You can edit this


Hopefully that gave you some ideas for how you might use MutationObserver in your future projects. Our next post is going to be on the history of CSS and what (very interesting) alernatives were considered instead. Subscribe below to be notified when it’s released.

Like this post? Share it with your followers.

Sign up to get our next post delivered to your inbox.

Follow us to get our latest updates.