Are you ready for this

Filed: Thu, Oct 11 2007 under Programming|| Tags: DOM onlaod onready ondomready ready functional

For small pages, the preferred method of starting up your scripts is to use the window.onload event. This is a perfectly great little event which will fire after the page has finished loading and call whatever function you've assigned to it. For instance the following code will run as soon as the page has finished loading.

<script>
   function startup() {
      alert("The page has finished loading!  I'm ready to do stuff!");  
   }
   window.onload=startup;
</script>

Here we create a startup function that simply issues an alert box, we then pass window.onload the pointer to the function.

Scaling onload

The problem with the above script is that it assumes that no other script or library will want to attach an onload event of its own. And in this day of booming frameworks and endless pre-written snippets, that is no longer a certainty. Indeed, as your Javascript applications become larger you my find yourself stumbling over your own code! You may have a module that needs to setup an onload event to start up a clock, and another that will populate a calendar.

The solution here is to create a registration function that allows you to register events. Fortunately Javascript allows you to pass functions as arguments to other functions so this is a surprisingly easy task, and here's the code that does it.

startStack=function() { };  // A stack of functions to run onload/domready

registerOnLoad = function(func) {
   var orgOnLoad = startStack;
   startStack = function () {
      orgOnLoad();
      func();
      return;
   }
}

This is actually pretty sophisticated javascript and a great example of functional programming. We begin by creating an empty function, it doesn't really do anything. But when registerOnLoad is called and passed a function as the parameter then it will note the location of the original function (orgOnLoad=startStac) then create a new function which will execute the original function ( orgOnLoad(); ) and then execute the new function ( func() ).

To illustrate what's happening, here's what startStack will look like after a few registrations.

startStack=null starting function()
registerOnLoad(functionA);
startStack=null starting function()
           functionA()
registerOnLoad(functionB);
startStack=null starting function()
           functionA()
           functionB()
registerOnLoad(functionC);
startStack=null starting function()
           functionA()
           functionB()
           functionC()

So now instead of doing window.onload=function, we're going to call registerOnLoad(function).

When is ready really "ready?"

So now we have our registration function now we need to figure out how and when to start it up. A document actually has two ready states. window.onready is fired after the page, css, scripts, images and all other objects have been loaded. That is, it will fire when the browser has loaded everything necessary to display the page.

There is another ready-state however known as DOM-ready. This is when the browser has actually constructed the page but still may need to grab a few images or flash files. This is when we actually want to fire our registered functions because all of the getElementsByIDs and getElementsByTagName and getElementsByName will all work because the HTML is all there.

Here's the script to fire our startStack when the DOM is ready for us.

var ranOnload=false; // Flag to determine if we've ran the starting stack already.

if (document.addEventListener) {
  // Mozilla actually has a DOM READY event.
   document.addEventListener("DOMContentLoaded", function(){if (!ranOnload) {ranOnload=true; startStack();}}, false);
}  else if (document.all && !window.opera) {
  // This is the IE style which exploits a property of the (standards defined) defer attribute
  document.write("<scr" + "ipt id='DOMReady' defer=true " + "src=//:><\/scr" + "ipt>");  
  document.getElementById("DOMReady").onreadystatechange=function(){
    if (this.readyState=="complete"&&(!ranOnload)){
      ranOnload=true;
      startStack();
    }
  }
}

var orgOnLoad=window.onload;
window.onload=function() {
   if (typeof(orgOnLoad)=='function') {
      orgOnLoad();
   }
   if (!ranOnload) {
     ranOnload=true;
     startStack();
   }
}

First we define a flag to determine if we've run the starting script and initialize it to false. Next, if we're using Mozilla we'll set up an event listener which will fire when the DOM is ready. IE doesn't support this so we hack around it and exploit the behavior of IE's implementation of the standards defined 'defer' attribute. Defer basically defers the execution of a script until the DOM has been loaded (but before images have been loaded). You can find out more about defer in the article: Deferred Javascript..

JQuery uses the IE deferred script approach in its domready handlers and the hack was initially documented by Matthias Miller in his article The window.onload Problem Revisited.

Finally we set up an onload event for browsers which can't set up an event listener and isn't IE. Notice we take care not to stomp on any existing onload events when we set up our window.onload event. We call any existing functions before we call our own stuff.

New problem, same as the old problem

This isn't a panacea though! If you have additional scripts farther down the page and they just blatantly overwrite window.onload then you will cause problems for a few of your visitors who will never see the results of all your registered startup scripts. Why only some you ask? Because startStack really only runs through windows.onload if the user is not running a version of IE older than 5.0, or not running Firefox. So only a very, very few people will ever have your registered scripts fire through windows.onload. That said, you should still test and make sure windows.onload isn't being blatantly overwritten lower down in your application.

Bringing it all together

It's true that the various frameworks provide on-ready methods for you, but it's not always desirable to use a framework for everything. And as a side bonus you may have learned a few new tricks about queuing up functions and how all those frameworks really handle onready.

And now, without further ado, here is the complete script from start to finish ready to cut and paste into your applications.

startStack=function() { };  // A stack of functions to run onload/domready

registerOnLoad = function(func) {
   var orgOnLoad = startStack;
   startStack = function () {
      orgOnLoad();
      func();
      return;
   }
}

var ranOnload=false; // Flag to determine if we've ran the starting stack already.

if (document.addEventListener) {
  // Mozilla actually has a DOM READY event.
   document.addEventListener("DOMContentLoaded", function(){if (!ranOnload) {ranOnload=true; startStack();}}, false);
}  else if (document.all && !window.opera) {
  // This is the IE style which exploits a property of the (standards defined) defer attribute
  document.write("<scr" + "ipt id='DOMReady' defer=true " + "src=//:><\/scr" + "ipt>");  
  document.getElementById("DOMReady").onreadystatechange=function(){
    if (this.readyState=="complete"&&(!ranOnload)){
      ranOnload=true;
      startStack();
    }
  }
}

var orgOnLoad=window.onload;
window.onload=function() {
   if (typeof(orgOnLoad)=='function') {
      orgOnLoad();
   }
   if (!ranOnload) {
     ranOnload=true;
     startStack();
   }
}

And here's an example of two functions, registered to run when the dom is ready.

var startup1 = function() {
   alert("I'm the first function!")
}

registerOnLoad(startup1);
registerOnLoad(function () {
   alert("I'm the second function!")
});

Note that the first function is classically defined and then passed to register on-load. The second function is created inside the argument itself and is an anonymous (unnamed) function. So you do have quite a bit of flexibility as to how you go about registering your functions.