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 jQuery Conference 2010 (Boston, MA) with:

Experimenting With Sub() And Deferred Objects In jQuery 1.5

By Ben Nadel on

jQuery 1.5 added a method called sub(). I'm not entirely clear on what sub() does or how it works(); but, from what I can gather, it creates a sub-class of the jQuery object in an effort to let you augment a copy of the library without permanently damaging the core jQuery instance. A feature like this could be great for plugin developers who want to simplify their internal code by working with a locally-augmented version of the library - a sub-classed instance would allow for this in an entirely encapsulated way.

 
 
 
 
 
 
 
 
 
 

To experiment with the new $.sub() method, I thought I would take the new $.when() method and augment it to support a chained when() method. In other words, I wanted to be able to write code that looked like this:

  • $.when( ... ).then().when( ... ).then();

By default, a call to the $.when() method returns a promise object that allows for then(), done(), and fail() callbacks. In the above case, however, I need to override the native $.when() method in order to augment the returned promise object with its own when() method. Furthermore, the promise object returned from this new when() method would, itself, need to be hooked into the outcome of the resolved-state for the original promise object.

Now, I don't want to alter the functionality of the core jQuery library; so, I'll use the new sub() method to sub-class the jQuery instance before I alter it. Before we do this, however, I think it would be worth-while to look at a very telling line from the sub() method source code:

  • jQuerySubclass.fn = jQuerySubclass.prototype = this();

Out of context, this line of code is not all that meaningful; so, I'll try to translate it into a more mundane example:

  • Person.prototype = Animal();

Here, it's pretty clear what's going on - we're creating an instance of the Animal class and then using that instance as the prototype of the Person class. This means that new instances of the Person class will inherit all the methods and properties of the Animal class. It also means that setting properties on a Person instance will affect the Person instance without affecting the Animal class (this is a bit of fuzzy generalization).

I'll be honest - the sub() method source code is super dense; I don't quite understand what it's doing. But, taking the above, simplified explanation, let's take a look at my sub-classing example.

  • <!DOCTYPE html>
  • <html>
  • <head>
  • <title>Using Sub() And Deferred In jQuery 1.5</title>
  • <script type="text/javascript" src="../jquery-1.5.js"></script>
  • <script type="text/javascript">
  •  
  •  
  • // First, we're gonna call noCoflict() to release the use of
  • // the $ as the library alias; we're doing this so we can
  • // turn the $ into our "sub" version.
  • //
  • // NOTE: This is not necessary, but it feels right.
  • jQuery.noConflict();
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Now, we are going to sub-class the jQuery library and
  • // store the new version in the $. However, since we're
  • // using some other methods, we'll wrap this in its own
  • // execution space.
  • var $ = (function( jQuery, $ ){
  •  
  • // I add "when"-ability to the given promise object.
  • function whenifyPromise( promise ){
  •  
  • // This promise object already has methods for
  • // then(), done(), and fail(). We need to add to it
  • // the ability to call when() again.
  • promise.when = function(){
  •  
  • // Capture the when-based arguments.
  • var whenArguments = arguments;
  •  
  • // Create a new deferred object to bridge the
  • // current promise to the new promise.
  • var deferred = $.Deferred();
  •  
  • // When the when() method is invoked on the
  • // promise object, what we actually want to do is
  • // hook into the done() on the original promise
  • // so that this is chained with the other
  • // callbacks; the difference, of course, is that
  • // we are using the callback to trigger another
  • // root-when() method call.
  • promise.done(
  • function(){
  •  
  • $.when
  • .apply( this, whenArguments )
  • .done( deferred.resolve )
  • ;
  •  
  • }
  • );
  •  
  • // Return a new promise that is bound to the new
  • // use of when(), not the previous use of when().
  • return(
  • whenifyPromise(
  • deferred.promise()
  • )
  • );
  •  
  • }; //// END --- promise.when() ---
  •  
  •  
  • // Return the when-augmented promise.
  • return( promise );
  •  
  • }
  •  
  •  
  • // By default, the when() method returns a promise
  • // object. We want to intercept that promise object and
  • // augment it, adding a chainable when() method.
  • $.when = function(){
  •  
  • // Pass the request onto the native when() method
  • // and capture the core promise that comes back.
  • //
  • // NOTE: We are using "jQuery" here instead of the
  • // "$" so as to access the original functionality.
  • var promise = jQuery.when.apply( this, arguments );
  •  
  • // Augment the promise to allow for a chained when()
  • // invocation for a new promise.
  • return(
  • whenifyPromise( promise )
  • );
  •  
  • };
  •  
  •  
  • // Return the augmented jQuery library - the one that
  • // we've subclassed.
  • return( $ );
  •  
  • })( jQuery, jQuery.sub() );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Load a bunch of scripts and make sure the DOM is ready.
  • $.when(
  • $.getScript( "./script.cfm?id=1" ),
  • $.getScript( "./script.cfm?id=2" ),
  • $.getScript( "./script.cfm?id=3" ),
  • $.getScript( "./script.cfm?id=4" ),
  • $.getScript( "./script.cfm?id=5" ),
  • $.getScript( "./script.cfm?id=6" ),
  • $.getScript( "./script.cfm?id=7" ),
  • $.getScript( "./script.cfm?id=8" ),
  • $.getScript( "./script.cfm?id=9" ),
  • $.getScript( "./script.cfm?id=10" )
  • ).done(
  • function( /* Deferred Results */ ){
  •  
  • // Log the fact that first set of scripts have
  • // finished loading successfully.
  • console.log(
  • "..........................",
  • "First set loaded."
  • );
  •  
  • }
  • ).when(
  • $.getScript( "./script.cfm?id=11" ),
  • $.getScript( "./script.cfm?id=12" ),
  • $.getScript( "./script.cfm?id=13" ),
  • $.getScript( "./script.cfm?id=14" ),
  • $.getScript( "./script.cfm?id=15" ),
  • $.getScript( "./script.cfm?id=16" ),
  • $.getScript( "./script.cfm?id=17" ),
  • $.getScript( "./script.cfm?id=18" ),
  • $.getScript( "./script.cfm?id=19" ),
  • $.getScript( "./script.cfm?id=20" )
  • ).done(
  • function( /* Deferred Results */ ){
  •  
  • // Log the fact that second set of scripts have
  • // finished loading successfully.
  • console.log(
  • "..........................",
  • "Second set loaded."
  • );
  •  
  • }
  • ).when(
  • $.getScript( "./script.cfm?id=21" ),
  • $.getScript( "./script.cfm?id=22" ),
  • $.getScript( "./script.cfm?id=23" ),
  • $.getScript( "./script.cfm?id=24" ),
  • $.getScript( "./script.cfm?id=25" ),
  • $.getScript( "./script.cfm?id=26" ),
  • $.getScript( "./script.cfm?id=27" ),
  • $.getScript( "./script.cfm?id=28" ),
  • $.getScript( "./script.cfm?id=29" ),
  • $.getScript( "./script.cfm?id=30" )
  • ).done(
  • function( /* Deferred Results */ ){
  •  
  • // Log the fact that third set of scripts have
  • // finished loading successfully.
  • console.log(
  • "..........................",
  • "Third set loaded."
  • );
  •  
  • }
  • );
  •  
  • </script>
  • </head>
  • <body>
  •  
  • <h1>
  • Using Sub() And Deferred In jQuery 1.5
  • </h1>
  •  
  • </body>
  • </html>

Once I have released the use of $ as a jQuery alias (an unnecessary but enjoyably explicit step), I then sub-class the jQuery library and assign the newly sub-classed instance to the $. I do all of this inside of a self-executing function which allows me to define helper functions without altering the global name-space.

Inside of this self-executing function, I override the $.when() method on my sub-classed instance. This allows me to change the way when() works for my sub-classed instance only, leaving the core jQuery library unchanged. And, in fact, within my sub-classed when() method, you can see that I am still able to make use of the core jQuery.when() method when needed.

I won't go into detail about how I'm altering the promise object returned by when(); but, basically, I need to create a Deferred bridge between the original promise object and the promise object returned by my chained when() invocation. It's a bit of mental tongue-twister to follow the control flow; but, if you watch the above video, you can sort of get the gist of what's going on.

While I am still wrapping my head around how this all works, I can definitely see some nice use-cases for plugin developers. I can imagine a plugin developer creating a locally-sub-classed jQuery instance for use only with the internal plugin logic. Such a local augmentation could provide highly customized value without any global fall-out.




Reader Comments

After listening to yayQuery about this new sub method, I'm still not sure how useful this will be. I tend to wander back to converting a jQuery into a utility core and ditching the bulk of it's DOM wrappers: http://james.padolsey.com/javascript/76-bytes-for-faster-jquery/

Potentially I could use sub combined with this to make a very fast custom core, for example to use new functions like forEach(). This seems very edge case with potentially many drawbacks with future compatibility.

Reply to this Comment

@Drew,

Oh snap - I didn't know there was a new YayQuery episode. I have to get me some of that. So far, I'm not sure how useful this stuff is. I have some thoughts on how it could be great; but, not sure if those are valid... or if they are just solutions looking for problems :)

Reply to this Comment

@Ben, I'm about to tackle building a large single page application in jQuery. My hope is sub can be used to help compartmentalize my code without breaking the dead simple jQuery core/UI-style API.

I'm a little concerned jQuery will not scale the way I need especially without some previous successful cases studies for me to dig into. I may have to jump ship to Dojo, as tempting as that is I have a lot of confident in the jQuery community.

Reply to this Comment

@Drew,

I'm certainly new to the concept of building big, complex client-side apps; but, from what I've seen so far, I am not sure there is anything inherently limited in scale when it comes to jQuery. Or, at least that other libraries are any more scalable?

I can't really say, though.

Reply to this Comment

@Cowboy,

I like the idea of having module-specific versions of jQuery; I don't have the best use-case in my mind at the moment. But, it definitely is something that makes sense - especially for people like you who rock crazy awesome jQuery plugins like 24x7 :D

Reply to this Comment

I've been using it for 'micro' widgets that add styles and such:

  • var style$ = jQuery.sub();
  • style$.extend(style$.fn,{
  • box : function(){
  • return this.addClass('ui-widget ui-widget-content ui-corner-all')
  • },
  • header : function(){
  • return this.addClass('ui-helper-reset ui-state-default ui-state-active ui-corner-top')
  • },
  • $ : function(){
  • return $(this);
  • }
  • });
  • $.fn.style$ = function(){
  • return style$(this)
  • }
  • $('#foo').style$().header()

Essentially its for a group of widgets that have such common names that I want to ensure there are no conflicts.

Reply to this Comment

Hi Ben,

I used your code to create a chain of .when().done().when().done()... code to accomplsih some steps involving animations and AJAX. But it wont work for me. Guess I am doing something wrong. I would appreciate your help. Basically I am doing a .when(do slide animation).done().when(ajax call to get data).done().when(another ajax call).done(a final animation)

When I run my specific call I put console.logs prior to the start of the slide animation and within the callback of it. and I get a log of the animation start and logs of all the other .when() calls executing before the log of the first slide animation finishing.

Do you know how I can use your idea to get all the events chaining properly?

Reply to this Comment

Use .pipe() to chain multiple (dynamically retrieved) ajax requests ie.

.when(ajax1).when(ajax2).pipe(function( ajax1, ajax2){
if( ajax1.more ){
//omg need more
return $.when( ajax1.more )
}
}).then( /*all done*/ );

Here's something I wrote in case I ran into the problem again, fill in your ajax calls as needed: https://gist.github.com/1219564

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.