dojo.required Reading: It Ain’t Loaded ‘Til It’s Loaded

August 23, 2010 by Ken · 8 Comments 

One of the most fundamental yet powerful features of the Dojo Toolkit is its module and dependency management system, harnessed via dojo.require. I’ve seen one important detail elude countless developers, however: the distinction between when a module is dojo.required, and when it is actually loaded and ready for use. It’s very easy to mishandle this, and the nature in which it tends to come back to bite leaves no indication of the actual problem to the unwary developer. This usually crops up in the form of the following question:

  • I’ve dojo.required widget X using cross-domain Dojo, but when I go to instantiate X, I get an exception saying that X is not a constructor!

Depending upon what module was requested, the exception might instead say SomeNamespaceAboveX is undefined.

Interestingly, both the API and Reference Guide pages for dojo.require actually sort of answer this question (or rather, show how to avoid encountering it completely), but in this post I aim to clarify why this happens (and perhaps encourage a few wandering souls to read the docs more often).

Recipe for Disaster

There are two things in common with pretty much every instance of this problem:

  • The developer is using dojo across domains (xd), or has debugAtAllCosts=true in djConfig (which also uses the xd loader logic).
  • The developer is issuing the dojo.require call in question directly within a script block; that is, it is not within a module which is itself dojo.required.

Despite the perhaps over-dramatic subheadline, there is absolutely nothing wrong with doing either of these two things! However, these preconditions in combination are what will give you grief if you go about handling dojo.requires The Wrong Way. Next we shall examine how and why these ingredients combine for an exceptional result. (See what I did there?)

The Wrong Way

There are two very common coding mistakes that, combined with the above preconditions, can lead to this problem. Here is the first:

<script type="text/javascript">
    dojo.require('dijit.form.Button');
    new dijit.form.Button({label:'Hello World!'}).placeAt(dojo.body());
</script>

Failing this, but not knowing better, a developer might also try this:

<script type="text/javascript">
    dojo.addOnLoad(function() {
        dojo.require('dijit.form.Button');
        new dijit.form.Button({label:'Hello World!'}).placeAt(dojo.body());
    }
</script>

Why is this wrong?

Both of these examples will fail for the same reason: they both assume that dojo.require operates synchronously. Here’s where one of those preconditions comes in: dojo’s cross-domain loader operates asynchronously – moreover, once 2.0 comes around, chances are dojo.require will always operate asynchronously. Therefore, this is a suboptimal assumption to make – and a downright dangerous one if you expect to ever consider going cross-domain or upgrading to 2.0 in the future.

Fortunately, dojo.addOnLoad (also known as dojo.ready in dojo 1.4 and up) is specifically tailored to alleviate this concern; it’s just a matter of using it and dojo.require together properly.

Using dojo.addOnLoad

Some people may not be fully aware of what dojo.addOnLoad does. Certainly it waits until a certain point before executing the functions passed to it, but exactly what does it wait for?

  • It waits for the page’s DOM to be loaded and thus ready for modifications – document.DOMContentLoaded when supported, window.onload otherwise (e.g. IE < 9).
  • It waits for any previously dojo.required modules to finish loading.

The second bullet above is of particular importance to the present topic. This means that with a trivial modification to the second example above, we can get it to work:

<script type="text/javascript">
    dojo.require('dijit.form.Button');
    dojo.addOnLoad(function() {
        new dijit.form.Button({label:'Hello World!'}).placeAt(dojo.body());
    }
</script>

Notice that the dojo.require call has been placed before the dojo.addOnLoad call rather than inside it. In this case, dojo.addOnLoad will see that a module has been requested, and will wait until it is sure the module has actually been fully loaded before executing. This way, code inside the function passed to dojo.addOnLoad will have no trouble utilizing the dojo.required module, regardless of whether dojo.require happens to be operating synchronously or asynchronously.

There is actually another solution as well:

<script type="text/javascript">
    dojo.addOnLoad(function() {
        dojo.require('dijit.form.Button');
        dojo.addOnLoad(function() {
            new dijit.form.Button({label:'Hello World!'}).placeAt(dojo.body());
        }
    }
</script>

This example nests one dojo.addOnLoad inside another dojo.addOnLoad! It may seem rather excessive, and in the majority of cases it probably is. However, what distinguishes this pattern from the first is that the dojo.require call is not fielded whatsoever until the initial DOM of the page has loaded, which might be preferable in some scenarios.

Exception to the Rule

You might still be wondering about that other lingering precondition from earlier. Here’s the kicker: when dealing with code inside a module which itself is being dojo.required, you do not need to bundle dependent code inside dojo.addOnLoad to ensure that prerequisite modules are loaded first.

For example, this will work fine:

/* File 1, my/Bar.js */
dojo.provide('my.Bar');

dojo.require('my.Foo');

dojo.declare('my.Bar', my.Foo, {
    /* ... */
});

/* File 2, something.html */
<script type="text/javascript">
    dojo.require('my.Bar');
    dojo.addOnLoad(function() {
        /* ... do stuff with my.Bar or my.Foo here */
    });
</script>

Notice that in the module being dojo.required, we don’t use dojo.addOnLoad to wait for my.Foo to be loaded before referencing it in the dojo.declare of my.Bar. However, this entire file will be dojo.required into the application in itself, and dojo performs a bit of magic to guarantee that nested requires will be loaded before any code referencing them is executed. Therefore, it’s unnecessary to go throwing dojo.addOnLoads into every single module you write. The only time you need be actively concerned about waiting for dojo.requires to be loaded is when using them directly within scripts that are not being dojo.required themselves.

Conclusion

To recap…

  • I’ve dojo.required widget X using cross-domain Dojo, but when I go to instantiate X, I get an exception saying that X is not a constructor!

Chances are you’re seeing this error because you’ve dojo.required something directly in a page, but it has not actually finished loading before you’ve started using it. In this case be sure that any code referencing the dojo.required modules is inside a dojo.addOnLoaded function placed after the dojo.requires.

Further Reading

Thanks

  • Peter Higgins for answering a couple of questions confirming some facts, and giving the first draft of this post a once-over
  • Rebecca Murphey for giving feedback on the draft, and giving me the final additional prod to write this darned post already.
  • Brian Arnold, for shedding some light on this topic in the past on IRC

Apologies

I apologize for the silence over the past 7 weeks. I initially intended to attempt to keep up a post per week, but some of these posts have proven to be quite time-consuming, and after my vacation in July I sort of ran out of steam as other things began to demand attention during my free time. I will certainly continue to post as the opportunity arises.

As always, drop a line if something was unclear, or if you’ve got something to add.

Comments

8 Responses to “dojo.required Reading: It Ain’t Loaded ‘Til It’s Loaded”
  1. Nice post! Pete’s a great guy; he helped me understand this exact same issue last year. And I hear ya about the time-consuming nature of this kind of blogging. My advice is to do things at your own pace, publish stuff when you can, but don’t stress if it’s not on a perfectly regular schedule. Quality over quantity. ;)

  2. Val says:

    Ken — your knowledge of JavaScript is just friggin amazing and you can also use human language pretty well. Thanks for this blog and keep it up ;)

  3. Chris Barrett says:

    KGF: So I thought that once the first addOnLoad was called, all subsequent addOnLoads were called in a synchronous fashion?

    • Ken says:

      To a point, yes. Dojo fires all functions bound via addOnLoad/ready consecutively/synchronously to each other when it determines the time is right (e.g. DOM is ready and no modules are still waiting to be loaded).

      The main exception to this is if you were to further dojo.require more modules after the first batch of addOnLoad/ready callbacks were fired, in which case you would again want to place dependent code into further addOnLoad/ready callbacks. Every time a dojo.required module (or a batch of modules) finishes loading, dojo again fires any addOnLoad/ready callbacks that were registered since the previous “ready” state. If an addOnLoad/ready callback is registered when nothing is waiting to load, it is fired immediately.

      Hope that addressed the question; I admit I’m not entirely sure what you were after.

      • Chris Barrett says:

        Yes it does, my question was just because I thought once the first addOnLoad triggers, it would just hit the 2nd addOnLoad and go through it because the first did. I see that I am wrong. :P

  4. Brian Feinberg says:

    Thanks for the great post. I had the same misconception that Chris had because I thought addOnLoad/ready was tied only to window.onload or document.DOMContentLoaded events.

    One thing I’m wondering is whether addOnLoad/ready is triggered by the completion of any other async events besides xd dojo.require such as loading a ContentPane widget. I’m guessing maybe not since a deferred object seems to be the intended way to handle that scenario.

  5. Børge A says:

    Ken, Thanks for this! :)

    Been abusing workarounds in my project, but I finally managed to organize the modules properly and remove some ugly tags and addOnLoad’s

Speak Up