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
|
Tip
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
Tip
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, “Commonly used properties on DOMEvents” 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 |
|---|---|---|
|
|
Boolean |
Indicates whether the event can bubble up the DOM tree. |
|
|
Boolean |
Indicates whether the event can have its default action prevented. |
|
|
DOMNode |
The current node whose event listeners are being processed. (Useful for when an event bubbles.) |
|
|
DOMNode |
The node that originally received the event. |
|
|
String |
The type of the
event, e.g., |
|
|
Boolean |
Indicates if the Ctrl key was depressed when the event fired. |
|
|
Boolean |
Indicates if the Shift key was depressed when the event fired. |
|
|
Boolean |
Indicates if the Meta key was depressed when the event fired. (This is the Command key on an Apple computer.) |
|
|
Boolean |
Indicates if the Alt key was depressed when the event fired. |
|
|
Integer |
The X coordinate where the event occurred on the screen. |
|
|
Integer |
The Y coordinate where the event occurred on the screen. |
|
|
Integer |
The X coordinate where the event occurred on the browser window. |
|
|
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, “A listing of the constants Dojo provides for accessing keyboard events via dojo.keys” 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, Toolkit Overview.
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 timeIn 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:
varhandle= 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:
The
dojo.connectfunction executes, and although an anonymous function is one of its parameters, the anonymous function has not yet been executed.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.The
dojo.connectfunction returns thehandlevariable before the anonymous function ever can ever be executed, so when the anonymous function does execute, it is readily available and passed to thedojo.disconnectcall.
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, Dijit Overview 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.
Tip
The OpenAjax Hub (http://www.openajax.org/OpenAjax%20Hub.html), which you'll read more about in Chapter 4, AJAX and Server Communication, 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)
Tip
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 BarTip
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.
Warning
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( );Tip
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.connectstandardizes the eventObjectthat is passed into event-handling functions, providing portability across platformsUnderstand how
dojo.connectallows you to arbitrarily chain DOM events, JavaScript Object events, and ordinary functions together to create an event-driven responseUse 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.connectversus 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.





Add a comment



Add a comment