Friday, January 24, 2025

Declare and Respond to a DOM Element Ready Event

Declare and Respond to a DOM Element Ready Event

In my Respond to DOM Changes with Mutation Observers article, I introduced the W3C DOM4 specification’s mutation observers, which replace the deprecated mutation events. Mutation Observers inform your script of changes to the DOM structure using an asynchronous callback mechanism. Both the types of changes and node types reported on include pretty much everything that you’ll find in a web page. In today’s follow-up, I’d like to provide a concrete example of how Mutation Observers may be utilized to tell us when particular elements have been added to the DOM. This frees us from having to wait for the document load or DOMContentLoaded events and works for dynamically-loaded elements as well!

The HTML Code

For our demo, we’ll watch for text input fields. Hence, whenever a text input field is appended to the DOM, whether from the original HTML document or a dynamic script, our callback function will execute. The following form contains two text fields:

<form>
  First name:<br>
  <input type="text" id="firstname"><br>
  Last name:<br>
  <input type="text" id="lastname">
</form>
<br/>
<div id="output"></div>

The JavaScript

This Mutation Observer approach is based on the outstanding work of fellow Ontarian Ryan Morr. I made some minor changes to the code to make it jQuery compatible and more my own, but any and all accolades should go to him, and not me.

Speaking of jQuery, it seems that they deprecated the .selector property, so once a DOM collection has been fetched using the $() function, it won’t be updated should the DOM change. That presents a problem for us because our code piggybacks on the MutationObserver so that whenever it picks up a change in the document, we perform our own checks, using the stored selectors. I originally wanted to add the ready function to the jQuery.fn object so that it could be chained to a jQuery selector $() call. Without a way to get at a jQuery DOM collection’s original selector, that plan crumbled into dust. Instead, I opted to define my own class called DynamicSelector. It stores the selector so that it may be recalled later. It also implements the ready() function so that we can chain it all together like so:

//select all text input fields
new DynamicSelector('input[type="text"]').ready(function(element){
  //display the time (getTime() not shown)
  //and a message that the field is ready
  $('div#output').append(getTime() + ': ' + element.id + ' field is ready!<br/>');
});

The ready() function accepts a callback function that is invoked for every element matched by the selector. That part is simple enough: ready() fetches a collection of matching DOM elements, iterates over them, and invokes the passed function for each of them. The rub is that the same function must also be invoked whenever a change to the DOM would cause additional elements to match. In fact, that’s the main reason for using a MutationObserver. Dynamic Selectors are pushed onto our global “selectors” array so that they may be individually refreshed within the MutationObserver’s callback function. Therein, matched elements are given a custom “ready” boolean flag so that they do not trigger a re-invocation of our ready()’s callback function every time the DOM changes. The result is that dynamically appending an element to the DOM will cause the ready() callback function to fire should said element match the DynamicSelector’s selector string.

Before we look at the code, I should point out that the MutationObserver is also stored in a global variable named “observer”, so that we only create a singleton the first time that the ready() function is invoked, because, once the MutationObserver has been instantiated, it continues to work throughout the lifetime of the page.

'use strict';
   
var selectors = [],
    observer,
    DynamicSelector = function(selector) {
      this.selector = selector;
      this.ready = function(fn) {
        // Store the selector and callback to be monitored
        selectors.push(this);
       
        if(observer === undefined){
          // Watch for changes in the document
          observer = new (MutationObserver || WebKitMutationObserver)(function(){
            // Check the DOM for elements matching  a stored selector
            selectors.forEach(function(dynamicSelector) {
              // Query for elements matching the specified selector
              $(dynamicSelector.selector).each(function() {
                // Make sure the callback isn't invoked with the
                // same element more than once
                if(!this.ready){
                  this.ready = true;
                  // Invoke the callback with the element
                  fn.call(this, this);
                }
              });
            });
          });
          observer.observe(document.documentElement, {
              childList: true,
              subtree: true
          });
        }
      }
    };

Testing for Changes to the DOM

To ascertain whether or not new matching elements are in fact triggering the execution of our callback function(s), we only have to dynamically append an element to the DOM and see what happens. The following code waits 3 seconds after the initial page load to append a new text field with an id of ‘new_text_input’.

$(function() {
  //dynamically add another one
  setTimeout(function() {
    $('<input>')
      .attr('id', 'new_text_input')
      .attr('type', 'text')
      .appendTo(
        $('form').append('<br>New field:<br>')
      );
  }, 3000);
});

Here’s what was outputted to the page during my test:

10:3:27: firstname field is ready!
10:3:27: lastname field is ready!
10:3:30: new_text_input field is ready!

As you can see, the two text fields that were part of the HTML markup fired our callback function within the same second. Then, exactly three seconds later, we can see our new field on the screen, along with the output of our callback function.

Rob Gravelle

In his spare time, Rob has become an accomplished guitar player, and has released several CDs. His band, Ivory Knight, was rated as one of Canada’s top hard rock and metal groups by Brave Words magazine (issue #92) and reached the #1 spot in the National Heavy Metal charts on Reverb Nation.

Rob Gravelle
Rob Gravelle
Rob Gravelle resides in Ottawa, Canada, and has been an IT guru for over 20 years. In that time, Rob has built systems for intelligence-related organizations such as Canada Border Services and various commercial businesses. In his spare time, Rob has become an accomplished music artist with several CDs and digital releases to his credit.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Popular Articles

Featured