Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at the New York ColdFusion User Group (May. 2008) with: Michael Smith

Powering Publish And Subscribe Functionality With Native jQuery Event Management

By Ben Nadel on

Let's face it - jQuery's native event management is straight-up awesome! Between multi-type binding, name-spaced events, proxied callbacks, and function-specific unbinding, there's not much that it can't do (and do well, for that matter). Without a doubt, it's better than any event management that I could write. As such, I wondered if I could use the native jQuery event management to power publish and subscribe (pub/sub) functionality within my Javascript applications.

 
 
 
 
 
 
 
 
 
 

While you can use jQuery to bind and trigger events on non-DOM-node Javascript objects, it's not yet clear as to whether or not this functionality is officially supported by the jQuery project. In my experience, there's also too much node-specific logic being used to make this a reliable approach. But, what if we used actual DOM nodes to power eventing within the domain model?

Last week, I confirmed that jQuery could bind and trigger events on detached DOM nodes. This means that we could theoretically use detached DOM nodes as event proxies within our domain objects. In other words, our domain objects could present a bind/unbind/trigger API that actually wrapped around an underlying, detached DOM node.

To explore this concept, I've defined the following Javascript class, Girl. The Girl's prototype (class methods) contains methods for bind(), unbind(), and trigger(). When you look at the logic of these methods, however, you'll notice that they simply pass execution off to an encapsulated, custom DOM node, "beacon."

  • <!DOCTYPE html>
  • <html>
  • <head>
  • <title>Creating Publish/Subscribe With jQuery Event Bindings</title>
  • <script type="text/javascript" src="./jquery-1.4.2.js"></script>
  • <script type="text/javascript">
  •  
  •  
  • // Define the girl class.
  • function Girl(name){
  • this.name = name;
  •  
  • // Internally, we are going to keep a free-standing DOM
  • // node to power our publish / subscribe event mechanism
  • // using jQuery's bind/trigger functionality.
  • //
  • // NOTE: We are using a custom node type here so that we
  • // don't have any unexpected event behavior based on the
  • // node type.
  • this.eventBeacon = $(
  • document.createElement( "beacon" )
  • );
  •  
  • // Create a help collection of bound event types.
  • this.eventBeacon.data( "preTrigger", {} );
  • }
  •  
  •  
  • // Define proxy bind method.
  • Girl.prototype.bind = function( eventType, callback ){
  • // Check to see this event type has a pre-trigger
  • // interceptor yet. Since event handlers are triggered
  • // in the order in which they were bound, we can be sure
  • // that our preTrigger goes first.
  • if (!this.eventBeacon.data( "preTrigger" )[ eventType ]){
  •  
  • // We need to bind the pre-trigger first so it can
  • // change the target appropriatly before any other
  • // event handlers get triggered.
  • this.eventBeacon.bind(
  • eventType,
  • jQuery.proxy( this.preTrigger, this )
  • );
  •  
  • // Keep track fo the event type so we don't re-bind
  • // this prehandler.
  • this.eventBeacon.data( "preTrigger" )[ eventType ] = true;
  •  
  • }
  •  
  • // Replace the callback function with a proxied callback
  • // that will execute in the context of this Girl object.
  • arguments[ arguments.length - 1 ] = jQuery.proxy(
  • arguments[ arguments.length - 1 ],
  • this
  • );
  •  
  • // Now, when passing the execution off to bind(), we will
  • // apply the arguments; this way, we can use the optional
  • // data argument if it is provided.
  • jQuery.fn.bind.apply( this.eventBeacon, arguments );
  •  
  • // Return this object reference for method chaining.
  • return( this );
  • };
  •  
  •  
  • // Define proxy unbind method.
  • Girl.prototype.unbind = function( eventType, callback ){
  • // Pass the unbind() request onto the event beacon.
  • this.eventBeacon.unbind( eventType, callback );
  •  
  • // Return this object reference for method chaining.
  • return( this );
  • };
  •  
  •  
  • // Define proxy trigger method.
  • Girl.prototype.trigger = function( eventType, data ){
  • // Pass the trigger() request onto the event beacon.
  • this.eventBeacon.trigger( eventType, data );
  •  
  • // Return this object reference for method chaining.
  • return( this );
  • };
  •  
  •  
  • // I am the first handler to be bound on this object for any
  • // event-type. I change the Target property to be the Girl
  • // instance, not the DOM node.
  • Girl.prototype.preTrigger = function( event ){
  • // Mutate the event to point to the right target.
  • event.target = this;
  • };
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Create a new girl instance.
  • var tricia = new Girl( "Tricia" );
  •  
  •  
  • // Define some function references which we can use later
  • // when we unbind the event handlers.
  • var normalHandler = null;
  • var hormonalHandler = null;
  •  
  •  
  • // Bind to any mood changes that are normal.
  • tricia.bind(
  • "moodChange.normal",
  • {
  • handlerType: "normal"
  • },
  • normalHandler = function( event, oldMood, newMood ){
  •  
  • console.log( "Target: ", event.target );
  • console.log( event.data.handlerType );
  • console.log( this.name + ", hey there " + newMood + "-pants!" );
  •  
  • }
  • );
  •  
  • // Bind to any mood changes that are hormonal.
  • tricia.bind(
  • "moodChange.hormonal",
  • {
  • handlerType: "hormonal"
  • },
  • hormonalHandler = function( event, oldMood, newMood ){
  •  
  • console.log( "Target: ", event.target );
  • console.log( event.data.handlerType );
  • console.log( this.name + ", do you need some time?" );
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • console.log( "--------------------------------------" );
  • // -------------------------------------------------- //
  •  
  •  
  • // Trigger a normal change.
  • tricia.trigger(
  • "moodChange.normal",
  • [ "relaxed", "sassy" ]
  • );
  •  
  • // Trigger a hormonal change.
  • tricia.trigger(
  • "moodChange.hormonal",
  • [ "happy", "truculant" ]
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • console.log( "--------------------------------------" );
  • // -------------------------------------------------- //
  •  
  •  
  • // Unbind the hormonal mood change handler. Now, only
  • // normal and non-namespaced events will be handled.
  • tricia.unbind( "moodChange", hormonalHandler );
  •  
  • // Trigger non-namespaced change (this will trigger invoke
  • // the only remaining event handler - normalHandler).
  • tricia.trigger(
  • "moodChange",
  • [ "sad", "happy" ]
  • );
  •  
  • </script>
  • </head>
  • <body>
  • <!-- Intentionally left blank. -->
  • </body>
  • </html>

If you look at the Girl's class constructor, you can see that I'm creating an internal jQuery collection that contains a single free-standing DOM node, "beacon." I chose to use a custom DOM node to make sure that none of the internal logic of the jQuery event mechanism would be altered by a more traditional node type (ie. <a>).

Within the Girl's event API, I am passing execution of the event functionality off to the "beacon" node where jQuery can work its magic. For the event handlers, however, you'll notice that I am using the proxy() method such that they will execute in the context of the Girl instance. By using the proxy() method, the original callback references can still be used to unbind a specific event handler (proxy is so badass!).

Notice that when I bind and trigger events on the Girl instance, I am making use of most of the jQuery event handling features (ie. bind-time data, name-spaces, trigger-time data, unbinding). When I run the above code, I get the following console output:

--------------------------------------

Target: Object { name="Tricia", more...}
normal
Tricia, hey there sassy-pants!

Target: Object { name="Tricia", more...}
hormonal
Tricia, do you need some time?

--------------------------------------

Target: Object { name="Tricia", more...}
normal
Tricia, hey there happy-pants!

As you can see, this worked perfectly; not only did the event binding and triggering work, the handlers executed in the context of the Girl instance, the event types were successfully name-spaced, and, thanks the use of proxy(), the "hormonal" event handler could be unbound using the original callback reference.

The biggest stumbling block that I encountered when using this approach was the event's "target" property; no matter how I triggered the event, the target property always pointed to the encapsulated "beacon" DOM node, not to the Girl instance. While this might seem like an insignificant problem, when you consider that event handlers are often proxied, the only way to reference the original Girl instance is to use the event's "target" property.

In order to get around this, I bound a pre-trigger event handler to every event type that was being bound to this Girl. Because jQuery guarantees that it will trigger handlers in the order in which they were bound, I know that I can inject my internal handler as the very first one to be invoked. The job of this pre-trigger event handler is simply to change the event's "target" as the event object begins to propagate.

This target-property workaround is definitely sloppy and leaves something to be desired. I am considering this a proof-of-concept, so I'm not going to worry too much about the fact that these pre-trigger handlers are never unbound and can lead to small duplications in effort.

In order to create a Javascript application that has decoupled modules, it is essential that the domain model contains a robust publication and subscription event mechanism. jQuery already has this kind of functionality; so, rather than building my own sub-par event mechanism, I figured it would be really cool to proxy the existing jQuery event architecture by encapsulating free-standing, detached DOM nodes.




Reader Comments

@Romain,

LowPro looks interesting. If I read it correctly, you're attaching an object instance to an element in order to provide a multitude of behaviors?

Where I see class-based event management in shining is when we start to create class-based View-controllers that have to announce events or subscribe to events within a thick-client application.

Reply to this Comment

jquery certainly has excellent event support and is a fantastic javascript library for managing your DOM interactions, however I think using it for pub/sub application framework is inappropriate, in fact all of the common JS DOM libraries fail in this regard, YUI, protoype, and dojo. They all provide great widgets, however they were not originally intended to be anything more than a better layer to interact with the DOM.

the KVO binding model of Sproutcore is the most powerful I've used hands down.

Reply to this Comment

@Erick,

Can I ask you what make the Sproutcore event binding model so powerful? If nothing else, I'd love to play around with rolling that kind of functionality into other frameworks.

Reply to this Comment

Personally, I use this to add jQuery events to non-DOM objects:

  • function ObjName(){
  • var _events = $({});
  • this.bind = function(event_, callback_){
  • _events.bind(event_, callback_);
  • };
  • this.unbind = function(event_, callback_){
  • _events.unbind(event_, callback_);
  • };
  • }

It always seemed to work fine so far and the brevity of the code seems so elegant, but I have a bad gut feeling about "$({})": is it using document internally (which would lead to leaks as far as I've read on some forums) ?

Reply to this Comment

(I wish there was an edit button :/ )

I forgot in the code snipplet that of course, for firing an event, you would use:

  • _events.trigger("myevent" /*and any additional parameters*/);

Reply to this Comment

I think I'll just replace

  • var _events = $({});

by

  • var _events = $(document.createElement('span'));

then I don't have to wonder if it uses document or not :)

Reply to this Comment

@Ben,

Hi Ben,

(let's hope this post all comes out OK)

Sorry for the (way) late reply - Thera's comment stream grabbed my attention back to this article.

Let me preface this all by saying read the docs, they'll do better than I will at explaining this:

http://wiki.sproutcore.com/w/page/12412966/Runtime-Introduction

Also let me make clear I won't even try to scratch the surface on Sproutcores actual model, view, or controller layer. This is just an overview of one very high level core concept.

Sproutcore is an entire MVC javascript application framework built closely to model after Apple's Cocoa framework. In fact it is so closely modeled that somewhere around 2008/2009ish Apple decided to adopt the Sproutcore platform as their platform for mobileme and continues to commit to it quite frequently.

Sproutcore's big win is the property binding pattern (http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/CocoaBindings/CocoaBindings.html) that is built directly into the parent object of all Sproutcore objects, SC.Object, so the key-value observing is inherent throughout the entire object hierarchy. Next they have a very easy to use/understand object inheritance model that is built on Crockford's beget scheme, with a bit of OOP flair layered on top. There are 3 main inheritance functions, +mixin+, +extend+, and +create+. Mixin adds properties to the receiver object. Extend uses mixin but makes the receiver object the prototype of the receiver. Create is the initializer function that sets up the prototype chain and +mixins+ any parameters to the initialized object. Finally, there is an extremely advanced run loop written entirely in javascript that manages all event propagation (these aren't native events) in order to assure that bindings fire and avoid browser timeouts in the case of long running code.

So... to put it all together:

  • Girl = SC.Object.extend({
  • name: '',
  • mood: '',
  •  
  • currentFoodPreference: function() {
  • switch(this.get('mood')) {
  • case 'pleasant':
  • return 'ice cream';
  • case 'unpleasant':
  • return 'ice cream';
  • default:
  • return 'ice cream';
  • }
  • }.property('mood') // computed properties - KICK ASS!
  • });
  •  
  • GirlsFacebookPage: SC.Object.extend({
  • girl: null,
  •  
  • _updateWall: function() {
  • var f = MagicFacebookObject.create();// doesnt exist
  • var m = this.getPath('girl.mood'),
  • f = this.getPath('girl.currentFoodPreference');
  • f.set('status', "I am in a " + m " + " mood and want to eat " + f);
  • }.observes('.girl.currentFoodPreference')
  • });
  •  
  • SomeHackyViewThatIsntARealSCView = SC.Object.extend({
  • girl: null,
  •  
  • _girlObserver: function() {
  • alert(this.get('saying'));
  • }.observes('.girl*')
  •  
  • saying: function() {
  • var g = this.get('girl'),
  • n = g.get('name'),
  • m = g.get('mood');
  •  
  • return ["I am ", n, " and I am extremely ", m].join(" ");
  • }
  • })
  •  
  • var tricia = Girl.create({
  • name: 'Tricia',
  • mood: 'pleasant',
  • })
  •  
  • var v = SomeHackyViewThatIsntARealSCView.create({
  • girl: tricia
  • });
  •  
  • var fb = GirlsFacebookPage.create({
  • girl: tricia
  • })
  •  
  • tricia.set('mood', 'unpleasant'); // fires bindings, 'currentFoodPreference' changes and updates fb page and hacky view sees change to all girl props
  •  
  • tricia.set('name', 'sasha'); // fires bindings, only the alerts fire becase fb page thing isn't watching for name changes

As you can see all the core eventing glue is done and inherent in the framework. The KVO pattern is built entirely around the +get+ and +set+ functions which act as smart accessor/mutator functions. The +set+ function in particular is particularly interesting and I haven't really highlighted it's power here at all, nor did I even scratch the surface on computed properties.

There is a TON more to sproutcore than this but it's all built off the core concept of these bindings. The next place to dig into Sproutcores power is in it's advance model layer architecture (the data store):

http://wiki.sproutcore.com/w/page/12412883/DataStore-Introduction

Enjoy,

EJ

Reply to this Comment

@Thera,

You *can* use the jQuery wrapper on Javascript objects, but it is/was a bit buggy depending on the type of event you are trying to trigger and whether or not the JS object has a "lenght" property. I have been told that Javascript objects are officially supported; but, I have not confirmed this and from what I recall, the documentation on it seems to be a little non-committal. But, please please please take that with a grain of salt.

Using "span" is probably a bit safer. I tried to use this approach in my post, creating a new "beacon" DOM object.

People seem to think that using a manually created internal queue is probably the most effective, especially if you don't need the event-canceling kind of stuff.

@Erick,

For starters,

Apple decided to adopt the Sproutcore platform as their platform for mobileme and continues to commit to it quite frequently.

... that is badass!! Talk about a "testimonial"!

I sooorta understand what you're doing there; though without a SproutCore background, it's just a bunch of assumptions. It looks like there is a serious amount of data-binding going on behind the scenes.

I believe that SproutCore has some video tutorials on their site. I looked at them a long time ago, but it's probably a time to check them out again now that I have a bit more experience with architecture.

Reply to this Comment

@Ben,

I sooorta understand what you're doing there; though without a SproutCore background, it's just a bunch of assumptions. It looks like there is a serious amount of data-binding going on behind the scenes.

There is definitely a bunch going on behind the scenes. What I wrote is merely a topic example. Something I never would have been able to write w/o having previously constructed an application on the 0.9.x branch and currently working on version 2 of my app on the SC 1.4.x branch.

The latest version of SC has made huge strides but it is a huge undertaking to go understand what is going on down deep... it's worth the journey though there is some really cool stuff in there.

Cheers,

EJ

Reply to this Comment

@Erick,

Ugg - why do I have to do all this "client" work :) Can't I just sit and play with Javascript all day???

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.