How to wait for an element to render in the DOM

If you are reading this article, chances are you are looking for a way to trigger some code when an element becomes available on the page. This has been possible for a long time using hacks such as setTimeout or third libraries (usually built on top of setTimeout anyway).

In this article, we are going to learn how to wait for an element on a page using one of the HTML 5 API interfaces called MutationObserver.

TLTR; The working code can be found on this codepen.

Mutation Observer

If you have never heard of this, do not worry, you are not the only one! I did not know of its existence until I had to do some research on this problem. MutationObeserver is:

The MutationObserver interface provides the ability to watch for changes being made to the DOM tree. It is designed as a replacement for the older Mutation Events feature, which was part of the DOM3 Events specification.

https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver?retiredLocale=it

Mutation Observer is part of a family of “observer” Web APIs like IntersectionObserver and Resize Observer that are helping developers create more readable, more performance and cleaner Javascript code.

The possible scenario

Before we jump to the code, let’s introduce possible scenarios that could require us to use this feature in our day-to-day. If you are already encountering this problem, feel free to jump to the next section “How to wait for an element on a page”.

If you follow my blog, you should know by now that I am a Vue Js developer and love to use Javascript frameworks during my day-to-day, but even in this case this scenario could still apply.

Due to the way the Web API works, we are going to give 3 different examples:

A new direct child added

The first example is the most common and covers the case in which we have to trigger an action when a new child is added to our target. A few common situations that would trigger this requirement are:

  • Need to calculate some specific value or design when a table row is added or removed
  • Need to change some state when a dialog or sidebar is open or closed (therefore added to the dom)
  • Need to trigger some code when the users drag and drop elements on the page

Element attribute has changes

In the previous example, we were observing for actual DOM tree modification, but in the current use case, we are actually looking for a modification of an element Attribute. This scenario may be a little less common, but it is still very powerful and useful.

Common scenarios for these examples are:

  • Custom Data attributes like [data-*] that are added by a different part of the system and need to listen too
  • Classes added or removed on an element.
  • Change in Attributes like disabled, max and min that require a javascript action to be triggered.

Change to an element in the subtree

This example is very similar to the first example, but it is slightly different as it requires a wider observation of the DOM tree and therefore further resources. In our first example, we were looking for a direct child change. To provide more context, in our first example, we were observing a Table for an addition of a Row, or a UL for an addition of an li, while in this scenario the changes can be very deep. For example, we may be observing the whole Body to trigger a change deep within the DOM, or a section for a small change within one of its children.

The use case of this example is similar to the first one, but the usage should differ due to its higher resource allocation.

As you may expect listening to a single DOM modification is much different than observing a very complete and deep DOM tree! My suggestion in this scenario is not actually how to use it, but to actually try and avoid this scenario, or just restrict it to the simple scenario that can be “disconnected” after being triggered.

How to wait for an element on a page

It is time to jump on the code and see how to actually use the MutationObserver. In the following example, we are going to observe a DOM modification (the first scenario introduced before).

How to use the MutationObserver to trigger a DOM modification

The acceptance criteria for this scenario are:

As a user, I would like to observe a List to be able to calculate the height of the rendered list

To do this we are going to first create a simple list in HTML and some functionality to allow us to modify the list entries.

// HTML
<ul id="target">
  <li>Simone</li>
  <li>Zelig880</li>
  <li>John</li>
</ul>
<button>Add random entry</button>
<div>The list height is: <span id="height"></span></div>

JS
document.querySelector( 'button' ).addEventListener("click", addRow);
function addRow() {
  const newRow = document.createElement( 'li' );
  newRow.innerText = 'RandomName' + Math.random();
  document.querySelector( 'ul' ).append( newRow );
};

Now that we have our dynamic list it is time to create our observer. Like any other observer available within the Web API, we need to 3 distinct parts:

  • The element that we are going to observe, is called the “target”
  • The callback function will be triggered every time this observer changes
  • Initializing the observation itself

Declare the target and the MutationObserver options

First, we are going to define our targets. This can be any DOM node, so anything that you can fetch by using getElementById, querySelector and more. In our case we are going to use the ID “target”:

const targetNode = document.getElementById( 'target' );

We are then going to define a couple of options for our method. As mentioned above we are going to listen to direct children modification, so we would declare our options as such:

const config = {
  attributes: false,
  childList: true,
  subtree: false
};

Declaring a mutation observer Callback

As previously mentioned, our next step required the declaration of a callback function. This will be called with 2 arguments. The first is an array of mutations, and the second is the instance of the observer itself (useful when you want to disconnect it).

First, let’s set the basic structure of our callback:

const callback = (mutationList, observer) => {
  for (const mutation of mutationList) {   

  }
};

Then we are going to ensure that the mutation action is what we expect that in our case are “addedNodes” and “removedNodes”.

const callback = (mutationList, observer) => {
  for (const mutation of mutationList) {   
    if ( mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0 ) {
      
    }
  }
};

Lastly, we are going to do something with this information, in our example, we are going to fetch the actual height of the list after the mutation. To do so, we can use the “target” object

const callback = (mutationList, observer) => {
  for (const mutation of mutationList) {   
    if ( mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0 ) {
      const height = mutation.target.getBoundingClientRect().height;
      document.getElementById( 'height' ).innerText = height;
    }
  }
};

Trigger our observation

Now it is time to create an instance of our observation. To do so we would first need to create an observation and then attach it to a specific target (you can attach the same observation to multiple targets):

// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);

// Start observing the target node for configured mutations
observer.observe(targetNode, config);

The complete code

You can find a workable example of this mutation in the following codepen, where you can also find the code full code.

Conclusion

MutationObserver is a very powerful tool. With its clear syntax and extensive options can help us deliver amazing functionalities for our users. Even if today’s frameworks like Vue js, React and Angular all help us on our day-to-day, sometimes we are in need of a little help.

Knowing precisely when the DOM is actually fully updated and triggering events has always been a struggle for us, and with this Web API, solving this issue could not have been easier.

🤞 Don’t miss these tips!

No spam emails.. Pinky promise!