Let's Make a Framework: Return of the DOM

2010-08-12 00:00:00 +0100 by Alex R. Young

Welcome to part 25 of Let's Make a Framework, the ongoing series about
building a JavaScript framework. This part is about handling when the
DOM had loaded.

If you haven't been following along, these articles are tagged with
lmaf. The project we're creating is called Turing and is available on GitHub:

The Point

I'm taking us on a diversion this week because I'm tired of creating
Turing examples without a DOM ready handler. This is a core feature of
browser-based frameworks, and it's the key to unobtrusive JavaScript.

The point is to watch for the DOM to finish loading, rather than the
entire document (with all of the related images and other assets). This
is useful because it gives the illusion of JavaScript-related code being
instantly available. If this wasn't done, JavaScript code could be
evaluated after the user has started interacting with the document.

Most jQuery users use this feature without realising it's there:

$(document).ready(function() {
  // Let the fun begin

Here's the core of what makes this happen:

bindReady: function() {
  if ( readyBound ) {

  readyBound = true;

  // Catch cases where $(document).ready() is called after the
  // browser event has already occurred.
  if ( document.readyState === "complete" ) {
    return jQuery.ready();

  // Mozilla, Opera and webkit nightlies currently support this event
  if ( document.addEventListener ) {
    // Use the handy event callback
    document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );

    // A fallback to window.onload, that will always work
    window.addEventListener( "load", jQuery.ready, false );

  // If IE event model is used
  } else if ( document.attachEvent ) {
    // ensure firing before onload,
    // maybe late but safe also for iframes
    document.attachEvent("onreadystatechange", DOMContentLoaded);

    // A fallback to window.onload, that will always work
    window.attachEvent( "onload", jQuery.ready );

    // If IE and not a frame
    // continually check to see if the document is ready
    var toplevel = false;

    try {
      toplevel = window.frameElement == null;
    } catch(e) {}

    if ( document.documentElement.doScroll && toplevel ) {

Prototype and other frameworks have similar code. It's not surprising
that Prototype's DOM loaded handling references Resig and other
prominent developers (Dan Webb, Matthias Miller, Dean Edwards, John
Resig, and Diego Perini). jQuery.ready gets called through
either a modern DOMContentLoaded event, or
onload events.

I like the way Prototype fires a custom event when the document is
ready. Prototype uses a colon to denote a custom event, because these
have to be handled differently by IE --
element.fireEvent('something else', event) causes an
argument error. I tried to duplicate that in Turing, but it'd take a
fair amount of work to adapt Turing's current event handling so I left
it out for now and used an array of callbacks instead.

Setting up multiple observer callbacks will work:

  p { color:red; }

  $(document).ready(function() {

  $(document).ready(function() {

  $(document).ready(function() {


I'm shooting for this:

turing.events.ready(function() {
  // The DOM is ready, but some other stuff might be

Implementing "onready"

I've based the current code on jQuery. It's just enough to work
cross-browser and do what I need it to do. It's all handled by private
methods and variables. The last thing to get called is

function ready() {
  if (!isReady) {
    // Make sure body exists
    if (!document.body) {
      return setTimeout(ready, 13);

    isReady = true;

    for (var i in readyCallbacks) {

    readyCallbacks = null;

This is just a tiny snippet, but to summarise the rest of the code:

  1. The public method, turing.events.ready calls
  2. If bindOnReady has already been called once, it will
  3. This method sets up DOMContentLoaded as an event, and
    will fall over to simply call ready()
  4. The "doScroll check" is used for IE through
    DOMReadyScrollCheck, which also calls ready() when it's done
  5. setTimeout and recursive calls to
    DOMReadyScrollCheck make this happen

The isReady variable is bound to the closure for
turing.events, so it's safely tucked away where we need it.
The rest of the code is similar to jQuery's -- after going through the
tickets referenced in the code comments relating to IE problems, I
didn't have enough time to create my own truly original version.

Something from the Archives

If you're interested, I previously used the following code which I
created (based on various blog posts and other bits of research) about 4
years ago:

function init(run) {
  // quit if this function has already been called
  if (arguments.callee.done) return;

  // flag this function so we don't do the same thing twice
  arguments.callee.done = true;

  // kill the timer
  if (_timer) clearInterval(_timer);

  // do stuff

/* for Mozilla/Opera9 */
if (document.addEventListener) {
  document.addEventListener("DOMContentLoaded", init, false);

/* for Internet Explorer */
/*@cc_on @*/
/*@if (@_win32)
  var script = document.getElementById("__ie_onload")

  script.onreadystatechange = function() {
    if (this.readyState == "complete") {
      init() // call the onload handler
/*@end @*/

/* for Safari */
if (/WebKit/i.test(navigator.userAgent)) {
  var _timer = setInterval(function() {
    if (/loaded|complete/.test(document.readyState)) {
      init(); // call the onload handler
  }, 10);

/* for other browsers */
window.onload = init;

As you can see, the concept of a loaded DOM is a messy thing to deal
with. It's not really the fault of any one browser manufacturer, it's
just growing pains as a consequence of JavaScript evolving through the
DOM levels.

I had this old code kicking around for years, and I was convinced the
whole DOM loaded thing was over. But last year Dean Edwards wrote
Callbacks vs Events on
the topic. In particular he deals with handling
DOMContentLoaded through custom events, and the article has
some interesting comments.


The current version of turing.dom.ready isn't exactly what
I wanted, and I had to cut short some features due to time constraints
(I spent about 4 hours on this article and had to call it a day). The code is here:
turing.dom.js -- if you can help simplify or improve it, I'd appreciate it! Maybe it could be a guest post?