Powering Publish And Subscribe Functionality With Native jQuery Event Management

Posted August 30, 2010 at 9:50 AM by Ben Nadel

Tags: Javascript / DHTML

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

Aug 31, 2010 at 11:08 AM // reply »
1 Comments

Very interresting approach to manage events (Class based), and more like an Prototype.js way. I've already used a similar technics (2 years ago...) for jquery with lowPro (http://www.danwebb.net/2008/2/3/how-to-use-low-pro-for-jquery), and it was very efficient.
Thanks.


Aug 31, 2010 at 2:55 PM // reply »
10,638 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.


Sep 15, 2010 at 2:59 AM // reply »
4 Comments

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.


Sep 15, 2010 at 8:31 AM // reply »
10,638 Comments

@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.


Feb 21, 2011 at 9:55 AM // reply »
4 Comments

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) ?


Feb 21, 2011 at 9:56 AM // reply »
4 Comments

@Thera,

Ah, sorry about the mismatched strong tag.


Feb 21, 2011 at 10:17 AM // reply »
4 Comments

(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*/);


Feb 21, 2011 at 10:42 AM // reply »
4 Comments

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 :)


Feb 21, 2011 at 11:45 AM // reply »
4 Comments

@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


Feb 21, 2011 at 11:49 AM // reply »
4 Comments

there's a few typos in that code i realize - but i hope you get the gist


Feb 21, 2011 at 2:31 PM // reply »
10,638 Comments

@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.


Feb 21, 2011 at 2:46 PM // reply »
4 Comments

@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


Feb 21, 2011 at 2:51 PM // reply »
10,638 Comments

@Erick,

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



Post A Comment

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.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
InVision App - Prototyping Made Beautiful With Prototyping Tools Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
Feb 3, 2012 at 10:49 PM
How I Got Node.js Running On A Linux Micro Instance Using Amazon EC2
Wow this was really helpful! Only thing I would add is you need to update your .bash_profile after you edit the secure_path. This is what I did: $ . ~/.bash_profile Otherwise, NPM won't be found. ... read »
Feb 3, 2012 at 10:14 PM
Pushing Base64-Encoded Images Over HTML5 WebSockets With Pusher And ColdFusion
@Ben, Just wanted to let you know that pusher are soon to start limiting sizes on messages. This was the detail that came through in the Feb dispatch: "However, we will soon be limiting the s ... read »
Feb 3, 2012 at 5:05 PM
Regular Expressions Make CSV Parsing In ColdFusion So Much Easier (And Faster)
I tried using your RegEx in my C# program, but it was matching an extra empty-string at the end and so I would end up with an extra field that doesn't exist, so I changed it to this: (^|,)("(?: ... read »
Feb 3, 2012 at 3:47 PM
ColdFusion Supports HTTP Verbs PUT And DELETE (As Well As GET And POST)
Josh Cyr posted this on Twitter just a little bit ago. Thought it was appropriate. http://stackoverflow.com/questions/1619152/how-to-create-rest-urls-without-verbs/1619677#1619677 ... read »
Feb 3, 2012 at 2:28 PM
Changing The Execution Context Of Your Self-Executing Function Blocks In JavaScript
@Michael, You definitely make a good point (and extra points for quoting movies - I love movies). When you use a return() statement to define the object's public API, it does provide a consistent a ... read »
Feb 3, 2012 at 2:04 PM
Changing The Execution Context Of Your Self-Executing Function Blocks In JavaScript
To quote Jurassic Park: "Just because you can doesn't mean you should". I completely, utterly disagree with the thought that this is more readable. Consider the current module pattern: if ... read »
Feb 3, 2012 at 1:10 PM
REST API Design Rulebook By Mark Masse
@Jordan, Yeah, WRML was created by Mark Masse (author of the book). I also found it to be a bit convoluted. I suppose it is intended to allow the Client to be able to programmaticaly respond to cha ... read »
Feb 3, 2012 at 1:08 PM
ColdFusion Supports HTTP Verbs PUT And DELETE (As Well As GET And POST)
@Jason, To be honest, I don't have good answers for that kinds of stuff. And, to the point, that is specifically why I *really* liked the REST API Design Rulebook by Mark Masse - he just cuts throu ... read »