Many developers have become accustomed to using some variant of a DOM ready method to signify when they can begin to traverse and manipulate the DOM. Or, better yet, placing the scripts at the bottom of the HTML to avoid blocking render while ensuring the DOM is loaded when the JavaScript is executed. However, in this day and age of dynamic web applications, it seems like a rather antiquated approach. The DOM is not static, it is alive and subject to manipulation long after the initial page load. Simply knowing when the DOM is ready, while still useful, does not encompass the entirely of the desired functionality. Instead, how about watching for when specific elements become available throughout the course of runtime? Well, I intend to offer a solution to support exactly that kind of functionality using mutation observers as the means to a more modern approach.

Mutation Observers

The W3C DOM4 specification initially introduced mutation observers as a replacement for the deprecated mutation events. The new specification essentially permits the same functionality, but with some added benefits. Most notably is that mutation observers are asynchronous, and will invoke the callback after a batch of changes rather than every single change. This allows mutation observers to operate in a more performant manner, one of the pitfalls of mutation events, among others.

Mutations that can be observed include the addition and removal of child nodes, changes to attributes, and updates to text nodes. For the purposes of this article, we want to focus on the addition of elements into the DOM. For this, we need to create a new instance of MutationObserver passing the callback function as the only argument. Then we invoke the observe method with the element we want to be observed and a configuration object as the first and second arguments:

var observer = new MutationObserver(callback);
observer.observe(document.documentElement, {
    childList: true,
    subtree: true            
});

The childList configuration option causes the observer to monitor the DOM for any nodes added or removed from the documentElement. The subtree option will additionally allow every child node of the documentElement to be monitored for these same changes. This gives us the means to detecting element insertion anywhere in the DOM, which bodes well for our purposes. The next step is to formulate the solution.

Watching for Desired Element Availability

The solution supports detecting element readiness on the initial page load as well as elements dynamically appended to the DOM. The code in full is as follows:

(function(win) {
    'use strict';
    
    var listeners = [], 
    doc = win.document, 
    MutationObserver = win.MutationObserver || win.WebKitMutationObserver,
    observer;
    
    function ready(selector, fn) {
        // Store the selector and callback to be monitored
        listeners.push({
            selector: selector,
            fn: fn
        });
        if (!observer) {
            // Watch for changes in the document
            observer = new MutationObserver(check);
            observer.observe(doc.documentElement, {
                childList: true,
                subtree: true
            });
        }
        // Check if the element is currently in the DOM
        check();
    }
        
    function check() {
        // Check the DOM for elements matching a stored selector
        for (var i = 0, len = listeners.length, listener, elements; i < len; i++) {
            listener = listeners[i];
            // Query for elements matching the specified selector
            elements = doc.querySelectorAll(listener.selector);
            for (var j = 0, jLen = elements.length, element; j < jLen; j++) {
                element = elements[j];
                // Make sure the callback isn't invoked with the 
                // same element more than once
                if (!element.ready) {
                    element.ready = true;
                    // Invoke the callback with the element
                    listener.fn.call(element, element);
                }
            }
        }
    }

    // Expose `ready`
    win.ready = ready;
            
})(this);

The ready function accepts a selector string as the first argument and the callback function as the second. Once ready, the callback is invoked, passing the element as the only parameter:

ready('.foo', function(element) {
    // do something
});

What is important to note here is that a ready invocation is not a one-off binding. Instead, the callback lives throughout the life of the page, and is invoked any time an element matching the selector string is added to the DOM. If multiple elements matching the selector are added at once, the callback is invoked in succession for each element in document order.

Conclusion

This serves as a more robust and forward-thinking approach to initializing DOM traversal and manipulation than your typical DOM ready methods. Despite growing browser support for mutation observers, some browsers such as IE 10 and below lack support. While it may be too early for widespread adoption, it could be a small window into the future of DOM initialization via JavaScript. View full documentation and download the source for this project on GitHub.