Cross Browser JavaScript DOMContentLoaded

This post shows you how you can call your JavaScript functions once the Document Object Model is ready (which is a lot faster than waiting for the page to finishing loading when using window.onload).

The following code is an amalgamation of the work of Jesse Skinner and Byron McGregor. It keeps track of the methods passed into the addDOMLoadEvent function and supports all modern browsers such as Firefox, Chrome, Opera, Safari (higher than 525) and falls back to window.onload for those browsers that fail all the other checks.

I’ve also updated the code to take into account one particular issue I found with previous versions when using the doScroll method of targeting Internet Explorer which would sometimes return “unavailable”, as well as fixing a Firefox error when hitting the Safari targeted code.

To start with lets take a look at the HTML for my test page:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>JavaScript. onDOMLoad</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<style type="text/css">
	*
	{ font-family: Arial, Helvetica, sans-serif; }
 
	li
	{ margin:25px; }
</style>
</head>
<body>
 
	<h1>addDOMLoadEvent Demo Page</h1>
	<h2>Supports Mozilla/Opera/Chrome/Safari > 525/IE6+</h2>
 
	<ol>
		<li>JavaScript will add more LI elements once the DOM is loaded.</li>
		<li><img src="Large1.jpg" height="100" width="100"> (approx 1.5mb)</li>
		<li><img src="Large2.jpg" height="100" width="100"> (approx 1.5mb)</li>
		<li><img src="Large3.jpg" height="100" width="100"> (approx 1.5mb)</li>
	</ol>
 
	<script type="text/javascript" src="domready.js"></script>
	<script type="text/javascript" src="library.js"></script>
 
</body>
</html>

The following code adds elements to the DOM while the images continue to download:

var MYAPP = {
	init: function()
	{
		MYAPP.addListElement("Thanks, the DOM appears to be loaded!");
	},
	addFunction: (function()
	{
		// Create a closure
		// so we can keep track of
		// the number of times this method is called
		var counter = 0;
		return function()
		{
			counter++;
			MYAPP.addListElement("Some random text " + counter);
		}
	})(),
	addListElement: function(text)
	{
		var li = document.createElement("li");
		li.appendChild(document.createTextNode(text));
		var ol = document.getElementsByTagName('ol').item(0);
		ol.appendChild(li);
	}
}
 
addDOMLoadEvent(MYAPP.init);
addDOMLoadEvent(MYAPP.addFunction);
addDOMLoadEvent(MYAPP.addFunction);
addDOMLoadEvent(MYAPP.addFunction);
addDOMLoadEvent(MYAPP.addFunction);
addDOMLoadEvent(function()
{
	MYAPP.addListElement('MY FINAL METHOD CALL');
});

And now here is the final addDOMLoadEvent code:

var addDOMLoadEvent = (function()
{
	var load_events = [], // Stacks all functions sent to addDOMLoadEvent
   	 load_timer, // Used as a timer for versions of Safari older than 525 and Internet Explorer
       done, // Used to check when the DOM has completed loading
       exec, // Used to execute each function in the load_events stack
       old_onload; // Used as a fallback for older browsers that fail all other means of emulating DOMContentLoaded
 
	// The init method will be called when the DOM is loaded (or when the window.onload event is triggered).
	// It lets the application know the DOM is ready by setting 'done' to true,
	// then runs a quick clean up of the load_timer interval,
	// and then executes all the functions in the load_events stack (in the order they were added).
	var init = function()
	{
		done = true;
 
		clearInterval(load_timer);
 
		while ((exec = load_events.shift())) {
			exec();
		}
	};
 
	return function (func)
	{
		// We check to see if the DOM has already loaded.
		// If it is then just run the function that's trying to be called and stop there.
      if (done) {
			return func();
		}
 
      if (!load_events[0]) {
      	if (document.addEventListener) {
				// Standard compliant browsers (inc. Mozilla/Opera/Chrome/Safari)
				document.addEventListener("DOMContentLoaded", init, false);
			}
 
			// Internet Explorer
 
			/**
			 * For full information see comment 201
			 * http://dean.edwards.name/weblog/2006/06/again/#comment335794
			 *
			 * Summing up the IE challenge:
			 * Initialization event behaviours and order vary greatly among different pages.
			 *
			 * Dean Edwards document.write of the deferred script has given problems on some pages (causing a consistent &gt; 60 sec delay).
			 * Dean confirmed that document.readyState is unreliable.
			 * We have seen cases where document.readyState was “complete” while document.body was still null.
			 * And other cases where document.readyState was not complete until after all images on the page were loaded.
			 *
			 * The doScroll method has been seen to succeed while document.body is still null.
			 * And as already mentioned, document.body can be non-null prior to the DOM being available.
			 *
			 * One solution that so far has tested 100% OK is to combine a test for both document.body and success of doScroll.
			 * And this is the method we use as it has so far provided perfect DOMContentLoaded emulation on all pages tested.
			 * Sometimes the doScroll is not available and IE falls back to window.onload so I've used a timer to counter this.
			 *
			 * Note that we use IE's Conditional Compilation to target IE rather than browser sniffing
			 * http://msdn.microsoft.com/en-gb/library/121hztk3(VS.85).aspx
			 */
 
			/*@cc_on @*/
			/*@if (@_win32)
				load_timer = setInterval(function()
				{
					if (document.body) {
						try {
							document.createElement('div').doScroll('left');
							clearInterval(load_timer);
							return init();
						} catch(e) {}
					}
				}, 10);
			@*/
			/*@end @*/
 
			// Safari (versions older than 525 which don't support the DOMContentLoaded event)
			// Use a try-catch statement to make sure no errors (such as Firefox generates) are displayed.
			try {
				if (navigator.userAgent.match(/AppleWebKit\/(\d+)/)[1] &lt; 525) {
					load_timer = setInterval(function()
					{
						if (/loaded|complete/.test(document.readyState)) {
							init();
						}
					}, 10);
				}
			} catch(e) {}
 
			// For other browsers fall back to calling init() on the window.onload event.
			// But first store the original window.onload (just in case it was already set) and execute after init().
			old_onload = window.onload;
			window.onload = function()
			{
				// Calling init() within the window.onload event wont affect modern browsers.
				// The reason being the init() function has already been called.
				// So the stack of waiting functions have been cleared out already.
				init();
 
				// Run the original window.onload after init()
				// as window.onload should run after DOMContentLoaded.
				if (old_onload) {
					old_onload();
				}
 
				alert("window.onload is complete");
			};
		}
 
		// We push the requested function into the stack array,
		// which will be executed either once the DOM has loaded or the window.onload is called.
		load_events.push(func);
	}
})();

This seems to work pretty well for me.

I’m always interested to hear other peoples views on the DOMContentLoaded dilemma so if you see anything that you would change then feel free to share.

Tags: , ,

Leave a Reply