TruerWords Logo
Google
 
Web www.truerwords.net

Search TruerWords

Welcome
Sign Up  Log On

Topic: Creating Custom Events with JavaScript: Decoupling

Messages: (19) 1


Author: Seth Dillingham

Date:6/30/2006

Permalink Icon

# 5568

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.

[Top]


Author: Seth Dillingham

Date:6/30/2006

Permalink Icon

# 5569

RE: Creating Custom Events with JavaScript: Decoupling

Hey everybody, sorry about the extreme size of that last message, and that so much of it was unreadable in email.

If you're interested in my technology-related articles (and I think this is a good one, personally), you should read it on the web site.

[Top]


Author: Seth Dillingham

Date:7/4/2006

Permalink Icon

# 5571

Updated the Custom Events Essay and Code

I've updated the custom events in javascript essay, and all of the reference code.

A couple of bugs in the code have been squished, the Event.Broker mix-in (that was described in the essay but not originally provided) is there now, and the essay text  has been slightly tweaked.

[Top]


Author: John Hermanson

Date:7/14/2006

Permalink Icon

# 5580

RE: Creating Custom Events with JavaScript: Decoupling

Very cool code. However, there appears to be a bug when toggling to 'stop listening'. The wrong widget will stop listening if more than one widget is listening and you toggle one widget to stop listening.

For example:

  1. Toggle 1 and 2 to listen for event 'A'.
  2. Click 'Post Event A'. Both widgets (1 and 2) respond.
  3. Toggle 2 to stop listening.
  4. Click 'Post Event A'. Widget 2 responds, widget 1 does not.

[Top]


Author: Seth Dillingham

Date:7/14/2006

Permalink Icon

# 5581

RE: Creating Custom Events with JavaScript: Decoupling

On 7/14/06, John Hermanson said:

> Very cool code. However, there appears to be a bug when toggling to 'stop
> listening'. The wrong widget will stop listening if more than one widget is
> listening and you toggle one widget to stop listening.
> For example:
> 
> Toggle 1 and 2 to listen for event 'A'.
> Click 'Post Event A'. Both widgets (1 and 2) respond.
> Toggle 2 to stop listening.
> Click 'Post Event A'. Widget 2 responds, widget 1 does not.

I've actually fixed this bug in a copy that I sent to a few people privately, but haven't updated the site yet. I'll take care of that in a few minutes, it's a very minor bug.

Could you tell me how you found this article? The page is getting a lot of traffic right now, but my referer logs aren't showing diddly.... I'm guessing that most people are coming in from a link in an RSS feed, so there is no referer.

Seth

[Top]


Author: John Hermanson

Date:7/14/2006

Permalink Icon

# 5582

Re: RE: Creating Custom Events with JavaScript: Decoupling

Google. Searched for something like 'custom javascript events' or something similar.

I have a fairly large chunk of html and javascript code that I am reusing for a new 'special case' scenario. The problem is that the existing script now needs to update an html tag that only exists in the new (slightly modified) html. Completely decoupling the code from either version of the html by providing a custom event would be a much more elegent solution than other available options (e.g. checking for the existence of the tag before updating it or a try/catch block).

One other comment/question: Is it possible for the form tag to be some other tag type? I work in ASPX and having form tags within the existing uber-form breaks the postback 'magic'.

[Top]


Author: Seth Dillingham

Date:7/14/2006

Permalink Icon

# 5583

RE: RE: Creating Custom Events with JavaScript: Decoupling

On 7/14/06, John Hermanson said:

> One other comment/question: Is it possible for the form tag to be some
> other tag type? I work in ASPX and having form tags within the existing
> uber-form breaks the postback 'magic'.

You don't need a form tag at all... I only used form tags to provide an easy way for the user to generate an event.

The flashing green time boxes pulse to the rhythm of a custom event (called "time") which is generated by a timer. No form tag used there at all.

*Anything* can generate an event. The key is the javascript code, not the HTML.

Events don't have to start with HTML, and they don't have to actually do anything to the HTML. As I said in the article, an event might not do anything more than notify the listeners that that some javascript object's state has changed.

Let me know if you have any more questions, or if I didn't really answer the question you already asked.

Seth

P.S. The bug you reported has been fixed, and the sample pages have been updated. There are now two versions, cross-linked (one uses the event broker, the other does not).

[Top]


Author: John Hermanson

Date:7/18/2006

Permalink Icon

# 5585

Re: RE: RE: Creating Custom Events with JavaScript: Decoupling

Ah, misinterpreted the code on my quick look through the other day. I had thought that the form in the "listener#" div was involved in the event code somehow.

Thanks again.

[Top]


Author: Seth Dillingham

Date:9/5/2006

Permalink Icon

# 5690

API Documentation for Custom Javascript Events

This page documents the core of the Custom Events code. Docs for the rest of the code (the tracer and broker) will be added as time permits.

Event.Publisher

Mix Event.Publisher into your class or object if it needs to publish custom events for Event.Listeners. Allows your objects to use the same event features as DOM elements, because it uses hidden DOM elements to implement the custom events.

Found in event_mixins.js (and the version without debugging, event_mixins_notrace.js).

.getEventTarget( event_name )
Private. Finds the secret <A> element that this publisher will use to publish the event event_name through the DOM.
.addEventListener( event_name, callback_func, capturing )
Adds the observer function callback_func for the event event_name (optionally capturing) to the current event publisher. Called by Event.Listeners.
.removeEventListener( event_name, callback_func, capturing )
Removes the observer function callback_func (where capturing must match) from the event publisher.
.dispatchEvent( event_name, data, can_bubble, cancelable )
Creates a new event_name event, attaches a custom object for the event data (see "passing data", below), and dispatches the event to the listeners via the browser's standard event distribution mechanisms. Generally called by the publisher itself.

Event.Listener

Mix Event.Listener into your class if it needs to act on the event notifications sent by any publishers.

Found in event_mixins.js (and the version without debugging, event_mixins_notrace.js).

.getEventHandlerName( event_name )

Private. If a call to listenForEvent or stopListeningForEvent omits the name of the listener function, getEventHandlerName is used to find the "correct" name of the listener function.

The "correct" name of the handler function is determined by upper-casing the first character of the event name, replacing all /[ _]/ with -, camelizing the result, and then prepending "on". So if the event's name is "i just did_something", this function would return "onIJustDidSomething", and the listener code would then assume that was the name of the listener function.

.listenForEvent( event_source, event_name, use_capture, onEvent_name )
Causes the listener to "subscribe" for events of type event_name which are published by event_source, assuming that event_source is an Event.Publisher.
.stopListeningForEvent( event_source, event_name, use_capture, onEvent_name )
Unsubscribe the current listener from the events of type event_name published by event_source.

Event (two new methods)

Adds the methods create and dispatch to Prototype's Event object.

Found in event_mixins.js (and the version without debugging, event_mixins_notrace.js).

.create( event_data, can_bubble, cancelable, fl_dispatch, target )

Browser-agnostic method for creating new click events.

Could/should be generalized for creating events of any type, but this will be difficult because the browsers all do this so very differently (createEvent versus createEventObject, and HTMLEvents versus MouseEvents...). For now, this method is only usable for click events.

event_data is any data the Publisher wants attached to the event for use by the Listeners, can_bubble and cancelable are straight out of the DOM event api, fl_dispatch is a boolean which (if true) causes the event to be dispatched to target as soon as it is created, and target is the DOM element which is to receive the event (as in, "pretend I clicked on [target]"). target is only used if fl_dispatch is true.

.dispatch( target, event )
Sends the event to the specified target DOM object. This is just a browser-agnostic wrapper for target.dispatchEvent (from the DOM) and target.fireEvent (from MSIE).

Event.Broker

The broker allows your publishers and listeners (subscribers) to remain unaware of each other. Publishers register their events (by name) with the broker. Listeners subscribe only to the broker, rather than to the publishers (the broker takes care of subscribing the listeners to the publishers, automatically).

Use of Event.Broker changes the events model so that listeners just need to know what events they care to receive, and don't need to know anything about which objects on the page will actually produce those events.

Event.Broker is especially useful in three situations:

  1. when you have a 'pluggable' interface (meaning Listeners may not know in advance about all of the available Publishers).
  2. when there are numerous (or even "many") publishers that can produce the same events
  3. when the available event publishers change regularly (for example, if you have a list of items, and the list is updated in response to changes on the server side)

In all of those cases, the benefit of the broker is that the listeners only need to know about the one, central Event.Broker object.

.registerEventsPublisher( event_types, publisher )

Public. Called by a Publisher, usually when it is first created/initialized.

event_types is an array of strings, where each string is the name (type) of one event produced by this publisher. publisher is the Publisher object which is being registered (usually 'this').

window.eventBroker.registerEventsPublisher( ['new msg arrived', 'msg deleted'], this )

Any Listeners which have already registered with the Broker for any of the events in event_types will be automatically subscribed to this publisher, immediately. New Listeners (for events of the types produced by this Publisher) will be subscribed to this publisher as soon as they register with the Broker.

.unregisterEventsPublisher( event_types, publisher )

Public. Called by a Publisher, usually when the Publisher object is about to be destroyed.

This is the opposite of .registerEventsPublisher().

event_types is an array of strings, where each string is the name (type) of one event produced by this publisher. publisher is the Publisher object which is being un-registered with the Broker (usually 'this').

All of the Listeners which have subscribed to this Publisher's events will be automatically unsubscribed by the Broker, and then the Broker will remove the Publisher from its internal lists of Publishers so that it will no longer receive any new subscribers.

addEventListener( event_type, event_listener, useCapture )

Public. Called by a Listener, usually when it is first created/initialized. However, you do not call this method directly. It's called by the Event.Listener interface. (See example, below.)

event_type is the name/type of the event that the Listener wants to receive. event_listener is the Listener object itself (usually 'this'). useCapture is the DOM-standard parameter to indicate at which phase the event should be sent to the Listener.

After registering with the Broker, the Broker will make sure that the Listener is subscribed to all Publishers which produce the events for which the Listener has subscribed. (This includes Publishers which which register before or after the Listener registered.)

this.listenForEvent( window.eventBroker, 'new msg arrived', false );

this.listenForEvent( window.eventBroker, 'msg deleted', false, 'onMsgDeleted' );
removeEventListener( event_type, event_listener, useCapture )

Public. Called by a Listener when it is no longer interested in receiving a given type of event (such as when it is being destroyed!) You do not call this method directly. It's called by the Event.Listener interface. (See example, below.)

event_type is the name/type of event that the Listener is no longer interested in receiving. event_listener is a reference to the Listener instance itself (usually 'this'). useCapture is the DOM-standard parameter that indicated at which phase the event was to be sent to the Listener: the boolean value of this param must match the original call to addEventListener.

Unregistering with the broker will cause the Listener to be unsubscribed from all Publishers of events of type event_type.

this.stopListeningForEvent( window.eventBroker, 'new msg arrived', false );

this.stopListeningForEvent( window.eventBroker, 'msg deleted', false, 'onMsgDeleted' );

Passing Data with Custom Events

Calls to an event publisher's .dispatchEvent() method may include an optional data param, which can be of any type. This data (or null if not provided) can be accessed by the listeners via event.event_data.data.

The event_data object that is attached to every custom event includes three properties:

  • event_name: the name (or type) of the custom event
  • event_target: the Event.Publisher which generated the event
  • data: the data param which was included when the publisher's .dispatchEvent() method was called

[Top]


Author: Jenny Walsh

Date:9/6/2006

Permalink Icon

# 5694

Bug in unregisterEventsPublisher

I think you forgot to put a .bind(this) in the unregisterEventsPublisher method of the Broker on line 108. I had to add that to make it work. I'm using your code in a project I'm working on and it's a big help, thanks!

[Top]


Author: Seth Dillingham

Date:9/6/2006

Permalink Icon

# 5695

RE: Bug in unregisterEventsPublisher

Jenny,

Thank you for the bug report! My own use of the code doesn't actually include the unregister command... I included it for completeness.

Your bug report was right, and the code on the server has been updated. I won't have a chance to update the web page until tomorrow.

Thanks again!

Seth

[Top]


Author: Andrew Revinsky

Date:1/15/2007

Permalink Icon

# 5819

RE: Creating Custom Events with JavaScript: Decoupling

Great for the substance and all.

But just how, referencing and extending Prototype, the author has left behind the coding conventions (esp. naming conventions) that Prototype uses? All those this_is_another_identifier things that looks absolutely out of context?

[Top]


Author: Seth Dillingham

Date:1/15/2007

Permalink Icon

# 5820

RE: Creating Custom Events with JavaScript: Decoupling

On 1/15/2007, Andrew Revinsky said:

>Great for the substance and all.

Thank you.

The Author :-)

[Top]


Author: Mike Czepiel

Date:3/8/2007

Permalink Icon

# 5864

Re: Creating Custom Events with JavaScript: Decoupling

Not sure what your preferred method of gathering feedback is, but I have a patch I'm using on event_mixins.js I figured you may find useful.

The diff is as follows:

198c198,205
<               var cb = this[ onEvent_name ].bindAsEventListener( this );
---
>               //added this in to allow for anonymous function handling of an event
>               var eventHandler = this[onEvent_name];
> 
>               if(typeof(onEvent_name) == 'function') {
>                       eventHandler = onEvent_name;
>               }
> 
>               var cb = eventHandler.bindAsEventListener( this );

Basically I didn't like having to create function following any particular naming scheme, it felt a little coincidental. Additionally I'll often handle the event in an anonymous function which will call the correct instance method. Ideally the instance methods don't even know they're being called due to an event having occurred.

I'm using the code on a pretty high profile site at the moment without any glaring issues with this patch, but I'm sure there's room for improvement.

Thanks for the the great work you've done.

[Top]


Author: omar

Date:2/12/2009

Permalink Icon

# 6287

RE: Creating Custom Events with JavaScript: Decoupling

Nice article, I haven't finished it yet but I tried the first sample code u put,the one in the "all about the click" section,it just doesn't work, not on IE nor Firefox, when I click on the link nothing happens, any ideas why ?

[Top]


Author: Seth Dillingham

Date:2/13/2009

Permalink Icon

# 6288

RE: Creating Custom Events with JavaScript: Decoupling

On 2/12/2009, omar said:

>Nice article, I haven't finished it yet but I tried the first sample code u
>put,the one in the "all about the click" section,it just doesn't work, not on
>IE nor Firefox, when I click on the link nothing happens, any ideas why?

I just tested it, and it worked fine for me.

However, there is a problem with the copy-and-paste: it leaves out all of the line breaks! That may have been your problem: there are a couple of javascript comments in there that need to be terminated with line breaks.

The article has been updated with a link to download that sample file.

Seth

[Top]


Author: DichlofoS

Date:3/23/2009

Permalink Icon

# 6297

RE: Creating Custom Events with JavaScript: Decoupling

Seems that ';' is missing in "Event" section, lines 20 and 26.
Btw, thanks for this brilliant article.

[Top]


Author: John Cotter

Date:4/13/2009

Permalink Icon

# 6299

RE: Creating Custom Events with JavaScript: Decoupling

Hey Seth,

This is a GREAT article. This hits on all the points that I am dealing with at the moment and takes my thoughts and solidify's them. I've noticed that the convention used for the class creation seems to have issues, I am assuming because of post 1.6 updates to Prototype. Are you planning on updating the code base to work with it? I am at the moment going through doing an update on the code you have so I can use it in a project that I am working on. This is great stuff Seth, thanks for putting it out there for people!


John

[Top]


Author: Seth Dillingham

Date:4/13/2009

Permalink Icon

# 6300

RE: Creating Custom Events with JavaScript: Decoupling

On 4/13/2009, John Cotter said:

>This is a GREAT article. This hits on all the points that I am dealing
>with at the moment and takes my thoughts and solidify's them.

Thank you and I'm glad you like it.

>I've noticed that the convention used for the class creation seems to
>have issues, I am assuming because of post 1.6 updates to Prototype.
>Are you planning on updating the code base to work with it?

Actually the version here on the site isn't what I use anymore.

Last year Apple (who I'm happy to say uses the custom events stuff quite extensively) had a concern, and I combined that with some stuff I'd been thinking about and produced a much cleaner new version.

Unfortunately, I never got around to releasing it. Ahem.

Maybe I should do that, huh?

Perhaps I'll work on it tomorrow. Would be great if it could generate a little work. ;-)

>I am at the moment going through doing an update on the code you have
>so I can use it in a project that I am working on. This is great stuff
>Seth, thanks for putting it out there for people!

Again, I'm glad you like it. Be sure to check back for the update, which should address any compatibility issues you've run into (and more).

Seth

[Top]



<- Previous Thread:
RailsConf 2006: Highs and Lows

Next Thread: ->
The Wizard Schools

Until August 31
My Amazon sales
benefit the PMC

Homepage Links

Apr 1 - Aug 31
Ad revenue
benefits the PMC


TruerWords
is Seth Dillingham's
personal web site.
Read'em and weep, baby.