Reacting to DOM changes with MutationObserver API


This is a guest post from Andrey Semin, a Javascript developer from Russia.


As developers, we love to build tools. And we love to enhance tools. It may be anything: an extension for the browser, a plugin for some product and so on. And we need a way to somehow react to DOM changes caused by some external process.

It's really cool if we have an API to use. But it is not always the case. Sometimes we just need to watch for DOM changes. And we may want to go with naïve approach: to apply a polling technique with the setInterval API   to check if something was changed within some period of time. This approach is pretty straightforward but it has some tradeoffs:

1) It may significantly affect your app's performance

2) Over time it becomes harder and harder to maintain the code.

Here is where MutationObserver comes into play. It is a Web API supported by the majority of modern browsers allowing us to watch for the changes made to the DOM tree and to react on them. Let's take a look at its API.

API

According to MDN, the MutationObserver constructor takes only one parameter: a callback function that would be later called on each DOM change for the observed node. This constructor returns a MutationObserver instance with 3 available methods:

  1. observe(): configures the MutationObserver instance to start reacting to DOM changes with provided callback. Takes 2 arguments: required targetNode and an optional config for this node. Obviously, targetNode should be a single node or a root of a subtree to be watched. The config object describes the rules to detect required mutations.
  2. disconnect(): stops the MutationObserver instance from reacting to DOM changes. Observation can be later restarted with another observe() call.
  3. takeRecords(): returns all pending MutationRecords which has not been processed by our callback function and clears mutation queue.

A common use case looks like the following:

Configuration

Now let's take a detailed look at config parameter used by the observe() method. This parameter allows us to configure mutation observer to only watch for specific changes. Here is a list of options available for configuration:

  1. attributes: Boolean. Configures the observer to watch for attribute changes on targetNode and its subtree.
  2. attributeFilter: An array of strings. Each string is a name of attribute to be watched. If you won't use this property explicitly changes to all attributes would be observed.
  3. attributeOldValue: Boolean. Configures the observer to include previous value of watched attribute. The value is available under oldValue key on MutationRecord.
  4. characterData: Boolean. Configures the observer to watch for changes in textual contents of targetNode and its subtree.
  5. characterDataOldValue: Boolean. Configures the observer to include previous value of watched text nodes.
  6. childList: Boolean. Configures the observer to watch for the addition or removal of any child nodes under targetNode and its subtree.
  7. subtree: Boolean. Configures the observer to watch for the same changes not only on provided targetNode but on its subtree as well. Uses same configuration for changes in subtree as for the root.

In order for MutationObserver to work it must know what it should watch for. So at least one of childList, attributes, or characterData must be set to true. Otherwise TypeError exception would be thrown.

Observer Callback

When fired, the provided callback function is called with 2 arguments: an array of MutationRecords representing each change(all changes would be batched if they have happened within some period of time) and an instance of MutationObserver which invoked this callback. Since the observer instance is the same described above we won't cover it once again. Instead, we focus on first argument.

Each object in this array represents an individual DOM change and contains the same set of fields:

  1. type: String. The type of the mutation. The value is attributes for an attribute mutation, childList for mutation of subtree and characterData for character data mutation.
  2. target: Node. The node affected by the mutation. The value depends on mutation type: element containing changed attribute for attributes, the node whose descendant changed for childList and CharacterData node for characterData.
  3. addedNodes: NodeList. A list of added nodes. Will be empty if no nodes were added during the mutation.
  4. removedNodes: NodeList. A list of removed nodes. Will be empty if no nodes were removed during the mutation.
  5. previousSibling: Node. Previous sibling of added or removed node. 
  6. nextSibling: Node. Next sibling of added or removed node.
  7. attributeName: String. A name of changed attribute for attributes mutation type, null for others.
  8. attributeNamespace: String. Namespace of changed attribute if there is any.
  9. oldValue: String. Depends on mutation type. The value of changed attribute before change for attributes, the data of changed node for characterData and null for childList.

Keep in mind to process all mutations happening you should iterate over mutations array by simply using forEach method on it.

Example

You can find an interactive example here (it's hosted on heroku so you may need to wait some time until dyno is started). Source code may be found here.


Author bio: Andrey Semin is a Javascript developer from Russia. He enjoys turning complex problems into beautiful UIs, providing great experience to both the users and businesses. He lives to learn new things and to share his knowledge and experience with the community. 


Get started, it’s free