— tomauger.com

JavaScript: Unobtrusive Script Execution onLoad or on DOMReady that Plays Nice with Legacy BODY onLoad

It’s been documented quite a bit on the web already: proper use of  unobtrusive JavaScript in your web pages to achieve not only behavioural separation (keeping content, style and functionality separated into HTML, CSS and JavaScript documents respectively) but also degrade gracefully, to support browsers or platforms where JavaScript is not available. These two concepts form the heart of the development ethic in New Web development:

  • Keep your HTML markup completely free of all inline styles and inline JavaScript (in essence, try to use only those attributes inside your HTML tags that are absolutely necessary
  • Do not make JavaScript a requirement to be able to access content / functionality on your sites (anticipate the user who is not using JavaScript during the browsing experience)

Using DOM traversal methods such as getElementsByTagName() and regular expressions on the class attribute, you can fairly easily parse through your markup and unobtrusively add in your behavioural JavaScript, eliminating the need to add onClick=”" or onMouseOver=”" attributes inline. However, this is all still done with JavaScript so at some point, the JavaScript code to do this unobtrusive replacement must be executed. Since it can only be executed once all the relevant DOM elements have been loaded, this has been traditionally done using the (inline) onLoad=”" attribute within the <BODY> tag.

For the purists, this represents an issue since we’re not achieving total separation. One migh argue that even putting the <script> tag in the head of the document is too tight of a coupling, but I don’t know of anyone who has cracked that chestnut yet, nor does it seem likely that we’re even going to go there.

Regardless of whether you’re that die-hard, there’s a certain elegance in letting your scripts execute themselves without any further dependencies in the HTML – you include the external .js file in your HTML’s header tag and it takes care of itself from then on. Set it and forget it.

Standard Method that Plays Nice (sorta)

There are a number of techniques out there that do this, to greater or lesser degrees of success. The technique that I had been using for a while is to examine what’s inside the window.onload property, and then to replace that with a function that executes whatever was in there (presumably other scripts that are using the same technique) and then execute the initialization function within our own script.

// example function to execute your script's initialization function once the page has loaded
// this function plays well with other scripts that use the same method
function addLoadEvent(func) {
	var oldonload = window.onload;
	if (typeof oldonload != 'function') {
		window.onload = func;
	} else {
		window.onload = function() {
			oldonload();
			func();
		}
	}
}

Well, this is lovely. You use it this way: addLoadEvent(myScriptInitializationFunction);

Basically what is happening is we look to see if one (or more) functions were already added to the window’s onLoad property (presumably by other scripts that use this or a similar technique). If there is not (typeof oldonload != ‘function’) then we simply put our function into the window.onload property and away we go. If there already is, we replace the window.onload with a new function that itself encapsulates or executes first the old onload function(s) and then our new one.

It’s a beautiful thing.

Except that it breaks the moment you try to use it with any legacy pages that already have some onLoad functionality embedded into their <BODY> tags.

That’s because this addLoadEvent() function must still execute BEFORE the page is loaded. By the time the page loads, the inline onLoad=”" attribute of the BODY tag clobbers any onload events that we painstakingly crafted, and the whole thing blows up in your face.

Beating onLoad

Rather than replacing the window.onload call stack the moment the script is linked (and before the BODY tag is loaded), we defer the execution to just before the onLoad event would normally fire. That way, any inline onLoad=”" event handlers are added to the window.onload property and we can then unobtrusively add our own functions to the call chain.

Of course, every browser has a different way of dealing with this stuff. So you need to do some sniffing about. The method that I’m using has been synthesized from some great work by better minds than me, but all but one (that I was able to find) are now a little dated. It turns out that if you want to target all modern browsers (IE6, 7, 8, Firefox 2, 3, Opera, Safari, Chrome) you really only need to know about 2 different events: DOMContentLoaded (Moz, Opera, Webkit) and ‘onreadystatechange’ (IE – still gotta be different, don’t you IE – when are you gonna learn?). To make matters even more practical, the DOMContentLoaded gang all use the addEventListener EMACScript standard method of attaching event handlers, while IE has its own attachEvent method, so it’s pretty easy to target the right browser.

Enough Already You Verbose, Pedantic Bastard

Hey, take it easy – I’m a teacher; I can’t help being a little pedantic. Anyway, here’s the goods.

First off, the entire .js library is available and documented here: http://www.tomauger.com/valzLibs/valzOnload/

And here is the code if you’d rather just copy and paste:

/*
	valzOnload.js - unobtrusive script self-execution on document load
	Tom Auger, Zeitguys, 2009
	blog.zeitguys.com
	www.tomauger.com

	You are free to use, distribute, disassemble or ignore as you see fit.

	USAGE:
	addLoadEvent(callback [, executionOrder])

	callback - a function reference or anonymous inline function
	executionOrder (optional) - one of ('prepend', 'append', 'early') where:
		prepend: execute this BEFORE any existing onLoad events
				(but after any other events previously prepended)
		append: execute this AFTER any existing onLoad events
		early: execute this once the DOM has finished loading
				(but BEFORE the document onLoad has fired)
*/

function addLoadEvent(newOnLoad, loadPosition) {
	// define a "global" variable - since we're not using a proper class
	if(! window.__addLoadEventGlobal){
		window.__addLoadEventGlobal = {
			'initComplete' : false,
			'loadEventCallstack' : {
				'prepend' : [],
				'append' : [],
				'early' : []
			}
		};
	}
	var __global = window.__addLoadEventGlobal;

	// register the new event handler as either an append, or a prepend
	if (loadPosition == 'early'){
		__global.loadEventCallstack.early.push(newOnLoad);
	} else if (loadPosition){
		__global.loadEventCallstack.prepend.push(newOnLoad);
	} else { // 'append'
		__global.loadEventCallstack.append.push(newOnLoad);
	}

	// wait for the document to load,
	// and attach the new event handler to the onLoad event queue
	if (!__global.initComplete){ // run this part only once
		if(document.addEventListener) { // Moz, Opera
			document.addEventListener(
				'DOMContentLoaded',
				function() {
					attachLoadEvents();
				},
				false
			);
		} else if(document.attachEvent) { // IE
			document.attachEvent(
				'onreadystatechange',
				function() {
					if(document.readyState == 'complete') {
						attachLoadEvents();
					}
				}
			);
		}

		__global.initComplete = true;
	}
}

function attachLoadEvents(){
	var loadEventCallstack = window.__addLoadEventGlobal.loadEventCallstack;

	// first, execute any "early" handlers
	for(var i=0, l=loadEventCallstack['early'].length; i<l; ++i){
		loadEventCallstack['early'][i]();
	} 

	// now, add the rest either at the front, or the end of the onLoad callstack
	var callstack = loadEventCallstack['prepend'];
	var oldOnLoad = window.onload;
	if (typeof oldOnLoad == 'function'){
		callstack.push(oldOnLoad);
	}

	for(var i=0, l=loadEventCallstack['append'].length; i<l; ++i){
		callstack.push(loadEventCallstack['append'][i]);
	} // i have no idea why i can't just push the whole damned array, 
          // but when i do that, it's no longer recognized as a function 
          // and throws an error.

	window.onload = function(){
		for (var i=0, l=callstack.length; i<l; ++i){
			callstack[i]();
		}
	}
}

Walking through the code

I’ve tried to make this versatile and a bit of a crowd-pleaser. You can use this code as just a better “onload” replacement, or, if you have image-intensive pages and you need to just make sure that the DOM, but not all the image content, has loaded you can use the “early” parameter and this will fire on DOMContentLoaded (or the recalcitrant Microsoft alternative).

The first thing seasoned developers will notice is this ugly __addLoadEventGlobal construct that I’m using. This may be a cop-out. The issue that I’m addressing is that I needed some kind of “global” variable to be able to store the callstack, so that multiple invocations of this function across multiple scripts that implement the same function will not clobber each other. I did not want to assume (in fact, I’m actually assuming the opposite) that people would only use this script in its own, standalone external .js file. I myself am more likely to copy-and-paste this code into any scripts that may use it, to avoid needless dependencies. But it’s possible that multiple script leveraging the same function could be independently linked into a web page. This “global” variable (which I’ve tried to give a fairly unique name) should ssolve that issue.

Each call to this function will register the desired script initialization function as either ‘early’, ‘prepend’, or ‘append’. This determines the ultimate order in which the initialization function will be executed. Early is notable, in that it “beats” onLoad, whereas prepend and append are called onLoad – prepend at the front of the stack, append at the end.

The real “magic”, if you can call it that, is in the two events – DOMContentLoaded or IE’s readyState==’complete’. As mentioned, above, the browser detect is easy in this case as it maps to these two different events and is done by detecting whether the browser has addEventListener or attachEvent. Once we figure that out, we register attachLoadEvents() with the appropriate event handler. When this ‘early’ event fires, attachLoadEvents traverses the global variable’s callstacks (that were built using calls to addLoadEvent() ) and either executes the functions stored as ‘early’, or pushes the ‘prepend’ and ‘append’ calls onto window.onload.