|
|
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.
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.
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:
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 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?
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:
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.
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.
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.
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.
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:
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!
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.
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:
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.
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.
We can take this decoupling one step further if we really want to.
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:
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.
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.
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:
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.
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; } } ); |
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; } } ); |
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 ); } } ); |
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 ); |
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(); } ); } } ); |
Page last updated: 2/13/2009
TruerWords
is Seth Dillingham's personal web site. Read'em and weep, baby. |