Dojo is big, but it’s not unwieldy. You don’t have to learn it all to be productive. There are probably large pieces of the toolkit that you’ll never even need, but Dojo is built so that if you don’t need something, it doesn’t weigh you down. Where it counts, Dojo is actually quite lean. In the most basic unit of the toolkit—dojo.js
itself—you’ll find enough power to get real work done. It’s worth looking at what you buy when you spend 26KB (gzipped) on JavaScript code rather than a pretty PNG. I think you’ll like what you find.
Let’s start with event handling. I’ll cover the basic techniques first, then finish with a real-world implementation of the concept by looking at Queued, our AIR-based Netflix queue manager.
One of the main reasons people start using JavaScript on a web page is to react to events so you can run validation code or change something on the page without needing to load an entirely new page from the server. This is so common now that having JavaScript in place handling page behavior is considered a basic beginner-level piece of web development. Dojo’s not just for experts; even at this stage of development, the toolkit helps make common tasks easier.
Enter dojo.connect
dojo.connect
is a workhorse. It allows you to arbitrarily run any function when some event occurs. This makes it trivial to set up code to react to clicks, keypresses, etc.:
dojo.connect(dojo.byId("someDiv"), "onclick", function(){
console.log("You clicked someDiv!");
});
It’s very simple. Why not just do dojo.byId("someDiv").onclick = function(){...}
? For a couple of reasons. First, what if you already had useful code in the click handler for the #someDiv
node? It’d be clobbered. With dojo.connect
, you can do the following, and the Right Thing happens:
dojo.connect(dojo.byId("someDiv"), "onclick", function(){
console.log("You clicked someDiv!");
});
dojo.connect(dojo.byId("someDiv"), "onclick", function(){
console.log("Some other code is running!");
});
The second reason to get in the habit of using dojo.connect
is because it handles more than just DOM events—it can connect arbitrary functions together. This is awesome. Thus, Dojo gives you a common API for telling the browser, “when event A happens, run code B.” Here’s a simple example:
function innocentMethod(){
console.log("I'm minding my own business.");
}
dojo.connect("innocentMethod", function(){
console.log("An interloper!");
}
innocentMethod(); // prints both messages to the console
If you’re the kind of person who loves design patterns, think of it as Dojo turning the original function into an Observer. You can call dojo.connect
over and over again to connect any number of functions to something, and it handles all of the tedious tracking itself rather than bothering you with it. When you’re done with the relationship, you call dojo.disconnect
to end it (see the API documentation for details).
Event Driven Design
The more you use dojo.connect
, the less it will be to simply handle DOM events, and the more it will be to conveniently synchronize code. But really, is there any difference between a DOM event firing and something calling an ordinary function? No. It’s just a block of code executing. When you look at things that way (and facilitated by the fact that you make the same dojo.connect
call in every circumstance), it becomes very simple to lay out larger packages of code so they follow the same event driven pattern of execution as the DOM. That’s pretty convenient, because it makes for systems that are very easy to maintain. For example:
var obj = {
doSomething: function(arrayOfMessages){
var message;
for (var i=0; i<arrayOfMessages; i++) {
message = arrayOfMessages[i] || "Doing something";
console.log(message);
}
}
};
obj.doSomething(["foo", "bar"]); // prints both strings to the console
What happens if you want to run some other code at various points in this “algorithm” (using the term loosely)? There’s only one place you can connect to, and if you dojo.connect(obj, "doSomething", function(){...})
, it’ll just run when obj.doSomething
is done. Obviously, adding code to this loop would be trivially easy, but what if the algorithm was more complicated, and what if it was in a library used by a lot of other code? Sometimes you just can’t risk breaking other stuff.
This is where event driven design comes in. The concept is laughably simple: create a stub function for any point where you want somebody to be able to connect, and call it. Even if the function is empty—actually, especially if the function is empty:
var obj = {
onStart: function(){},
onItem: function(){},
onEnd: function(){},
doSomething: function(arrayOfMessages){
var message;
this.onStart();
for(var i=0; i<arrayOfMessages; i++){
message = arrayOfMessages[i] || "Doing something";
this.onItem(message);
console.log(message);
}
this.onEnd();
}
};
obj.doSomething(["foo", "bar"]); // still prints both strings to the console
// this will get called each time through the loop in obj.doSomething()
dojo.connect(obj, "onItem", function(msg){
console.log("Processing item: ", msg);
});
All we’ve done is create a few empty functions and call them in strategic places, but this opens up some new doors; now you can connect to the onStart
, onItem
, or onEnd
methods and run anything you need without getting in the way of the original code (insofar as the stubs were added). This pattern promotes loose coupling of components, which is a well-known way to make even large systems maintainable.
How About Some Real-World Code?
In the interests of teaching the basic concept from scratch, I’ve deliberately kept the example code trivial. Here’s a situation where I recently imposed a little bit of event driven design in a real project.
I’ve been using dojo.behavior
quite a bit lately. As I mentioned in the post introducing Queued, we used it for most if not all of the DOM behavior handlers in that app. Depending on how you construct behaviors, dojo.behavior
uses either dojo.connect
or dojo.publish
as the mechanism for hooking up the actual functions you define (the way we used it in Queued, it’s all dojo.connect
).
Near the end of the push to Queued 1.0, I ran into a situation where I needed one piece of code to listen for an action happening somewhere else, then react to that. Specifically, in search results, if you click a movie’s title or box art to open the More Info dialog, then click the “Add” button to add the movie to your DVD queue, the dialog would close, but the “Add” button that appears directly in the search results wouldn’t update to match the new state (you can see it in the screencast; I add The Office from the dialog, and the “Add” button in the search results list doesn’t change to “In Q” like it should). I had code like this:
// create this as an object so we can use it on multiple selectors below
var movieDialogAddHandler = {
onclick: function(evt){
// code to add the movie to the queue, redacted
}
}
// wire up the DOM behaviors
dojo.behavior.add({
// various handlers, redacted; then:
"#movieInfoTemplateNode .movie span.addButton": movieDialogAddHandler
});
Using an Event Stub
You might expect to be able to simply do dojo.connect(movieDialogAddHandler, "onclick", function(){...})
, but that didn’t work; my function wasn’t being called. Ever. The reason was because dojo.connect
actually replaces the connected function with a custom function that calls the original one followed by the listeners. So by the time my connection was made, the original function didn’t exist at the right spot.
I had a few options:
- Add a state variable to the current scope to indicate whether or not the click that invoked the More Info dialog came from the search results list (the dialog can be invoked from all over the app), and add a few lines to the original function to re-process the results list after any Add operations that happen in that case. Quick and dirty, but also ugly. Queued is supposed to be an example of good code design.
- Rewrite the code such that I was able to address the function in question by name. I didn’t like this option because we were so close to release (I had already recorded the screencast, after all) and I wasn’t too keen on adding to our testers’ load. And from a technical standpoint, I wasn’t particularly happy with the nested
if
chain that it would take to get the connected function to do its stuff only under the right circumstances (similar to the state variable in the first choice). - Insert an event stub and connect to that. Simple. I could make the connection only when the dialog was invoked from the search results list, and we could avoid tracking the extra state.
So now we come down to it. I was able to add an empty function whose reason for existence is expressly to facilitate wiring up code. In the end, the new code was dead simple to add and didn’t require a whole new round of testing. Roughly, it went like this:
function onTitleAddedFromDialog(){} // empty stub, connect to me
// create this as an object so we can use it on multiple selectors below
var movieDialogAddHandler = {
onclick: function(evt){
// code to add the movie to the queue, redacted; then:
onTitleAddedFromDialog();
}
}
// wire up the DOM behaviors
dojo.behavior.add({...}); // same as what we saw earlier
// connect to the stub
var __h = dojo.connect("onTitleAddedFromDialog", function(){
dojo.disconnect(__h);
//
// modify the button state here
//
});
So I added the stub and the connected function at the same time, but if I had been smarter up front, the stub would already have been there, and all I would’ve had to do was create the connection. The actual code is a touch more complicated because of the specifics of the app; if you’d like to read it, it’s in the Queued repository on Google Code, at the bottom of /js/dev/qd/app/movies.js.
Did I invent this concept? Heck no! You see it all over the place—lots of Dojo is built this way, even. However, if you’ve never been exposed to this concept, it can change the way you structure your code. The ability to arbitrarily connect and disconnect pieces of code is a game-changer.
Keep It Simple
To sum up, the basic dojo.js
is full of great tools that make everyday tasks easier; in particular, dojo.connect
is awesome, and putting it to targeted use can make code easier to read and easier to maintain. Though dead simple, the concept of creating stub functions to act as connection targets is a handy tool to have at hand. Using it to fire your own events by spreading stubs throughout your code can make your systems very easy to extend.