TruerWords Logo
Google
 
Web www.truerwords.net

Search TruerWords

Welcome
Sign Up  Log On

Creating Custom Events with JavaScript: Decoupling

Change list is at the end.

The Elevator Pitch

Many "Web 2.0" applications suffer from too-tight-coupling between the various javascript objects used to model the data and control the interface. This has always been seen as a necessary evil because there seemed to be no good alternative.

One common solution in desktop applications is called "event driven programming." Not just user-supplied events like mouse clicks, hovers, and window scrolls (obviously, we do all of that in our javascript apps already), but actual data-driven or state-driven events such as "message selected" or "preference changed".

Truly custom events, when used correctly, allow you to decouple many of the objects in your application. This leads to better self-containment and maintainability.

It has generally been considered impossible — or at least too difficult — to create truly custom events in our applications. There is a W3C spec for custom events, but it's not supported by most of the browsers so is really of no use to us. (Note that some javascript libraries, like YUI and Dojo, have implemented their own "custom events" systems from scratch.)

Keep reading and we'll examine the problem a little more closely, and consider my solution to it. (There may be others, but all I found was attempt after attempt to duplicate the features already provided by the browser.)

In a hurry? You can jump to the ultra short summary, at the risk of missing something important.

Assumption

Let's make something clear from the start: this 'essay' assumes a fairly deep understanding of JavaScript. It's intended for people who write JavaScript for a living, or at least those who are very comfortable with its objects, prototypes, and events.

If you read prototype.js the first time and thought, "Wow! bind() is a clever way to do object callbackcs!" (or if you happen to be Sam Stephenson, prototype's author), then read on: this essay is for you.

On the other hand, if your reaction was something closer to, "What the <bleep> is this!? I thought this was JavaScript! Where are all of the alert()'s ? What the heck is a .map() ?" then you might find this a wee bit confusing. I'm not saying, "don't read it," but am suggesting that you probably aren't writing scripts with enough complexity to need the feature(s) I've offered here.

Introduction

As AJAX-based "Web 2.0" applications become more common and more complex, the client side of these applications becomes increasingly difficult to maintain and extend. This seems obvious: as more is sent to and done in the browser, more work is required by the developer.

One common situation that often leads to extra complexity is when multiple parts of an application must be updated in response to some type of change.

Consider the standard browser-based email application. When a user clicks on a message line in a list of messages, three or more parts of the user interface must be updated:

  • the line that was clicked is marked as "read", which is usually shown with a change from bold to plain text
  • the message-display pane is updated with the contents of the new message
  • the count of "unread messages" is decremented by one

What happens when you decide on a better (visual) design for your application, or you add hierarchal mailboxes, or any other change? The answer is that changes to one area require changes in many of the other areas, because they're all bound together with direct calls (almost the javascript version of "deep linking").

In fact, thousands of other examples are possible. Complexity is always introduced when different parts of an application are tightly coupled, because a change in one area requires change in — or causes breakage in — all of the others.

Even if you've written your code in a very object-oriented fashion, and have hidden the details of each part of your app behind an object contract, you still have tighter coupling that would be necessary if you were writing a desktop application. Sticking with the example above: your "message list" object might not actually update the HTML in your mailbox list or message display panes, but it still has to know which objects to call to cause them to be updated, and which methods to call in those objects.

Can you really say that your objects are "self contained" if they have to call into other (essentially unrelated) items every time their states change?

There is a better way.

The Goal: A Nearly Complete Decoupling of Your Objects

The solution is to think about your client-sde application at a higher level. What are the major parts of your application, the controllers and models where most of the real work is done? What messages do they send?

People In a Room

Not clear? Pretend for a minute that your objects are unable to talk to each other directly, that they aren't allowed to know about each other at all. All they can do is broadcast a message, and anyone who is listening can act on it or not.

It's like a room full of people, working together. You each have specific tasks, but your tasks are related. When person A completes task A1, nine other people in the room want to do something as a result. When A completes task A2, though, only one other person in the room cares. The same goes for all of the other people in the room and all of their tasks.

These people are the objects in your application.

Consider the three-paned email interface again. Here we have at least three "people," one for each pane (there are probably a lot more 'people', but let's keep it simple). When a message is selected, the message-list pane yells into the room, "Hey! Message xxxx-yyy-z123 was just selected for reading!" and then goes on about its business. Two other "people" in the room (message-display-pane and mailbox-list-pane) hear that message and act on it. Nobody else in the room is listening for that type of message, so they don't do anything.

That's how events work (but without all the YELLING!). Rather than using and maintaining the contracts of all of the other objects in the app, each object only needs the following:

  • what objects are available and what sort of events and data they 'post'
  • its own set of messages to 'post' for any objects who care to listen

That first point sounds a little like we're back to square one, but there's an important difference. Rather than knowing how to deal with the interface of other objects, your object only needs to know that the object exists, and that it publishes events your object cares about.

Each object which consumes events only needs to know about the event producers that post the events it consumes. (And, I'll show you how to eliminate even that requirement.)

Even better: registering to receive those events is always the same, no matter which object you "listen to."

Still not convinced? In the next section we'll look at how simple it is to set it all up, and then we'll compare that with the more traditional, more tightly-coupled model that's common today.

Pseudo Custom Events

Reminder

As mentioned earlier, the W3C spec for creating custom events has been ignored (or worse) by most of the browser developers, so it doesn't do us any good. We need to simulate custom events in a way which works "cross-browser", and we don't want to waste a lot of time reinventing the wheel.

What interests us about events?

  • There are publishers, or broadcasters, of events.
    (e.g., Forms broadcast 'submit' events, and Fields broadcast 'change'
  • Event broadcasters all have a common, simple interface for "subscribing" to the events they publish.
    (In IE, it's attachEvent(), and in everything else it's addEventListener)
  • There are different types of events
  • Events (which are objects) carry data which is specified by the publisher and can be read by the listener

Implementation

The above description of events applies to all of the browsers for which we would ever try to write a javascript-driven application. "All" is good. We like all. There are differences (of course) between the browsers in how they allow you to trigger events (suck as clicks and scrolls), but that list of event properties is universal. So if we can hook into the existing event system in all of the browsers, we automatically "get it all."

All, that is, except for truly custom events. We still need to fake it, just a little. The goal, though, is to hide that fact and just make it work like we'd expect.

The trick to making it all work is surprisingly simple. There's one particular event which is guaranteed to be supported by every browser which supports javascript. It's probably the oldest "DOM Level 0" event, and it's often the first little trick we learn when we're exploring javascript or reading a book. What is it?

It's the "click" (or "onclick") event of the html <a> tag.

All About the Click

We've all seen the classic: <a href="#" onclick="alert( 'Hello, World!' )">Say Hello</a>.

That's where we started, years ago, but with event subscribers in modern javascript, we can do the same thing in a more flexible way. Consider this example:

<html> <head> <title>Click Demo</title> <script type="text/javascript"> function addEventListener( element, event_name, observer, capturing ) { if ( element.addEventListener ) // the DOM2, W3C way element.addEventListener( event_name, observer, capturing ); else if ( element.attachEvent ) // the IE way element.attachEvent( "on" + event_name, observer ); } function init() { var link = document.links[ 0 ]; addEventListener( link, "click", listener1, false ); addEventListener( link, "click", listener2, false ); } function listener1( evt ) { alert( "Hello from listener1" ); } function listener2( evt ) { alert( "Hello from listener2" ); } </script> </head> <body onload="init()"> <a href="#" onclick="void(0)">Hello, World!</a> </body> </html>

Update: some trouble was reported with this sample, I think it was just a problem with how it copies-and-pastes. Here's the source HTML file.

With this new way, we have one link but multiple, uncoupled listeners, all waiting eagerly for the "click" message.

Beyond Click. Sort of.

Again, we want custom events but we know we can't truly have them. To simulate them, we'll create new <a> tags in the document, one for each event. The <a> tags will be created and 'tracked' by the event publishers. Event listeners will think they're subscribing to our object's custom events, but in reality they're subscribing to the "click" events of <a> tags.

Better yet, we don't need to put any content (like text) into these <a> tags when we add them to the page, so they remain completely invisible and un-clickable by the user. Even screen readers will ignore them.

The event publishers create these event 'targets' (the A elements) once, and keep a reference to them in a local hash keyed off of the event names.

There are only three times that a publisher needs to retrieve a reference to one of these targets: when another object tries to (1) subscribe to or (2) unsubscribe from one of your object's events, and when your object wants to (3) publish an event. In all three cases, the reference to the target can be acquired in the same way:

  • If the specified event name has already been associated with a target <a> tag, return the reference to that <a> tag
  • Otherwise, create a new a tag, add it to your object's record of event<-->target associations, and return the reference to the <a> tag.

The operations of an event publisher are already specified by the browsers for us: they allow subscribers to subscribe and unsubscribe, and they publish events. We don't even need to design the API for these things, we just need to do what the browsers already do, and pass the calls to our (secret) event targets!

Sending Events

There are two possible approaches to posting events. Which you use depends on just how "decoupled" you want to be. In reality, total decoupling of objects which actually need to share messages and data is very difficult, but we can come close.

Partial Decoupling: Let the Listeners Decide

In the simpler approach, the listener objects need to be aware of all of the publishers which post events of interest. Compared with the "old way", this is still a huge improvement. Event publishers become "servers," and consumers become anonymous "clients." Your publisher objects only need to 'think about' their own data.

The listeners register with the publishers by calling the publisher's addEventListener method. The publisher (as described above) actually passes the call on to the 'secret' <a> tag.

All of your event publishers will have a single method for posting events (in the reference implementation, I've called it dispatchEvent). Whenever it wants to post an event, it will call that method with (at least) two parameters:

  • the name of the event to be posted
  • the data to be included with the event

dispatchEvent() then creates a 'click' event, and extends it with an 'event_data' object. This object contains the event name, a reference to the event source, and a 'data' parameter to contain the data provided to the dispatchEvent method. The event which is actually passed to the listeners looks something like this:

event = {
    ...,
    event_data: {
        event_name: event_name,
        event_target: this,
        data: anything publisher wants to pass to listeners
    }
}

Again, this approach is cleaner than the current (common) approach because objects only need to think about their own data, and their own input sources. This is often enough of an improvement to be good enough. Just as databases don't care how clients use the results of SQL queries, our event publishers can concentrate on what's in their own domain and let all of the listeners take care of themselves.

How would this work in our earlier example of a three-pane email interface?

There are many possible events in a complex application, so let's just consider what happens when the user selects a message in the message list.

The mailbox-list pane and the message-display pane would both subscribe (during application initialization) to the message-list pane's "message-selected-for-reading" event. When a message is selected (either by clicking on it, or navigating to it with the keyboard), this event would be dispatched with a reference to the message object represented by the item in the list.

  • If the message was unread, then the mailbox-list pane would find the appropriate mailbox and decrement it's display of the number of unread messages. If the message was already read, nothing happens.
  • The message-display pane displays the message attached to the event.
  • After the event has fired, the message-list pane marks the selected message as 'read' (if it was previously unread).
  • All the message list pane had to do was post an event and then act on its own data and user-interface objects.

We can take this decoupling one step further if we really want to.

Decoupling Level II: Ignorance is Bliss

The previous method released the publishers from the listeners: the publisher does it's job whether or not anybody is listening, but the listeners have to know about the publishers in advance.

However, we can take it one step further. Our listeners each care about a specific set of event messages, and our publishers only publish a specific set of event messages. If we create a central registry — a subscription broker — then the listeners don't need to know about the publishers in advance. This could be accomplished in a couple of ways:

  • The listeners ask the broker for references to all of the publishers which publish event x-y-z, and then manually register with each of those publishers. This sounds easy enough at first, but is difficult because the publishers all need to be registered before the listeners start listening.
  • The listeners register with the broker itself for their events of interest, as though the broker was the publisher.
    • If any objects have already registered as publishers of that event, the listener is automatically registered with the publisher
    • The broker keeps track of the listeners that are listening for each type of event
    • When a new publisher registers an event with the broker, the broker automatically registers the 'interested' listeners.

Many apps can keep their objects clean and self-contained (enough) that they don't need this "Level II Decoupling", but it's a useful tool to have in the toolbox. In some applications there could be events that come from just about anywhere.

User preferences are often set in a dedicated "prefs window," which would make it a candidate for "Partial Decoupling" (so other objects could be notified of changes to the prefs they use). However, some interfaces will also adjust the user's preference based on how the app is used.

One example: Google's GMail has a "rich text editor" that allows the user to see the bold, italics, links, etc., as they type. This editor can be activated and deactivated with a single click. Once activated, it remains active for all subsequent messages until it's deactivated again. Clearly, this is a preference being set from a location other than their "Preferences" UI. This alone does not necessarily make it a good candidate for the type of decoupling being described here, but it is the type of situation that should make you (the developer) start thinking about it.

Another example: if your application will interact with third-party objects, you definitely have a candidate for this type of decoupling because you CAN'T know about the other objects ahead of time. Looking at the email app again: a third party might supply a little widget which lists the total number of unread messages in all of the mailboxes. By registering with the event broker for the events 'message-selected-for-read', 'new-message-arrived', and 'messages-deleted' (and perhaps even 'mailbox-deleted'), this little plug-in module can keep it's display up to date as if it was a built-in part of your application.

Tracing and Debugging Events

My reference code, in the next section, includes support for a debugging mechanism which I've called the 'tracer'. When active, all events and event data will be automatically logged to a mini "event console" on the page. It's a very useful way to debug the events (though not nearly as good as using Venkman or even the Microsoft Script Debugger).

This has the unfortunate side-effect of making the code more than twice as long. Therefore, I've included a second copy of the code that omits the 'tracing' features completely.

Just the Facts, Maam #

  • Custom events are not "click" and "scroll" and "changed", but are the truly custom events you define for your own application. Exmaples: message_selected, new_messages_arrived, preference_changed, and editing_cancelled.
  • Custom events allow us to decouple the major objects in our code, keeping them self-contained.
  • There's a W3C DOM spec for custom events, but it doesn't do us any good because support for it is very uncommon.
  • The key to implementing custom events — without recreating the entire event subsystem from scratch — is to built it on top of the "click" event.
  • Event publishers create empty <a> tags, one for each event they support, as needed.
  • When a listener subscribes to a publisher's custom event (by name), it's actually subscribing to the invisible/empty <a> tag's 'click' event, but it never needs to know that.
  • Data — any data — can be passed to the listeners as part of the event object.

Reference Code

Notes

Live Demo

Mix-ins and Prototype

My reference implementation for custom events depends on Prototype (from Sam Stephenson), and JavaScript's ability to extend objects with new features (properties, functions) at runtime.

The custom-events concept itself does not require prototype at all. Once you understand the concept, duplicating it without the use of prototype.js would be relatively simple.

Prototype is used in a few ways:

  • the Event object is extended with Event.Listener, which is a "mix-in" for turning any object into a listener
  • the Event object is extended with Event.Publisher, which is a "mix-in" for turning any object into an event publisher
  • the Event object gains the methods .create() for creating a custom event, and .dispatch() for sending the event to the registered listeners.
  • the method Event.observe() is used as a browser-agnostic wrapper for element.addEventListener and element.attachEvent
  • the method Object.extend( destination_obj, source_obj ) is used to add Event.Listener and Event.Publisher to your objects

Frames and Windows

This implementation works with multiple frames and windows. This means that you can have an object in one window or frame listening for custom events in another window or frame. This does add the slight overhead complexity of gaining a reference to the publishers in the other window/frame. Navigating to a new page (in either frame or window) will break all of your "listens", but will not usually generate errors (either the remaining publishers have lost their listeners, or the remaining listeners no longer have publishers).

My first use of this code was in a browser-based RSS news aggregator (developed for Conversant) which used a parent window, an iFrame containing two frames, and an external "widget" in the parent window. The headache of trying to keep everything coordinated is what led to this implementation in the first place. So, it's been multi-frame friendly from the very beginning. I just can't take credit for that: the browsers do all the hard work.

Source Code

  • Event Mixins: Event.Publisher, Event.Listener, and Event
  • Event.Tracer: For debugging / tracing / logging your events
  • Event Broker: First pass at an implementation of Event.Broker, as described in the text above.
  • Event Play: The code that runs the useless demo. Only worth looking at as a demonstration of how to use the code.

Event.Publisher

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/** * Description: * add support (to any object or class) by mixing this class into your own * * Requires prototype.js * * Usage: * To publish custom events: * 1. mix this class with your own via * Object.extend( [your class or prototype], Event.Publisher ) * 2. post events by calling * this.dispatchEvent( [event name], [data for event] ) * * To activate and deactivate the event-tracing feature, just call * this.toggleEventsTrace() **/ Event.Publisher = Class.create(); Object.extend( Event.Publisher, { _ls_event_targets: null, _event_source_id: null, _fl_trace_events: false, getEventSourceId: function() { if ( typeof this._event_source_id == 'function' ) return this._event_source_id(); else return this._event_source_id; }, getEventTarget: function( event_name ) { if ( ! this._ls_event_targets ) this._ls_event_targets = new Array(); if ( ! this._ls_event_targets[ event_name ] ) document.body.appendChild( this._ls_event_targets[ event_name ] = document.createElement( 'A' ) ); return this._ls_event_targets[ event_name ]; }, addEventListener: function( event_name, callback_func, capturing ) { var targ = this.getEventTarget( event_name ); Event.observe( targ, 'click', callback_func, capturing ); if ( this._fl_trace_events ) { var data = { publisher: this.getEventSourceId(), event_name: event_name, listener: callback_func, capturing: capturing, event_source_proxy: targ }; this.dispatchEvent( 'eventListenerAdded', data, true, true ); } }, removeEventListener: function( event_name, callback_func, capturing ) { var targ = this.getEventTarget( event_name ); Event.stopObserving( targ, 'click', callback_func, capturing ); if ( this._fl_trace_events ) { var data = { publisher: this.getEventSourceId(), event_name: event_name, listener: callback_func, capturing: capturing, event_source_proxy: targ }; this.dispatchEvent( 'eventListenerRemoved', data, true, true ); } }, dispatchEvent: function( event_name, data, can_bubble, cancelable ) { var targ = this.getEventTarget( event_name ); var event_data = { event_name: event_name, event_target: this, data: data ? data : null }; if ( ! can_bubble ) can_bubble = false; if ( ! cancelable ) cancelable = false; var event = Event.create( targ, event_data, can_bubble, cancelable, true ); if ( this._fl_trace_events ) { if ( event_name.match( /event(?:ListenerAdded|ListenerRemoved|Dispatched|Received)/ ) ) return; var data = { publisher: this.getEventSourceId(), event_name: event_name, event_data: event_data, can_bubble: can_bubble, cancelable: cancelable, event_source_proxy: targ, result: event }; this.dispatchEvent( 'eventDispatched', data, true, true ); } }, toggleEventsTrace: function() { var trace = Event.Tracer.findTracer(); if ( ! trace || ! this._fl_trace_events ) { this._fl_trace_events = true; trace = Event.Tracer.startTrace(); trace.registerPublisher( this ); } else { this._fl_trace_events = false; if ( trace ) trace.unregisterPublisher( this ); } return this._fl_trace_events; }, isEventsTraceActive: function() { return this._fl_trace_events; } } );

Event.Listener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/** * MIX IN: Event.Listener * Description: * easily add support for receiving totally custom events * (to any object or class) by mixing this class into * your own * Usage: * To receive custom events: * 1. mix this class with your own via * Object.extend( [your class or prototype], EventListener ) * 2. listen for events by calling (from your object) * this.listen() * (see params for this.listen, below) **/ Event.Listener = Class.create(); Object.extend( Event.Listener, { _listens: new Array(), getEventHandlerName: function( event_name ) { var onEvent_name = event_name.split( /[ _]/ ).join( '-' ).camelize(); return "on" + onEvent_name.charAt( 0 ).toUpperCase() + onEvent_name.substr( 1 ); }, /** * Params: * event_source [object]: * the object which will generate the events, and which implements (or * mixes in) the Event.Publisher interface (we need addEventListener) * event_name [string]: * the name of the event for which your object will listen * use_capture [boolean]: * standard DOM Event API param * onEvent_name [string]: * the name of the method in your object which will be called when the * event is received if you omit this param, listen will look for a * function named with the CapitalizedCamelCased name of the event with * "on" at the front. So, if the event is named "message_received", * we'll look for a function named "onMessageReceived" You can override * this behavior by overriding getEventHandlerName in your object. **/ listenForEvent: function( event_source, event_name, use_capture, onEvent_name ) { if ( ! onEvent_name ) onEvent_name = this.getEventHandlerName( event_name ); var cb = this[ onEvent_name ].bindAsEventListener( this ); this._listens.push( [ event_source, event_name, use_capture, onEvent_name, cb ] ) event_source.addEventListener( event_name, cb, use_capture ); }, stopListeningForEvent: function( event_source, event_name, use_capture, onEvent_name ) { if ( ! onEvent_name ) onEvent_name = this.getEventHandlerName( event_name ); var ix_item; var ls = this._listens.detect( function( val, ix ) { if ( ( val[ 0 ] == event_source ) && ( val[ 1 ] == event_name ) && ( val[ 2 ] == use_capture ) && ( val[ 3 ] == onEvent_name ) ) { ix_item = ix; return true; } } ); if ( ix_item ) { this._listens.splice( ix_item, 1 ); event_source.removeEventListener( event_name, ls[ 4 ], use_capture ); return true; } return false; } } );

Event

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/** * Extensions to Prototype's Event object, * for cleanly creating and dispatching custom events * * Called from Event.Publisher **/ Object.extend( Event, { create: function( target, event_data, can_bubble, cancelable, fl_dispatch ) { var event; if ( document.createEvent ) // gecko, safari { if ( ! can_bubble ) can_bubble = false; if ( ! cancelable ) cancelable = false; if ( /Konqueror|Safari|KHTML/.test( navigator.userAgent ) ) { event = document.createEvent( 'HTMLEvents' ) event.initEvent( 'click', can_bubble, cancelable ); } else // gecko uses MouseEvents { event = document.createEvent( 'MouseEvents' ) event.initMouseEvent( "click", can_bubble, cancelable, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null ); } } else // msie { event = document.createEventObject(); } event.event_data = event_data; if ( fl_dispatch ) Event.dispatch( target, event ); return event; }, dispatch: function( target, event ) { if ( document.createEvent ) return target.dispatchEvent( event ); else return target.fireEvent( 'onclick', event ); } } );

Event.Tracer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
/** * Event Tracer (singleton class) * (c) 2006 Seth Dillingham <seth.dillingham@gmail.com> * * This software is hereby released into the public domain. Do with it as * you please, but with the understanding that it is provided "AS IS" and * without any warranty of any kind. * * (But I'd love to be told about where and how this code is being used.) **/ /** * Singleton Object: Event.Tracer * * Description: * provides some minimal event debugging/tracing/logging * Event.Tracer is a sort of singleton * Only use Event.Tracer for starting and stopping the event tracing * service, and for acquiring a reference to the live tracing object * (which is returned by both find_tracer and start_trace) * * IMPORTANT * You only need to use this file when debugging your custom events. * * Requires: * prototype.js * the custom events services provided by event_mixins.js * * Usage: * Start up event tracing by calling Event.Tracer.startTrace( viewport ) * viewport is a div (or the id of a div) on your page where trace * output will be written * returns: a reference to an instance of Event.Tracer.prototype * (not the singleton) * Kill the tracer (disable tracing in all publishers) with: * Event.Tracer.stopTrace() * Note that you can start the tracer (if it's not already running) * and register your publishers, all in one step, with a call like: * [publisher].toggleEventsTrace() **/ Event.Tracer = Class.create(); Object.extend( Event.Tracer, { _tracer: null, findTracer: function() { return ( this._tracer != undefined ) ? this._tracer : false; }, startTrace: function( viewport ) { if ( ! this._tracer ) this._tracer = new Event.Tracer( viewport ); return this._tracer; }, stopTrace: function() { if ( this._tracer ) { this._tracer.shutdown(); delete this._tracer; this._tracer = null; return true; } return false; } } ); /** * Class Prototype: Event.Tracer.prototype * * Description: * acts as a "logger" for custom event publishers which * are registered with the tracer * * This code should generally be consider a black box: you shouldn't * ever need to call it from your own code, because it's called * automatically from any class which has been extended with * Event.Publisher (after calling .toggleEventsTrace() to enable * debugging on the publisher) * * Requires: * prototype.js * the custom events services provided by event_mixins.js * * Usage: * Generally, you can activate the tracer from your event publisher * by calling[publisher].toggleEventsTrace() **/ Object.extend( Event.Tracer.prototype, { _publishers: null, _viewport: null, _outputlist: null, initialize: function( viewport ) { this._publishers = {}; if ( viewport ) viewport = $( viewport ); if ( ! viewport ) { viewport = $( 'event_tracer_output' ); if ( ! viewport ) throw new Error( 'No view port specified, and none found.' ); } this._viewport = viewport; var ls = $( 'event_tracer_output_list' ); if ( ! ls ) { ls = document.createElement( 'UL' ); ls.id = 'event_tracer_output_list'; viewport.appendChild( ls ); } this._outputlist = ls; }, _shutdownPublisher: function( pair ) { this.unregisterPublisher( pair[ 1 ].publisher ); }, shutdown: function() { $H( this._publishers ).each( this._shutdownPublisher.bind( this ) ); var li = document.createElement( 'LI' ); li.innerHTML = '<b>Tracer shutdown.</b>'; this._outputlist.insertBefore( li, this._outputlist.firstChild ); }, unregisterPublisher: function( event_publisher ) { this.stopListeningForEvent( event_publisher, 'eventListenerAdded', true, 'onEventListenerAdded' ); this.stopListeningForEvent( event_publisher, 'eventListenerRemoved', true, 'onEventListenerRemoved' ); this.stopListeningForEvent( event_publisher, 'eventDispatched', true, 'onEventDispatched' ); delete this._publishers[ event_publisher.getEventSourceId() ]; }, registerPublisher: function( event_publisher ) { var id = event_publisher.getEventSourceId(); var publisher_data = { id: id, publisher: event_publisher }; this._publishers[ id ] = publisher_data; this.listenForEvent( event_publisher, 'eventListenerAdded', true, 'onEventListenerAdded' ); this.listenForEvent( event_publisher, 'eventListenerRemoved', true, 'onEventListenerRemoved' ); this.listenForEvent( event_publisher, 'eventDispatched', true, 'onEventDispatched' ); }, traceEvent: function( evt ) { var li = document.createElement( 'LI' ); var eventinfo = document.createElement( 'UL' ); li.innerHTML = '<b>Event:</b> ' + evt.event_data.event_name; this._outputlist.insertBefore( li, this._outputlist.firstChild ); li.appendChild( eventinfo ); this._outputlist = eventinfo; $H( evt.event_data ).each( this._prettyprintObject.bind( this ) ); this._outputlist = this._outputlist.parentNode.parentNode; }, onEventListenerAdded: function( evt ) { this.traceEvent( evt ); }, onEventListenerRemoved: function( evt ) { this.traceEvent( evt ); }, onEventDispatched: function( evt ) { evt = evt.event_data.data; var li = document.createElement( 'LI' ); var eventinfo = document.createElement( 'UL' ); li.innerHTML = '<b>Event Dispatched:</b> <span class="event_name">' + evt.event_data.event_name + '</span>'; this._outputlist.insertBefore( li, this._outputlist.firstChild ); li.appendChild( eventinfo ); this._outputlist = eventinfo; $H( evt.event_data ).each( this._prettyprintObject.bind( this ) ); this._outputlist = this._outputlist.parentNode.parentNode; }, _prettyprintObject: function( pair ) { var li = document.createElement( 'LI' ); li.innerHTML = '<span class="key">' + pair[ 0 ] + ':</span> '; if ( typeof pair[ 1 ] != "undefined" ) li.innerHTML += '<span class="value">' + pair[ 1 ].toString() + '</span>'; this._outputlist.appendChild( li ); switch ( pair[ 0 ] ) { case 'data': case 'event_data': var data = document.createElement( 'UL' ); li.appendChild( data ); this._outputlist = data; $H( pair[ 1 ] ).each( this._prettyprintObject.bind( this ) ); this._outputlist = this._outputlist.parentNode.parentNode; default: if ( pair[ 1 ] && pair[ 1 ].tagName ) li.innerHTML = '<span class="key">' + pair[ 0 ] + ':</span> <span class="value">' + pair[ 1 ].tagName + '</span>'; } } } ); Object.extend( Event.Tracer.prototype, Event.Listener );

Event Broker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
/** * Description: * Allows listeners to subscribe to event types, and remain * blissfully unaware of the available publishers of those events * * Requires prototype.js * * Usage: * You'll usually want to create a copy of the Broker object by mixing * it in with one of your own classes as follows: * Object.extend( your_obj, Event.Broker ) * * Publishers register the event types they send with a call to * your_broker_object.registerEventsPublisher * and "unregister" via your_broker_object.unregisterEventsPublisher * * Nothing changes for the event listeners, except they call their * listen() method once for each event type (passing a reference * to the broker) instead of once per event type per publisher **/ Event.Broker = Class.create(); Object.extend( Event.Broker, { _listeners: null, _publishers: null, _initListenerType: function( event_type ) { if ( this._listeners == null ) this._listeners = {}; if ( typeof( this._listeners[ event_type ] ) == "undefined" ) this._listeners[ event_type ] = new Array(); }, _initPublisherType: function( event_type ) { if ( this._publishers == null ) this._publishers = {}; if ( this._publishers[ event_type ] == undefined ) this._publishers[ event_type ] = new Array(); }, /** * Register a publisher with the broker. Listeners that want the types * of events produced by this publisher will be subscribed automatically. * * Params: * event_types: an event type, or an arry of event types, * which are published by the publsher * publisher: the publisher object being registered with the broker * (must mix in Event.Publisher, or implement the same public interface) **/ registerEventsPublisher: function( event_types, publisher ) { if ( typeof( event_types ) != typeof( [] ) ) event_types = [event_types]; event_types.each( function( event_type ) { this._initPublisherType( event_type ); this._publishers[ event_type ].push( publisher ); this._initListenerType( event_type ); this._listeners[ event_type ].each( function( listener_rec ) { publisher.addEventListener( event_type, listener_rec.listener, listener_rec.useCapture ); } ); }.bind( this ) ); }, /** * Unregister a publisher with the broker. Listeners that had been * automatically subscribed to the publisher will be un-subscribed. * * Params: * event_types: an event type, or an arry of event types, * which are published by the publsher * publisher: the publisher object being un-registered with the broker **/ unregisterEventsPublisher: function( event_types, publisher ) { if ( typeof( event_types ) != typeof( [] ) ) event_types = [event_types]; event_types.each( function( event_type ) { this._listeners[ event_type ].each( function( listener_rec ) { publisher.removeEventListener( event_type, listener_rec.listener, listener_rec.useCapture ); } ); var ix = this._publishers[ event_type ].indexOf( publisher ); if ( ix > -1 ) this._publishers[ event_type ].splice( ix, 1 ); } ); return; }, /** * Register a listener with the broker. Causes the listener * to be automatically registered with all publishers that produce * the specified event_type. * * You shouldn't have to call this from your own code: it's * called automatically when your listener listens for events * from the broker. * * See Event.Listener.listenForEvent **/ addEventListener: function( event_type, event_listener, useCapture ) { this._initListenerType( event_type ); this._listeners[ event_type ].push( { listener: event_listener, useCapture: useCapture } ); this._initPublisherType( event_type ); this._publishers[ event_type ].each( function( publisher ) { publisher.addEventListener( event_type, event_listener, useCapture ); } ); }, /** * Un-register a listener with the broker. The listener is * "unsubscribed" from all publishers of the given event_type * * You shouldn't have to call this from your own code: it's * called automatically when your listener stops listening * for events from the broker. * * See Event.Listener.stopListeningForEvent **/ removeEventListener: function( event_type, event_listener, useCapture ) { this._publishers[ event_type ].each( function( publisher ) { publisher.removeEventListener( event_type, event_listener, useCapture ); } ); var ix_listener = -1; this._listeners[ event_type ].each( function( listener_rec, ix ) { ix_listener = ix; throw $break; } ); if ( ix_listener > -1 ) this._listeners[ event_type ].splice( ix_listener, 1 ); return; }, /** * tracing/debugging feature only * * toggles event tracing on all of the publishers of a given type of event * * note: this is a first pass at this feature. Since it toggles * the trace feature, toggling event tracing on two different event_types * could activate trace for an object via the first event type, and then * deactivate it with the second event type **/ toggleEventPublishersTrace: function( event_type ) { this._initPublisherType( event_type ); this._publishers[ event_type ].each( function( publisher ) { publisher.toggleEventsTrace(); } ); } } );

Changes

  • June 30, 2006: Wrote and posted first version.

  • July 2, 2006: Added section “Just the Facts, Please

  • July 3, 2006:

    • Fixed a bug in the Event.Publisher mixin. _ls_event_publisher now starts as null instead of an empty array. Otherwise, all publishers share the same subscription set. (Which would be an interesting, alternative approach to Event.Broker!)
    • Added first implementation of Event.Broker
    • Minor tweaks to the text.
  • July 6, 2006

    • All of the public methods in all of the objects are now camelCased.
    • More comments in the code.
    • Second pass at Event.Broker. Fixed some bugs, added comments for each method, and added the method toggleEventPublishersTrace(). If debugging/tracing features are wanted in the broker, more work is still needed.
  • July 10, 2006: Fixed a typo, wen Peter Lewis pointed it out. ;-)

  • July 11, 2006

    • Shortened some of the text in the first half of the essay. Too long and boooriiinnnnnng.
    • Updated the sample code in "the click" section.
  • July 15, 2006

    • Bugs fixed. When using Event.Broker, "stopListeningForEvent" would sometimes unsubscribe the wrong listener. It was a weird bug, but easily fixed by, uh... passing the right parameters. Nevermind. It works now.
    • The "useless demo" code now has to pages: one which uses the broker, and one which does not. The two pages are cross-linked, so you can go back and forth to see the differences.

Page last updated: 2/13/2009



Until August 31
My Amazon sales
benefit the PMC

Homepage Links

TruerWords
is Seth Dillingham's
personal web site.
Truer words were never spoken.