Use MutationObserver to spy on the DOM

Have you ever wished that you could “listen to” changes that happen in the DOM? Of course, you have “event handlers” for this, it’s as easy as this:

However, you can’t catch everything with that. Indeed, what if you want to be notified when the text or an attribute value of an element changes? Well, the answer to this question is “MutationObserver”.

Observe attribute changes

Say we have this super useful code:

A way to detect this change would be to have a function executed every few milliseconds to check the value of the “style” attribute, compare it to the last known one, then fire an event if it changed, however, this would be pretty cumbersome to develop and probably pretty bad for performances.

A better solution consists in using “MutationObserver”:

The “MutationObserver” constructor takes a callback that gets executed when a change is detected. As several changes can occur at the same time, the parameter passed to this callback is a list of all the changes that occured. Once they have been displayed, “disconnect” is called to stop listening to changes.

Once the configuration is defined, you just need to call “observe” on it to start watching for changes. The second argument of this function allows you to define which events you want to observe (we’ll cover each of them below). For this example, we set “attributes” on “true” to configure the observer to watch for attribute value changes.

Executing the code above would produce the following result:

Untitled.png

The “MutationRecord” above shows some information about the event:

  • “type” indicates that the change is of type “attributes”, so it means that the value of an attribute changed.
  • “attributeName” contains the name of the attribute that changed.

You can also retrieve the current value of the attribute by using the property “target” of the “MutationObserver”. This property contains a reference to the “div” for which the attribute value changed.

That’s already pretty cool but a such, an important information is missing: the previous value of the attribute. By default, this value is not recorded but you can leverage this by setting the property “attributeOldValue” of the configuration object to “true”.

To be able to test this, update your HTML with this:

If we didn’t do that, the result would have been the same as the previous value of this attribute would have been “null” as well (as we didn’t define it).

This time, the execution of the code would produce the following result:

Untitled

“oldValue” now contains the previous value of the attribute.

As we said earlier, the value passed to the callback function is a list. We can see that by modifying another attribute of the element:

Untitled.png

This time, two changes have been detected:

  • The modification of the “style” attribute.
  • The modification of the “hidden” attribute.

Now, we can imagine that we don’t want to listen to “all” attribute value changes. Indeed, in the example below, we might not care about the fact that the text gets bold but we might still want to know when the “div” gets hidden. This can be achieved by filtering the attributes you want to watch:

The filtering of the attributes is done by using the property “attributeFilter” of the configuration object. It just expects a list of the attributes to observe. All changes related to other attributes than the ones defined in this array are ignored. Refresh your page to see that this time, the “style” attribute value modification is no longer taken into account.

Untitled

Observe text changes

Besides watching attribute value changes, you can also watch text value changes. Although it’s not more difficult that observing an attribute value change, a detail has to be known. Basically, observing a text changed can be done like this:

  • “characterData” defines that we want to observe the text changes.
  • “subtree” defines that we want to observe events occuring in the children of the target element. This was not required for attribute changes as it’s really the element that get changed when one of its attribute value changes. However, in the case of text changes, it’s the value of a child element of type “text” that changes, so we need to observe children as well.

However, this code wouldn’t work if your HTML looks like this:

In this case, the function executed by the “setInterval” would not update the text of the element but it would actually append a new “text” element to it, so the change wouldn’t be of type “text” but (as we’ll see a bit later) “childList”. For our code to work, we have to ensure that the “div” already contains a text value.

Untitled.png

As for attribute value changes, you can record the previous text value of the element by settings “characterDataOldValue” to “true”.

Untitled.png

As expected, “oldValue” now contains the previous text value of the element.

Observe child nodes

As mentioned previously, observing text changes when the element hasn’t already a text element would fire a “childList” event instead of a “characterData” one. We can test this by removing the text value of our “div” and updating our code like this:

“childList” is used to observe changes that occur to child elements of the target element. Running the code above would display the following result:

Untitled

The type of the first “MutationRecord” is “childList” because the “div” didn’t contain any text value before we updated it on “I’ve been updated”, so the change is not a text update but the insertion of a child element of type “text”. However, the second “MutationRecord” is a “characterData” one has the text element now exists (as it has been inserted the line before).

Note that “childList” gives you some information about what exactly happened. Update your code like this:

This code results in a list of five “MutationRecord”:

  1. “childList” as a first child has been added to the main “div”.
  2. “childList” as a second child has been added to the main “div”.
  3. “childList” as a “text” element has been added to the first child “div”.
  4. “attributes” as the attribute “style” has been updated for the first child of the main “div”.
  5. “childList” as the second child of the main “div” has been removed.

If you have a look at the “addedNodes” of the first “MutationRecord”, you’ll see that the list contains one element that references the “div” that has been added. In the same way, for the last “MutationRecord”, the property “removedNodes” contains a reference to the “div” that has been removed from the main “div”.

takeRecords

Besides “observe” and “disconnect”, there is a third function that you can call on your observer: “takeRecords”. This function can be used to directly get the list of “MutationRecord” that have not been treated.

For example, let’s consider the following example. The code won’t make much sense but I couldn’t come up with another way to demonstrate the usage of this function.

If you test this code, you won’t get anything logged in the console. Indeed, as the callback function is executed asynchronously, it cannot run before the current event loop cycle is executed. In other word, the callback cannot be executed until all our code is executed. However, in this code, we call the “disconnect” function to force the observer to stop listening to events, meaning that even though the observer caught some changes, it won’t get the chance to execute the callback function as it’s disconnected.

To solve this issue, you can use “takeRecords” to retrieve a list of events that haven’t been treated yet. Doing so clean up the queue to ensure that events are not handled twice. Replace the last line of the code above by this:

We retrieve the list of unhandled events, display them, then finally, we disconnect the observer.

Untitled.png

So now, even though the observer gets disconnected at line 5, we can treat the “MutationRecord” that occured between the subscription and the disconnection.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.