Event-driven programming is a handy model for making graphical user interfaces. The browser developers recognized this and created an event model that ties in to the browser's DOM. Unfortunately, they created slightly different models, and one browser that shall remain nameless (that one browser that lots of people use but is always a pain for developers) has some nasty memory leaks when using DOM events.
The popular Qt GUI library from Trolltech uses an event model called Signals and Slots. When some kind of event occurs, a "signal" is sent, and the signal is received by "slots." It's a simple model, and it's the one that's used by MochiKit.Signal.
Even better than choosing a nice, simple model, MochiKit.Signal provides a consistent model. Every browser serves up the same kinds of events, and none of the browsers will leak memory as a result of hooking in to browser events.
15.2.1. Getting Started with MochiKit.Signal
The first thing to do is to not use the native browser event application programming interfaces (APIs). This is straightforward: if you catch yourself adding onclick or some other on attribute to an element, stop what you're doing and call connect, which we talk about in a minute. The same goes for calls on the DOM nodes themselves (addEventListener) and so on. Finally, MochiKit's own addToCallStack and addLoadEvent functions are tied to traditional DOM events, so avoid those, too. In other words, to get the benefit of Signal (and it's a big benefit, indeed!), you have to make a break from the past.
To get into using Signal, we start with an example, pictured in Figure 15.1, that shows you what the signals themselves look like and how you can flexibly attach them to any DOM node.
Figure 15.1. Signal Example
This demo lets you choose one of the DOM nodes defined on the page and then the kind of event you want to watch. As the events occur, you'll see output in the logging pane with the details of the event:
Here is the signal.js file:
var currentSelected = null;
This sample introduces two functions from MochiKit.Signal. The most important function available in Signal is connect, because that's what activates the whole thing. Our use of connect here is about the simplest it could be. We're passing in a string with the ID of the DOM node we're interested in, the name of the event we're interested in, and a function. MochiKit hooks things up so that the event is passed neatly along to our function. You'll see more about connect in just a minute.
We also use disconnectAll here, passing in the ID of the node that was previously selected. That removes all signals that had previously been set up for that node. That might seem a little heavy-handed. What if you want to keep some signals? The answer to that starts with a deeper look at connect.
15.2.2. Connecting and Disconnecting
As mentioned previously, the form of connect we used looks like this: connect(src, signal, func); and is about as simple as it gets. Lucky for us, connect never really gets much harder than that. The full signature is connect(src, signal, dest, func). src can be a string that is the ID of a DOM node, or it can be any object that you want to get a signal from. signal is the name of the signal you're setting up.
dest is an object that you want to define the slot on. When you use the form of connect with a dest object, func can either be a string with the name of the function on the object or a function object itself. Either way, when the function is called, this will be bound to dest.
The connect function also returns a value that we didn't use in the example program. The value is an identifier that uniquely identifies that connection. You can call disconnect(ident) to remove that signal connection without disturbing any other connections.
15.2.3. Using MochiKit's Cross-Browser Event Objects
Often, an event handler (slot) is hooked up to multiple signals. So, one of the first things you need to know is where that signal came from. It turns out that there are potentially two answers to this. Consider this case:
<table > <tr> <td>Here is some text in a table</td> </tr> </table>
If you connect to the "foo" table, and click the <td> node in there, there are two interesting possibilities for the origination of the event: "foo", where the signal was defined; and that <td>, where the actual click took place. Calling src() on the event will get you "foo" back, and target() will give you the <td> node where the click took place.
It turns out that there's actually a third element that you are interested in for some types of events: what element did the user move to? Specifically, for onmouseout and onmouseover events, you want to know about this third element, and the relatedTarget() method will give it to you.
A slot can also accept multiple kinds of signals, and the type() method on the event will tell you exactly which kind you're dealing with. Note that the return value of type() does not include the on prefix, so you'll have click instead of onclick, for example.
Next, you'll often want to know a bit more about the event. Questions you might want to ask an event object include these: What key was pressed? Where was the mouse when the button was clicked? Was it a Shift-click or a Ctrl-click?
The modifier() method returns an object with these properties: shift, ctrl, meta, alt, any. These will be true if the given modifier key was pressed, and any is true if any one of the modifier keys was pressed.
If you are listening for keyboard signals, you'll want to use the key() method on event. This method returns an object with two properties: code (a numeric code for the key) and string (a string representation of the key that was pressed). The value for string varies depending on whether you're listening for onkeyup/onkeydown or onkeypress events. You should use onkeypress for "printable" characters. When you do, you'll be rewarded with a string value that matches the character typed ("a" if the A key is pressed). However, special keys are not represented in onkeypress. If you're looking for function keys or other special keys, use onkeyup or onkeydown. The special keys are consistently named across browsers as KEY_ESC, KEY_F1, KEY_ARROW_DOWN, and so on, which is much easier to use than figuring out the character codes for the keys.
If you are listening for mouse signals, use the mouse() method on the event to get information about the mouse cursor position and the buttons that are pressed. The page and client properties are MochiKit.Style.Coordinates objects that tell you where in the HTML document and where within the browser window the cursor is located, respectively.
The button property returns an object with left, right, and middle properties that are true depending on which button was pressed.
As of this writing, the MochiKit documentation lists some browser bugs that impact the use of mouse(), so be sure to check the latest MochiKit documentation for up-to-date information.
Finally, there are methods to determine what happens next after your event handler. These are part of the DOM as defined by the W3C. stopPropagation() prevents the event from triggering any other event handlers connected to a parent or child node. preventDefault() allows the event to be passed along to other handlers, but stops the default behavior from happening if the event is cancelable. As an example, preventDefault can stop characters from being added to a text area or a check box from being checked without preventing other event handlers from acting on the event. The preventDefault behavior is akin to the behavior you get if you return false in the old DOM event handling model.
15.2.4. Custom Events
If signals and slots are so useful a model for DOM events, why can't you use them for other things in your app? Short answer: You can. There's one more function in the Signal package that has not been discussed: signal(src, signal, …). That function fires off a signal and passes all the additional parameters to the connected slots.
Here is something you can try in the interactive interpreter:
The signals and slots model provides an easy way to produce loosely coupled, event-driven scripts. This model works quite nicely for dealing with DOM events, and larger Ajax-style applications will likely benefit in their own code, too.