Chapter 3. Event Listeners and Pub/Sub Communication

Base provides extremely useful and versatile utilities for communication between JavaScript objects, DOM nodes, and any combination thereof. This chapter introduces these constructs as well as guidelines for when each of them might be most appropriate to employ. As writing portable code that involves DOM events necessarily depends on a standardized event model, you'll also learn a little bit about how Dojo works behind the scenes to smooth out some of the inconsistencies amongst mouse and keyboard events. The chapter concludes with a discussion of publish/subscribe communication, which provides a great vehicle for realizing an architecture with loosely coupled components.

Event and Keyboard Normalization

Some of the oldest code in the toolkit was written to smooth out inconsistencies with the underlying event model amongst different browsers. This section provides a brief overview of the events that you can count on being normalized when you use Dojo to develop an application. The basis of standardization is the W3C model.

Mouse and Keyboard Event Normalization

The dojo.connect machinery that you'll read about in the following section often involves a mouse event on a particular DOM node. Whenever you use Dojo, you can rest assured that the following mouse and keyboard events are supported in accordance with the W3C standard:

onclick
onmousedown
onmouseup
onmouseover
onmouseout
onmousemove
onkeydown
onkeyup
onkeypress

In addition to supporting the standardized W3C events, the nonstandard onmouseenter and onmouseleave events are also supported.

In addition to being able to count on these events firing in a standardized way, you can also rely on the event objects that are passed to event handling functions to also be normalized. In fact, if you ever have a need to normalize events yourself, you can use the following Base function:

dojo.fixEvent(/*DOMEvent*/ evt, /*DOMNode*/ sender) //Returns DOMEvent

DOMEvent is the standard convention that'll be used in the rest of the book to refer to the DOM event objects.

In other words, pass in the event and the node that should be treated as the current target, and you'll get back a normalized event that you can count on meeting the W3C specification. Table 3-1 provides a synopsis of some of the most commonly used properties on a DOMEvent.[11]

Table 3-1. Commonly used properties on DOMEvents

Name

Type

Comment

bubbles

Boolean

Indicates whether the event can bubble up the DOM tree.

cancelable

Boolean

Indicates whether the event can have its default action prevented.

currentTarget

DOMNode

The current node whose event listeners are being processed. (Useful for when an event bubbles.)

target

DOMNode

The node that originally received the event.

type

String

The type of the event, e.g., mouseover.

ctrlKey

Boolean

Indicates if the Ctrl key was depressed when the event fired.

shiftKey

Boolean

Indicates if the Shift key was depressed when the event fired.

metaKey

Boolean

Indicates if the Meta key was depressed when the event fired. (This is the Command key on an Apple computer.)

altKey

Boolean

Indicates if the Alt key was depressed when the event fired.

screenX

Integer

The X coordinate where the event occurred on the screen.

screenY

Integer

The Y coordinate where the event occurred on the screen.

clientX

Integer

The X coordinate where the event occurred on the browser window.

clientY

Integer

The Y coordinate where the event occurred on the browser window.

Standardized Key Codes

The toolkit also exposes the following table of named key codes, which are available via dojo.keys. For example, you might detect whether a Shift + Enter key combination was processed via the following code snippet:

/* ... snip ... */
  if (evt.keyCode == dojo.keys.ENTER && evt.shiftKey) {
    /* ... */
  }
/* ... snip ... */

Table 3-2 provides a list of the constants for accessing keyboard events.

Table 3-2. A listing of the constants Dojo provides for accessing keyboard events via dojo.keys

BACKSPACE

DELETE

NUMPAD_DIVIDE

TAB

HELP

F1

CLEAR

LEFT_WINDOW

F2

ENTER

RIGHT_WINDOW

F3

SHIFT

SELECT

F4

CTRL

NUMPAD_0

F5

ALT

NUMPAD_1

F6

PAUSE

NUMPAD_2

F7

CAPS_LOCK

NUMPAD_3

F8

ESCAPE

NUMPAD_4

F9

SPACE

NUMPAD_5

F10

PAGE_UP

NUMPAD_6

F11

PAGE_DOWN

NUMPAD_7

F12

END

NUMPAD_8

F13

HOME

NUMPAD_9

F14

LEFT_ARROW

NUMPAD_MULTIPLY

F15

UP_ARROW

NUMPAD_PLUS

NUM_LOCK

RIGHT_ARROW

NUMPAD_ENTER

SCROLL_LOCK

DOWN_ARROW

NUMPAD_MINUS

 

INSERT

NUMPAD_PERIOD

 

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.

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);

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)

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.

Publish/Subscribe Communication

While there are plenty of times when the direct "chained" style of communication provided by dojo.connect is exactly what you'll need to solve a problem, there are also a lot of times when you'll want a much more indirect "broadcast" style of communication in which various widgets communicate anonymously. For these circumstances, you might instead use dojo.publish and dojo.subscribe.

A classic example is a JavaScript object that needs to communicate with other objects in a one-to-many type relationship. Instead of setting up and managing multiple dojo.connect connections for what seems like one cohesive action, it's considerably simpler to have one widget publish a notification that an event has transpired (optionally passing along data with it) and other widgets can subscribe to this notification and automatically take action accordingly. The beauty of the approach is that the object performing the broadcast doesn't need to know anything whatsoever about the other objects—or even if they exist, for that matter. Another classic example for this kind of communication involves portlets—pluggable interface components (http://en.wikipedia.org/wiki/Portlet) that are managed within a web portal, kind of like a dashboard.

The OpenAjax Hub (http://www.openajax.org/OpenAjax%20Hub.html), which you'll read more about in Chapter 4, calls for publish/subscribe communication to be used as the vehicle for effectively employing multiple JavaScript libraries in the same page.

In many situations, you can achieve exactly the same functionality with pub/sub style communication as you could by establishing connections, so the decision to use pub/sub may often boil down to pragmatism, the specific problem being solved, and overall convenience of one approach over another.

As a starting point for determining which style of communication to use, consider the following issues:

  • Do you want to (and can you reliably) expose an API for a widget you're developing? If not, you should strongly prefer pub/sub communication so that you can transparently change the underlying design without constantly wrangling the API.

  • Does your design contain multiple widgets of the same type that are all going to be responding to the same kind of event? If so, you should strongly prefer connections because you'd have to write additional logic to disambiguate which widgets should respond to which notifications.

  • Are you designing a widget that contains child widgets in a "has-a" relationship? If so, you should prefer setting up and maintaining connections.

  • Does your design involve one-to-many or many-to-many relationships? If so, you should strongly prefer pub/sub communication to minimize the overall burden of communication.

  • Does your communication need to be completely anonymous and require the loosest coupling possible? If so, you should use pub/sub communication.

Without further delay, here's the pub/sub API. Note that in the case of dojo.subscribe, you may omit the context parameter and the function will internally normalize the arguments on your behalf (just as was the case with dojo.connect ):

dojo.publish(/*String*/topic, /*Array*/args)
dojo.subscribe(/*String*/topic, /*Object|null*/context,
   /*String|Function*/method) //Returns a Handle
dojo.unsubscribe(/*Handle*/handle)

Just as the handle that is returned from dojo.connect should be considered opaque, the same applies here for dojo.subscribe.

Let's get to work with a simple example involving dojo.subscribe and dojo.publish :

function Foo(topic) {

  this.topic = topic;

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

    /* Foo directly publishes information, but not to a specific destination... */
    dojo.publish(this.topic);
  }

}

function Bar(topic) {

  this.topic = topic;

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

  / * Bar directly subscribes to information, but not from a specific source */
  dojo.subscribe(this.topic, this, "greet");

}

var foo = new Foo("/dtdg/salutation");
var bar = new Bar("/dtdg/salutation");

foo.greet(  ); //Hi, I'm Foo...Hi, I'm Bar

Although there is no formal standard, the toolkit uses the convention of prefixing and using a forward slash to separate the components of topic names. An advantage of this approach is that the forward slash is uncommon enough in JavaScript code that it is fairly easy to spot (whereas using a dot to separate topic names in source code would be a lot more difficult).

As you can see, whereas connect involves a connection from a specific source to a specific destination, publish/subscribe involves a broadcast that could be sent from any source and could be received by any destination that cares to respond to it in some way. Some amazing power comes built-in with a very loosely coupled architecture because with minimal effort and great simplicity comes the ability to have what amounts to an application that is conceptually a collection of coherent plug-ins.

Let's illustrate how to unsubscribe with an interesting variation on Bar 's implementation. Let's have Bar respond to the topic that Foo publishes only a single time:

function Bar(topic) {

    this.topic = topic;

    this.greet = function(  ) {
        console.log("Hi, I'm bar");
        dojo.unsubscribe(this.handle);

        //yackety yack, don't talk back
    }

    this.handle = dojo.subscribe(this.topic, this, "greet");
}

Note that you can also send along an array of arguments by providing an additional second argument to publish that is an Array of values, which gets passed to the subscribe handler as named parameters.

It's a common mistake to forget that the arguments passed from dojo.publish must be contained in an Array and that dojo.subscribe 's handler receives these arguments as individual parameters.

For a final rendition of our example, let's say you are not able to reliably change Foo 's greet method to include a dojo.publish call because an external constraint exists that prohibits it; perhaps it is code that you do not own or should not be mucking with, for example. Not to worry—we'll use another function, dojo.connectPublisher, to take care of the publishing for us each time a particular event occurs:

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

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

}

var foo = new Foo;
var bar = new Bar;

var topic = "/dtdg/salutation";
dojo.subscribe(topic, bar, "greet");
dojo.connectPublisher(topic, foo, "greet");

foo.greet(  );

In case you're interested, behind-the-scenes connectPublisher is basically using dojo.connect to create a connection between a dojo.publish call each time a particular function is called.

In this final example, the primary takeaway is that the dojo.connectPublisher call allowed us to achieve the same result as adding a dojo.publish call to its greet method, but without mangling its source code to achieve that result. In this regard, foo is an indirect sender of the notification and is not even aware that any communication is going on at all. Bar, on the other hand, as a subscriber of the notification, did require explicit knowledge of the communications scheme. This is essentially the opposite of a typical dojo.connect call in which the object that provides the context for a connection has explicit knowledge about some other object or function that provides the "target" of the connection.

Summary

After reading this chapter, you should:

  • Be aware that dojo.connect standardizes the event Object that is passed into event-handling functions, providing portability across platforms

  • Understand how dojo.connect allows you to arbitrarily chain DOM events, JavaScript Object events, and ordinary functions together to create an event-driven response

  • Use publish/subscribe to facilitate connections and achieve a loosely coupled communications backbone in an application

  • Be aware of some of the considerations and trade-offs for using dojo.connect versus pub/sub in an application architecture

Next up is AJAX and server communication.



[11] Dojo currently normalizes against the DOM2 specification, which is available at http://www.w3.org/TR/DOM-Level-2-Events/events.html. See http://www.w3.org/TR/DOM-Level-3-Events/events.html for an overview of the DOM3 Event specification.