domReady version II

A domReady function handles DOMContentLoaded events in your browser. The DOMContentLoaded event allows you to add behavior or change the HTML of a page after the HTML has loaded and before the onload event which happens after the complete page, including images has loaded. This allows you to add menu, tree behavior, AJAX functionality or anything else without having to wait for all items on a page to load. You may have experienced the need for a DOMContentLoaded event on a page that includes drop down menus or a tabbed interface which doesn't work until all images have loaded. Using the DOMContentLoaded event allows you to add the behavior before images and objects have loaded, giving a better user experience. The domReady function is similiar to the jQuery ready method without requiring jQuery.

Back in 2005 I modified code from Dean Edwards that had been modified by Tino Zijdel and added a DOMContentLoaded event. This is what is commonly known today as a domReady event. In 2008 I created a separate domReady module using Object Literal Notation (OLN). With time, code styles and methodology change. Even though the code still worked, it was time to for an update. With about an hour of time, I modified the domReady code to use private functions, delete code for KTLM browsers, and clean up the code.

I changed the code to leave only the domReady global function. All other functions are private to domReady instead of the previous two global varibles. With the OLN object, all methods could be accessed globally, those methods are now private. The code needed to not create itself if the js file had been loaded multiple times and to reveal only the domReady function. Here's where I started.

// Create the domReady event handler once.
if(typeof domReady !== "function") {
    var domReady = (function() {
        // Array of domReady event handlers.
        var events = {}, domReadyID = 1, bDone = false;

        // Function that adds domReady listeners to the array.
        function add(handler) {
        }

        // Function to process the domReady events array.
        function run() {
        }

        function init() {
        }

        init();
        return add;
    })();
}

The first line checks to see if the domReady function already exists. If domReady does not exist, the second line creates the global domReady variable that calls an anonymous function. This function creates:

  • Private variables.
  • The add, run, and init functions.
  • Calls the init function.
  • Returns the add function to the domReady varible, causing it to become a proxy for the add method.

When you call domReady you are actually calling the add function within the anonymous function. This allows you to hide private variables and functions from the programmer using your object.

The init function sets up the code to trigger a domReady event and execute the run function even if the browser doesn't support a DOMContentLoaded event. The first thing it does is check for addEventListener and if it exists adds a DOMContentLoaded event. The addEventListener method is not supported on older versions of IE. Checking to see if addEventListener existes prevents errors in IE.

if(document.addEventListener) {
    document.addEventListener("DOMContentLoaded", run, false);
}

At the time I wrote this DOMContentLoaded wasn't supported by all implementations of addEventListener. With that in mind I needed a fallback method and decided to use the load event. If DOMContentLoaded is not supported, then the run function is called on load. This is only there for very old browsers, all current browsers support the DOMContentLoaded event, including IE. I'm using the window object instead of the document object because the load event wasn't triggered off the document object, but did get triggered off the window object. The attachEvent method is used for older versions of IE.

if(window.addEventListener) {
    window.addEventListener("load", run, false);
} else if(window.attachEvent) {
    window.attachEvent("onload", run);
}

Now I had to support a domReady event for old versions of IE and onload wasn't a solution. You just can't not support old versions of IE, since many corporations and government entities still use older versions of IE. When I originally created these functions IE did not support addEventListener or the DOMContentLoaded event.

/*@cc_on
    @if (@_win32 || @_win64)
        document.write("<script id=__ie_onload defer src=\"//:\"><\/script>");
        var script = document.getElementById("__ie_onload");
        script.onreadystatechange = function() {
            if (this.readyState == "complete") {
                run(); // call the onload handler
            }
        };
    @end
@*/

What we're doing here is using Microsoft javascript conditional comments and variables to target IE on windows. The code creates a script element with the defer attribute and checks when the browser tells it that the script is loaded. The defer attribute in IE causes the script to load and execute when the DOM has loaded. This was Microsoft's way of supporting a DOMContentLoaded event before anyone had ever thought of a DOMContentLoaded event. Unfortunately at the time other browsers didn't support the defer attribute and would load the script immediately which is why we use Microsoft conditional comments to implement this code. Other browsers ignore the Microsoft conditional comments.

Time to take a look at the add function which enables you to add functions that are called when the DOM has loaded.

function add(handler) {
    // handler must be a function
    if(typeof handler !== "function") {
        return;
    }

    // Assign each event handler a unique ID. If the handler has an ID, it
    // has already been added to the events object or been run.
    if (!handler.$$domReadyID) {
        handler.$$domReadyID = domReadyID++;
        // store the event handler in the hash table
        events[handler.$$domReadyID] = handler;

        // If the domReady event has happened, run the function.
        if(bDone){
            handler();
        }
    }
}

The first thing we do is check to see if the handler is a function. It won't do us any good to try to run a string or object as a function. We next check to see if our $$domReadyID variable has been added to the function so we don't run the function twice. Then the handler is added to the events array. The last bit of code check to see if the domReady event has been executed. If it has there is no reason to wait to run the handler function, so we run it now.

Let's take a look at the run function which is executed by the domReady event.

if (bDone) {
    return;
}

// Flag this function so we don't do the same thing twice
bDone = true;

// iterates through array of registered functions
for (var i in events) {
    events[i]();
}

The first then is to check the bDone private varible to see if the run function has already been executed. We do this because the run function can be executed by DOMContentLoaded, onload, and in IE the special IE code. This keeps us from executing domReady functions more than once.

If the run function hasn't been executed, set the bDone varible so that it does not execute the domReady functions more than once. Then the code iterates through the added domReady event functions, executing each function. We could add a try/catch structure to prevent errors in the calling functions from stopping our loop, but I think that you would most likely want the code to stop so that you can correct the errors. I may wrong and may add a try/catch structure in the future.

That's it. If you don't use requires, add the following to the <head> element and add calls to domReady, passing it a function to run when the DOM has loaded.

<script src="js/domready.js"></script>

In your script make a call to domReady.

domReady(myFunction);

Check out the sample.

Sample and Download

Related Articles

0 Comments

Add a comment

Discussion for this entry is now closed.