Event Listeners

Direct communication channels are constructed by explicitly chaining together functions and/or DOM events so that when one executes, another is automatically invoked afterward. For example, each time an object changes via a "setter" method, you may want to automatically trigger a change in the application's visual interface. Or, perhaps each time one object changes, you might want to automatically update a derived property on another object. The possibilities are endless.

The two primary methods involved in a direct communication scheme are dojo.connect and dojo.disconnect. In short, you use dojo.connect to chain together a series of events. Each call to dojo.connect returns a handle that you should keep and explicitly pass to dojo.disconnect whenever you are ready to dispose of the connection. Conveniently, all handles are disconnected automatically when the page unloads, but manual management of the handles may be necessary for preventing memory leaks in long-running applications that invoke a lot of connections that are used temporarily. (This is particularly the case on IE.) Coming up is the API that was introduced in Chapter 1.

Warning

Don't ever connect anything until after the page is loaded. Trying to use dojo.connect before the page is loaded is a very common mistake and can cause you to sink a lot of time into trying to debug something that isn't very easy to track down the first time you run into it. You should always set up your connections within the function that you pass into dojo.addOnLoad to stay safe.

Setting up and tearing down connections is easy. Here's the basic API:

/* Set up a connection */
dojo.connect(/*Object|null*/ obj,
            /*String*/ event,
            /*Object|null*/ context,
            /*String|Function*/ method) // Returns a Handle

/* Tear down a connection */
dojo.disconnect(/*Handle*/handle);

Tip

For all practical purposes, you should treat the handle that is returned from a call to dojo.connect as an opaque object that you don't do anything with except pass to disconnect at a later time. (In case you're wondering, it is nothing special—just a collection of information that is used to manage the connection internally.)

Let's take a look at an example that illustrates a kind of problem that dojo.connect would be suitable for helping us to solve:

function Foo(  ) {
    this.greet = function(  ) { console.log("Hi, I'm Foo"); }
}

function Bar(  ) {
    this.greet = function(  ) { console.log("Hi, I'm Bar"); }
}

foo = new Foo;
bar  = new Bar;

foo.greet(  );

//bar should greet foo back without foo
//ever having to know that bar exists.

As it turns out, we can solve this little conundrum with one line of code. Modify the previous listing like so, and test this out in Firebug:

function Foo(  ) {
    this.greet = function(  ) { console.log("Hi, I'm foo"); }
}

function Bar(  ) {
    this.greet = function(  ) { console.log("Hi, I'm bar"); }
}

foo = new Foo;
bar  = new Bar;

//Anytime foo.greet fires, fire bar.greet afterward...
var handle = dojo.connect(foo, "greet", bar, "greet"); //set up the connection

foo.greet(  ); //bar automatically greets back now!

The payout for writing that one line of code was pretty high, don't you think? Notice that the second and fourth parameters to dojo.connect are string literals for their respective contexts and that a handle is returned that can later be used to tear down the connection. In general, you always want to tear down the connection at some point, whether it be to accomplish some kind of functional requirement in your design, or when you're performing some final cleanup—such as when an object is destroyed or the page is unloaded. Here's how:

var handle = dojo.connect(foo, "greet", bar, "greet");
foo.greet(  );

dojo.disconnect(handle);

foo.greet(  ); //silent treatment this time

In addition to dojo.connect accomplishing so much with so little effort, notice how clean and maintainable the source code remains. No boilerplate, no spaghetti code, no wiring up your own solution, no maintenance nightmare.

Firing methods off in response to happenings in the page is really useful, but sooner or later you'll need to pass around some arguments. As it turns out, one additional feature of connect is that it automatically passes the arguments from the first context's function to the second context's function. Here's an example that shows how:

function Foo(  ) {
    this.greet = function(greeting) { console.log("Hi, I'm Foo.", greeting); };
}

function Bar(  ) {
    this.greet = function(greeting) { console.log("Hi, I'm Bar.", greeting); };
}

foo = new Foo;
bar = new Bar;

var handle= dojo.connect(foo, "greet", bar, "greet");
foo.greet("Nice to meet you");

As you might imagine, having the arguments get passed around automatically is quite handy, and this is especially the case when a function is connected to a DOM event such as a mouse click because it gives the function instant access to all of the important particulars of the event such as the target, the mouse coordinates, and so on. Let's investigate with yet another example:

//Note that the third argument is skipped altogether since the handler is a
//standalone anonymous function. Using null to placehold the third parameter would
//have produced the very same effect.

dojo.connect(
    dojo.byId("foo"), //Some DOM element
    "onmouseover",
    function(evt) {
      console.log(evt);
    });

If you set up a sample page, wire up the connection, and watch the Firebug console, you'll see that the entire event object is available to the event-handling function, empowering you with just about everything you'd ever need to know about what just happened.

"But it's so easy to specify handlers for DOM events. Why would I even bother with learning another fancy library function?" you wonder. Yes, it may not take a brain surgeon to put together some simple event handlers, but what about when you have a complex application that may need to handle lots of sophisticated event handling based on user preferences, custom events, or some other event-driven behavior? Sure, you could handle all of this work manually, but would you be able to connect or disconnect in one line of code with a single consistent interface that's already been written and battle-tested?

Finally, note that while the examples only illustrated one event being chained to another one, there's no reason you couldn't wire up any arbitrary number of ordinary functions, object methods, and DOM events to fire in succession.

Event Propagation

There may be times when you need to suppress the browser's built-in handling of some DOM events and instead provide custom handlers for these tasks yourself via dojo.connect. Two fairly common cases that occur are when you'd like to suppress the browser from automatically navigating when a hyperlink is clicked and when you'd like to prevent the browser from automatically submitting a form when the Enter key is pressed or the Submit button is clicked.

Fortunately, stopping the browser from handling these DOM events once your custom handlers have finished is as easy as using dojo.stopEvent or the DOMEvent 's preventDefault method to prevent the event from propagating to the browser. The stopEvent function simply takes a DOMEvent as a parameter:

dojo.stopEvent(/*DOMEvent*/evt)

Tip

While you can suppress DOM events that participate in a series of dojo.connect functions, there is no way to stop the dojo.connect event chain from within an ordinary function or JavaScript object method.

The following example illustrates stopEvent at work:

var foo = dojo.byId("foo"); //some anchor element

dojo.connect(foo, "onclick", function(evt) {
    console.log("anchor clicked");
    dojo.stopEvent(evt); //suppress browser navigation and squash any event bubbling
});

Likewise, suppressing automatic submission of a form is just as easy; simply swap out the context of the connection and associate with the submit event. This time, though, we'll use the preventDefault method of a DOMEvent to suppress the event, while allowing bubbling to continue:

var bar = dojo.byId("bar"); //some form element

dojo.connect(bar, "onsubmit", function(evt) {
    console.log("form submitted");
    evt.preventDefault(  ); //suppress browser navigation but allow event bubbling
});

Leveraging Closures with dojo.connect

This section covers some semi-advanced content that you may want to skim over but not get bogged down with your first time through this chapter. Do come back to it though, because sooner or later you'll find yourself needing it.

One-time connections

Consider a situation in which you need to establish and soon thereafter tear down a connection that fires only a single time. The following example gets the job done with minimal effort:

var handle = dojo.connect(
    dojo.byId("foo"),  //some div element
    "onmouseover",
    function(evt) {
      //some handler goes here...
      dojo.disconnect(handle);
    }
);

If you're still getting comfortable with closures, your first reaction might be to object and claim that what we've just done is not possible. After all, the variable handle is returned from the call to dojo.connect, and yet it is being referenced inside of a function that gets passed to dojo.connect as a parameter. To better understand the situation, consider the following analysis of what's going on:

  1. The dojo.connect function executes, and although an anonymous function is one of its parameters, the anonymous function has not yet been executed.

  2. Any variables inside of the anonymous function (such as handle ) are bound to its scope chain, and although they might exist within the function, they aren't actually referenced until the function actually executes, so there's no possible error that could happen yet.

  3. The dojo.connect function returns the handle variable before the anonymous function ever can ever be executed, so when the anonymous function does execute, it is readily available and passed to the dojo.disconnect call.

Setting up connections within a loop

Another situation that frequently occurs during development is that you need to set up connections in the body of a loop. Suppose for now that you simply have a series of elements on the page, foo0, foo1,...foo9, and you want to log a unique number when you move the mouse over each of them. As a first attempt, you might end up with the following code block that will not accomplish what you would expect:

/* The following code does not work as expected! */
for (var i=0; i < 10; i++) {
  var foo = dojo.byId("foo"+i);
  var handle = dojo.connect(foo, "onmouseover", function(evt) {
    console.log(i);
    dojo.disconnect(handle);
  });
}

If you run the snippet of code in Firebug on a page with a series of named elements, you'll quickly find that there's a problem. Namely, the value 10 is always printed in the console, which means that the final value of i is being referenced across the board and that the same connection is erroneously trying to be torn down in each of the 10 handlers. Taking a moment to ponder the situation, however, it suddenly occurs to you that the behavior that is happening actually makes sense because the closure provided by the anonymous function that is passed into dojo.connect doesn't resolve i until it is actually executed—at which time it is in a final state.

The following modification fixes the problem by trapping the value of i in the scope chain so that when it is referenced later it will actually resolve to whatever value it held at the time the dojo.connect statement executed:

for (var i=0; i < 10; i++) {
   (function(  ) {
       var foo = dojo.byId("foo"+i);
       var current_i = i; //trap in closure
       var handle = dojo.connect(foo, "onmouseover",
         function(evt) {
           console.log(current_i);
           dojo.disconnect(handle);
         }
       );
   })(  ); // execute anonymous function immediately
}

The block of code may seem a little bit convoluted at first, but it's actually pretty simple. The entire body of the loop is an anonymous function that is executed inline, and because the anonymous function provides closure for everything that is in it, the value of i is "trapped" as current_i, which can be resolved when the event handler executes. Likewise, the proper handle reference is also resolved because it too exists within the closure provided by the inline anonymous function.

If you've never seen closures in action like this before, you may want to take a few more moments to carefully study the code and make sure you fully understand it. You're probably tired of hearing it by now, but a firm grasp on closures will serve you well in your JavaScript pursuits.

Connecting in Markup

It is worth noting that it is also possible to set up connections for dijits without even the minimal JavaScript writing required by using special dojo/connect SCRIPT tags that appear in markup. You can read more about this topic in Chapter 11 when Dijit is formally introduced.

Get Dojo: The Definitive Guide now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.