DEV Community

loading...
Cover image for The MutationObserver Web API

The MutationObserver Web API

daviddalbusco profile image David Dal Busco Originally published at daviddalbusco.Medium ・4 min read

I recently developed multiple features across projects with the help of the MutationObserver Web API. A bit to my surprise, I noticed that some colleagues never had used it or, even heard about it before. That’s why I got the idea for this blog post.


Introduction

The MutationObserver interface provides the ability to watch for changes being made the DOM tree (source MDN Web Docs).

It is a web feature, natively implemented in all browsers (yes even Internet Explorer v11 according Caniuse), which allows us to detect when changes are made to a document, to the web page.


In Other Words

I dislike "The Last stand" movie but, do you remember when Rogue gets the vaccine (1) to remove her powers (2)? Without any other information, we still don't know if the cure was effective or not. To resolve the question (3), we would have to try our luck and get in contact but, without knowing what result to expect. On the other hand, thanks to his psychokinesis power, the professor X would him be able to detect the mutation (4) and knows if it worked out or not.

Our web page follows the same idea.

When we apply a modification to the DOM (1), such as modifying a tag or an attribute, with or without framework, it is interpreted and rendered by the browser (2). Even though the operation is really fast, if we query (3) the DOM elements touched by our changes right afterwards, we cannot be 100% sure that the modifications were already applied. Fortunately, thanks to the MutationObserver, we can detect the mutation (4) to get to know when and if it effectively worked out.


Walk-through

To initialize a MutationObserver , you shall invoke its constructor with, as parameter, a callback function to be called when DOM changes occur.

const observer = new MutationObserver(callback);
Enter fullscreen mode Exit fullscreen mode

The callback gets as parameter an array of the individual DOM mutations which have been applied.

To observe a targeted node and to begin receiving notification through the callback, you can invoke the function observe() .

observer.observe(targetNode, config);
Enter fullscreen mode Exit fullscreen mode

As second parameter, a configuration shall be passed. It defines which kind of mutations we are looking to observe. These are documented on the excellent MDN Web Docs. When it comes to me, I often use attributes to observe modifications to style and, class or, as in previous example, childlist to observe changes to the children of an element.

To stop the MutationObserver from receiving further notifications until and unless observe() is called again, the function disconnect() shall be used. It can be called within the callback or anywhere, as long as it is called on the instance.

observer.disconnect();
Enter fullscreen mode Exit fullscreen mode

Last but, not least, it exposes a function takeRecords() which can be queried to remove all pending notifications.


Concrete Example

I was developing some improvements in the WYSIWYG inline editor of DeckDeckGo in which I had to apply a color to the user’s selection, entered via an input field, while preserving the range so that each time the user enters a new color, it would be applied to the same selected text 🤪.

Summarized something like following:

class Cmp {

      private range = window.getSelection()?.getRangeAt(0);

      applyColor() {
        const selection = window.getSelection();

        selection?.removeAllRanges();
        selection?.addRange(this.range);

        const color = document.querySelector('input').value;

        document.execCommand('foreColor', false, color);

        this.range = selection?.getRangeAt(0);
      }

}
Enter fullscreen mode Exit fullscreen mode

It should have worked right? Well, no, it did not or at least not fully 😉.

Indeed, getting and applying the color to the selection did work as expected but, I was unable to save the range afterwards, this.range was not re-assigned as I was expecting.

Fortunately, I was able to solve the issue with the MutationObserver .

class Cmp {

      private range = window.getSelection()?.getRangeAt(0);

      applyColor() {
        const selection = window.getSelection();

        selection?.removeAllRanges();
        selection?.addRange(this.range);

        const color = document.querySelector('input').value;

        // A. Create an observer
        const observer = new MutationObserver(_mutations => {
            // D. Disconnect it when triggered as I only needed it once
            observer.disconnect();
            // E. Save the range as previously implemented
            this.range = selection?.getRangeAt(0);
        });

        // B. Get the DOM element to observe
        const anchorNode = selection?.anchorNode;

        // C. Observe 👀
        observer.observe(anchorNode, {childList: true});

        document.execCommand('foreColor', false, color);
      }
}
Enter fullscreen mode Exit fullscreen mode

First (A) I created a new MutationObserver. I defined which node element, in my case a parent one, had to be observed (B) and, I configured the observer (C) to begin receiving notifications through its callback function when DOM changes occurred. In the callback, I first disconnected (D) it, as only one event was interesting for my use case and finally (E) was able to save the range as expected 🥳.


Go Further

If you liked this introduction about the MutationObserver , I can suggest you to go further and, have a look to the ResizeObserver and IntersectionObserver.

The first one can for example be used to detect changes to the size of editable fields and, the second one to lazy load content.


Summary

You might not use the observers every day but, they are extremely useful when it comes to detecting changes applied to the DOM. In addition, it is fun to develop features with these 🤙.

To infinity and beyond!

David

Cover image source from forum resetera


You can reach me on Twitter or my website.

Give a try to DeckDeckGo for your next slides!

DeckDeckGo

Discussion (7)

Collapse
gaurav5430 profile image
Gaurav Gupta

were you able to figure out the reason why range was not changed immediately? is it because execCommand is asynchronous?

Collapse
daviddalbusco profile image
David Dal Busco Author

My interpretation is that when you apply any change to the DOM the browser has to evaluates / renders it. Generally it is really performant, super fast and almost instant but, in that particular case or often with sizes too, it took a few more milliseconds.

Collapse
gaurav5430 profile image
Gaurav Gupta

i do't think it should depend on how much time the browser takes. The browser has to model this as a synchronous or async functionality. if it is synchronous, the browser would wait for these changes to be applied before evaluating the next line. if it is asynchronous, then it would always be async and you can't expect it sometimes evaluate the next line in time amd sometimes not.

Thread Thread
daviddalbusco profile image
David Dal Busco Author

sorry what I meant, I interpret the browser painting operations as async (the operations which happen inside the browser, not javascript).

Thread Thread
gaurav5430 profile image
Gaurav Gupta

yeah, you are right, the paint / rendering operation is asynchronous, but regular dom manipulation is not. I think the manipulation here using execCommand might be asynchronous, which is why you need to wait.

Collapse
jmau111 profile image
Julien Maury

This is an awesome API, I did not know about it until recently. I'm to bookmark your post, thanks.

Collapse
daviddalbusco profile image
David Dal Busco Author

Cool Julien, content d'entendre ça! Merci pour le feedback 😃.

Forem Open with the Forem app