TMNT Secrets of the MutationObserver

I recently read an article about the lack of real world examples for using the MutationObserver and it got me thinking about where might CTCT take advantage of this nifty API? Oh yeah, BTW, the MutationObserver gives you hooks into when DOM nodes are added, modified, or removed. The following monologue chronicles my thoughts as I tinkered with the API. I realize this is a long email so to keep you interested I’ve included references to Teenage Mutant Ninja Turtles because what self-respecting geek doesn’t like TMNT?

TLDR; The MutationObserver may prove useful for utility/framework-like code that handles crosscutting concerns.

It occurred to me that if you are in total control of your page you very likely know when nodes get added, modified, or removed and as such have little need for detecting these events. But if you are in complete control that means you are doing absolutely everything yourself, and within Constant Contact this is rarely the case. The conglomeration of UIs known as Toolkit is an excellent example of this. Every page has crosscutting concerns which are handled by external Javascript (JS). So if you are paranoid about what 3rd parties are doing to your Document Object Model (DOM) or if you are writing needfully intrusive scripts consider looking at the MutationObserver.

One such example of a crosscutting concern is the Site Chrome that gives our apps that fresh Toolkit look. To make it consistent we inject JS and CSS into every page. The first version of the code which makes the Toolkit header waited for $(document).ready() to hunt down the “.banner-container” and modify it. Two weeks later we removed the $(document).ready() and executed the selector immediately because waiting was unnecessary and caused a delay which resulted in an ugly FOUC (flash of unstyled content). We were able to ditch the $(document).ready() because we knew the JS was being loaded after the targeted element.

Which is a helpful tip of its own. You don’t have to wait for document ready in some cases. If the MutationObserver had been a viable option at the time we could have leveraged it in the banner. Lets take a look at how that might have worked.

MutationObserver in the banner

var observer = new MutationObserver(function(mutations){
 mutations.forEach(function(mutant){
  mutant.addedNodes.forEach(function(newNode){
   if(newNode.matches(".banner-container")){
     newNode.innerHTML = "Some Toolkit Banner Markup";
    }
  });
 });
}); 

observer.observe(document.documentElement, {
  childList: true, // watch for addition and removal of nodes in the list of child elements
  subtree: true // watch for mutations on descendant elements });

With this approach the browser calls our handler when the banner-container element is being parsed, giving us an opportunity to modify it immediately.

DRYer approach

There’s a lot of code there that quickly starts to feel repetitious when using this pattern for multiple tasks.
So lets DRY it up with a helper function that takes a selector to be matched and a function which mutates the matched nodes.

function Mutator(selector, mutationFunc){
 var observer = new MutationObserver((mutations)=>{
   mutations.forEach((mutant)=>{
    mutant.addedNodes.forEach((newNode)=>{
     if(newNode.matches(selector)){
      mutationFunc(newNode);
      }
   });
  });
});

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

We could use that helper to intercept new nodes with the class “teenage-turtle” and add to them a class of “mutant-ninja”:

  Mutator(".teenage-turtle", (targetNode)=>{
    targetNode.classList.add("mutant-ninja");
  });

The output of which looks like: unnamed Hopefully the TMNT reference got you interested enough to keep reading 🙂 Another real world example of a crosscutting concern is the Multi User Privileges plugin that scans many pages for annotated elements and modifies their display or behavior. We could create a greatly simplified version which hides elements like so:

  window.userPrivileges = ["can-see-turtles"];
Mutator("[data-privilege]",(targetNode)={
 if(window.userPrivileges.indexOf(targetNode.getAttribute("data-privilege"))==-1){
   targetNode.style.display="none";
  }
 }); 

Would result in markup that looks like:

mutated-html-annotated

Again, this would process nodes as they are being added to the page rather than waiting for the whole page to load. It has the added benefit that it would catch any new nodes coming on to the page after load as well. That means your app doesn’t need to be littered with privilege specific code if you generate more UI after page load.

But we still have the problem of getting our script on the page before all the nodes it would want to modify. Since that doesn’t sound like something we can guarantee we’ll have to create a fallback that handles unprocessed elements which are found when the DOM finishes loading.

 function Mutator(selector, mutationFunc){
  const MUTATED_ATTR = "data-mutated";
   const MUTATED_BY_ATTR = "data-mutated-by";
    var observer = new MutationObserver((mutations)=>{
     mutations.forEach((mutant)=>{
      mutant.addedNodes.forEach((newNode)=>{
       if(newNode.nodeType == 1 && newNode.matches(selector)){
        mutationFunc(newNode);
        newNode.setAttribute(MUTATED_ATTR,true);
        newNode.setAttribute(MUTATED_BY_ATTR,"MutationObserver");
       }
     });
    });
  });
 
  observer.observe(document.documentElement, {
    childList: true,
    subtree: true
  });

 document.addEventListener("DOMContentLoaded",()=>{
   document.querySelectorAll(selector).forEach((matchedNode)=>{
     if(!matchedNode.getAttribute(MUTATED_ATTR)){
        mutationFunc(matchedNode);
        matchedNode.setAttribute(MUTATED_ATTR,true);
        matchedNode.setAttribute(MUTATED_BY_ATTR,"DOMContentLoaded");
     }
   });
  });
 }

The resulting markup might look like this

Now our MutationObserver catches all the nodes coming onto the page after it is loaded and our DOMContentLoaded handler will catch the ones that we missed from before the script was loaded.

Keep in mind that the code above is the output of me playing around to learn something new. I DO NOT intend to pass it off as production ready, complete, bug free, supported by all browsers, etc., etc.

Hope that helps.

What are your observations after working with MutationObserver? – Share them in the comments section below!

Leave a Comment