JavaScript MutationObserver

Summary: in this tutorial, you will learn how to use the JavaScript MutationObserver API to watch for changes being made to the DOM tree.

Introduction to the JavaScript MutationObserver API

The MutationObserver API allows you to monitor for changes being made to the DOM tree. When the DOM nodes change, you can invoke a callback function to react to the changes.

The basic steps for using the MutationObserver API are:

First, define the callback function that will execute when the DOM changes:

function callback(mutations) {
    // 
}Code language: JavaScript (javascript)

Second, create a MutationObserver object and pass the callback to the MutationObserver() constructor:

let observer = new MutationObserver(callback);Code language: JavaScript (javascript)

Third, call the observe() method to start observing the DOM changes.

observer.observe(targetNode, observerOptions);Code language: JavaScript (javascript)

The observe() method has two parameters. The target is the root of the subtree of nodes to monitor for changes. The observerOptions parameter contains properties that specify what DOM changes should be reported to the observer’s callback.

Finally, stop observing the DOM changes by calling the disconnect() method:

observer.disconnect();Code language: JavaScript (javascript)

The MutationObserver options

The second argument of the observe() method allows you to specify options to describe the MutationObserver:

let options = {
    childList: true,
    attributes: true,
    characterData: false,
    subtree: false,
    attributeFilter: ['attr1', 'attr2'],
    attributeOldValue: false,
    characterDataOldValue: false
};Code language: JavaScript (javascript)

You don’t need to use all the options. However, to make the MutationObserver works, at least one of childList, attributes, or characterData needs to be set to true, otherwise the observer() method will throw an error.

Observing changes to child elements

Assuming that you have the following list:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>MutationObserver Demo: ChildList</title>
</head>
<body>
    <ul id="language">
        <li>HTML</li>
        <li>CSS</li>
        <li>JavaScript</li>
        <li>TypeScript</li>
    </ul>

    <button id="btnStart">Start Observing</button>
    <button id="btnStop">Stop Observing</button>
    <button id="btnAdd">Add</button>
    <button id="btnRemove">Remove the Last Child</button>

    <script src="app.js"></script>
</body>
</html>
Code language: HTML, XML (xml)

The following example illustrates how to use the childList property of the mutation options object to monitor for the child node changes.

First, select the elements like the list and buttons using the querySelector() method. By default, the Stop Observing button is disabled.

// selecting list
let list = document.querySelector('#language');

// selecting buttons
let btnAdd = document.querySelector('#btnAdd');
let btnRemove = document.querySelector('#btnRemove');
let btnStart = document.querySelector('#btnStart');

let btnStop = document.querySelector('#btnStop');
btnStop.disabled = true;Code language: JavaScript (javascript)

Second, declare a log() function that will be used as a callback for the MutationObserver:

function log(mutations) {
    for (let mutation of mutations) {
        if (mutation.type === 'childList') {
            console.log(mutation);
        }
    }
}Code language: JavaScript (javascript)

Third, create a new MutationObserver object:

let observer = new MutationObserver(log);
Code language: JavaScript (javascript)

Fourth, start observing the DOM changes to the child nodes of the list element when the Start Observing button is clicked by calling the observe() method with the childList of the options object is set to true:

btnStart.addEventListener('click', function () {
    observer.observe(list, {
        childList: true
    });
    
    btnStart.disabled = true;
    btnStop.disabled = false;
});
Code language: JavaScript (javascript)

Fifth, add a new list item when the add button is clicked:

let counter = 1;
btnAdd.addEventListener('click', function () {
    // create a new item element
    let item = document.createElement('li');
    item.textContent = `Item ${counter++}`;

    // append it to the child nodes of list
    list.appendChild(item);
});
Code language: JavaScript (javascript)

Sixth, remove the last child of the list when the Remove button is clicked:

btnRemove.addEventListener('click', function () {
    list.lastElementChild ?
        list.removeChild(list.lastElementChild) :
        console.log('No more child node to remove');
});
Code language: JavaScript (javascript)

Finally, stop observing DOM changes when the Stop Observing button is clicked by calling the disconnect() method of the MutationObserver object:

btnStop.addEventListener('click', function () {
    observer.disconnect();    
    // set button states
    btnStart.disabled = false;
    btnStop.disabled = true;
});Code language: JavaScript (javascript)

Put it all together:

(function () {
    // selecting the list
    let list = document.querySelector('#language');

    // selecting the buttons
    let btnAdd = document.querySelector('#btnAdd');
    let btnRemove = document.querySelector('#btnRemove');
    let btnStart = document.querySelector('#btnStart');

    // disable the stop button
    let btnStop = document.querySelector('#btnStop');
    btnStop.disabled = true;

    function log(mutations) {
        for (let mutation of mutations) {
            if (mutation.type === 'childList') {
                console.log(mutation);
            }
        }
    }

    let observer = new MutationObserver(log);

    btnStart.addEventListener('click', function () {
        observer.observe(list, {
            childList: true
        });

        btnStart.disabled = true;
        btnStop.disabled = false;
    });

    btnStop.addEventListener('click', function () {
        observer.disconnect();

        // Set the button state
        btnStart.disabled = false;
        btnStop.disabled = true;
    });

    let counter = 1;
    btnAdd.addEventListener('click', function () {
        // create a new item element
        let item = document.createElement('li');
        item.textContent = `Item ${counter++}`;

        // append it to the child nodes of list
        list.appendChild(item);
    });

    btnRemove.addEventListener('click', function () {
        list.lastElementChild ?
            list.removeChild(list.lastElementChild) :
            console.log('No more child node to remove');
    });

})();
Code language: JavaScript (javascript)

Notice that we placed all code in an IIFE (Immediately Invoked Function Expression).

Observing for changes to attributes

To observe for changes to attributes, you use the following attributes property of the options object:

let options = {
  attributes: true
}
Code language: JavaScript (javascript)

If you want to observe the changes to one or more specific attributes while ignoring the others, you can use the attributeFilter property:

let options = {
  attributes: true,
  attributeFilter: ['class', 'style']
}Code language: JavaScript (javascript)

In this example, the MutationObserver will invoke the callback each time the class or style attribute changes.

Observing for changes to a subtree

To monitor the target node and its subtree of nodes, you set the subtree property of the options object to true:

let options = {
    subtree: true
}
Code language: JavaScript (javascript)

Observing for changes to character data

To monitor the node for changes to its textual contents, you set the characterData property of the options object to true:

let options = {
    characterData: true
}Code language: JavaScript (javascript)

Accessing old values

To access the old values of attributes, you set the attributeOldValue property of the options object to true:

let options = {
    attributes: true,
    attributeOldValue: true
}Code language: JavaScript (javascript)

Similarly, you can access the old value of character data by setting the characterDataOldValue property of the options object to true:

let options = {
    characterData: true,
    subtree: true,
    characterDataOldValue: true
}Code language: JavaScript (javascript)

A practical example of MutationObserver

In JavaScript applications, the elements on the page are typically dynamically generated. To wait for a dynamic element, you need to use MutationObserver.

The following waitForElement() function waits for one or more elements specified by a selector using MutationObserver.

function waitForElement(selector) {
  return new Promise((resolve) => {
    if (document.querySelector(selector)) {
      return resolve(element);
    }
    const observer = new MutationObserver(() => {
      const element = document.querySelector(selector);
      if (element) {
        resolve(element);
        observer.disconnect();
      }
    });
    observer.observe(document.body, {
      childList: true,
      subtree: true,
    });
  });
}Code language: JavaScript (javascript)

How it works.

The waitForElement() function returns a promise. The promise will be resolved once the element is available.

First, resolve the element if it is available:

if (document.querySelector(selector)) {
    return resolve(element);
}Code language: JavaScript (javascript)

Second, create a new MutationObserver object to observe the DOM tree if the element is not available:

const observer = new MutationObserver(() => {
  const element = document.querySelector(selector);
  if (element) {
    resolve(element);
    observer.disconnect();
  }
});Code language: JavaScript (javascript)

The observer object will call the resolve() function once the element is available and stop observing the DOM tree.

Third, observe elements of the whole DOM tree:

observer.observe(document.body, {
      childList: true,
      subtree: true,
});Code language: CSS (css)

Because the waitForElement() returns a Promise, you can use the then() method like this:

waitForElement()('.a-class').then((element) => {
    console.log('Element is ready');
    console.log(element.textContent);
});Code language: JavaScript (javascript)

Or you can use await syntax:

const element = await waitForElement()('.a-class');
console.log(element.textContent);Code language: JavaScript (javascript)

In this tutorial, you have learned about the JavaScript MutationObserver API that monitors the DOM changes and executes a callback every time the change occurs.

Was this tutorial helpful ?