Merging jQuery Deferreds and .animate()

read 2 comments

Editor's Note: This article originally appeared on danheberden.com.

jQuery’s .animate() method, and the shorthand methods that use it, are fantastic tools to create animations. Creating animations that link together to achieve a particular effect, and do something specific at the end of the animation, can be a painful, messy task. Luckily, we have .queue() for mashing animations together.

But what happens when you want to bridge the gap between ajax requests and animating? When you want to queue a bunch of animations, get data from the server, and handle it all at once, without a crap-load of nested callbacks? That’s when jQuery.Deferred() puts on its cape, tightens its utility belt, and saves the day.

Disclaimer

I should note, however, that this is more-or-less giving an example of a pending feature request to add deferreds support in $.fn.animate. If the feature request is accepted and landed, it won’t show up until version 1.6 of jQuery. The principles, however, speak to jQuery’s flexibility and how to forge its multitude of great features into an even stronger tool.

While this works, its behavior isn’t consistent with that of jQuery. Namely, the new custom animate method doesn’t return ‘this’, but a Deferred object. However, that’s kind of the point of $.sub(): allowing you to copy the jQuery object and have your way with it. So, do try this at home – just don’t threaten my life if your site explodes.

The Demo

The following demo is a basic, distilled use-case for this kind of situation. Clicking the button opens a div that contains a loading message. While it’s opening, it is also querying the server for information to populate the box. Once both have finished, the loading div is hidden and the box with the retrieved data remains.

Modifying .animate()

Here is the code driving the change to .animate()

JavaScript:
  1. // create a sub of jquery (Basically, a copy we can mess with)
  2. var my$ = $.sub();
  3.  
  4. // make my$ have a modified animate function
  5. my$.fn.animate = function( props, speed, easing, callback ) {
  6.   // from jQuery.speed, forces arguments into props and options objects
  7.   var options = speed && typeof speed === "object" ?
  8.   jQuery.extend({}, speed) : {
  9.     complete: callback || !callback && easing ||
  10.     jQuery.isFunction( speed ) && speed,
  11.     duration: speed,
  12.     easing: callback && easing || easing &&
  13.     !jQuery.isFunction(easing) && easing
  14.   };
  15.  
  16.   // create the deferred
  17.   var dfd = my$.Deferred(),
  18.   // a copy of the complete callback
  19.   complete = options.complete,
  20.   // and the count of how many items
  21.   count = this.length;
  22.  
  23.   // make a new complete function
  24.   options.complete = function() {
  25.     // that calls the old one if it exists
  26.     complete && complete.call( this );
  27.     // and decrements count and checks if it's 0
  28.     if ( !--count ) {
  29.       // and when it is, resolves the DFD
  30.       dfd.resolve();
  31.     }
  32.   };
  33.  
  34.   // all the hooks have been made, call the regular animate
  35.   jQuery.fn.animate.call( this, props, options );
  36.  
  37.   // return the promise that we'll do something
  38.   return dfd.promise();
  39. };

While the comments explain just about everything, be sure to read up on $.sub() if you haven’t already. The new animate function on my$ simulates the method signature of the function, $.fn.animate, it’s attempting to replace. In short, speed, easing, and callback are forced into an options object — the same as if the second parameter was an object.

The deferred is created, a copy of the complete callback, and the count of how many items. Why? We want to fire the resolve() function on the deferred once all of the animations have finished. The complete() callback is replaced with a wrapper function that calls the original callback and decrements and checks the count. When the count reaches zero, all items have been animated and it’s safe to fire the resolve() function.

The untouched, standard version of $.fn.animate is called with the same ‘this’, properties, and the modified options object with the new complete wrapper function.

Returned is the promise, dfd.promise(), that lets $.when() do its awesomeness.

Putting it into action

If you haven’t familiarized yourself with deferreds, I highly recommend you read Eric Hynds’ fantastic article about it.

JavaScript:
  1. // retrieves content and updates a dom element
  2. // returns a promise
  3. function populateBox() {
  4.   return $.ajax({
  5.     url: 'your/server/url',
  6.     data: { },
  7.       type: "POST",
  8.       success: function( data ) {
  9.         $('#content').slideDown().html( data );
  10.       }
  11.     });
  12.   }
  13.  
  14. // the "Get Message" click handler
  15. $('button.load').click( function() {
  16.  
  17.   // save the button, box and loading as my$ objects
  18.   var $button = my$(this).hide(),
  19.   $box = my$('#box'),
  20.   $loading = my$('.loading');
  21.  
  22.   // when the functions are done
  23.   $.when(
  24.     // $box was created with my$, so it will
  25.     // use the custom animate function
  26.     $box.slideDown(),
  27.     populateBox()
  28.     // then run the 1st function on success
  29.     // and the second function if either fails
  30.   ).then(
  31.     function() {
  32.       // remove loading, we're done
  33.       $loading.slideUp();
  34.     },
  35.     function() {
  36.       // get that button back here
  37.       $button.show();
  38.       // and hide the box
  39.       $box.slideUp();
  40.     }
  41.   );
  42. });

Because the new animate function was created on my$, the copy of jQuery made using $.sub(), .hide(), .slideUp(), and other helper functions that use the custom .animate() function.

When the animation and ajax request both succeed, thus resolving the promise as true, the first callback function of $.then() is called. However, if one of them fails, the second will be called. You can re-run the demo (by clicking on the run button) and opt to fail the ajax request to see the second function get run. The .then() function is a handy mix of .done() and .fail(), which are useful if you want to provide multiple callbacks.

Summary

While I highly doubt this will be the solution to using deferreds with .animate(), I’m confident it’ll get the ball rolling. Too, it covers some other topics, such as $.sub(), deferreds, and wrapping functions with alternate behavior, that hopefully you found interesting.

comment feed

2 comments

  1. Thomas

    The ternary operations going on in that var options = ... line are crazy. Can you say some more about what's going on there?

  2. I'm not sure I understand what this does:

    var dfd = my$.Deferred(),

    Could you please explain that to me?

One Ping

  1. [...] This post was mentioned on Twitter by mclaughj rss, Alcides Ramos, dz khadorkin, Codrops, Larry King and others. Larry King said: Merging jQuery Deferreds and .animate() » Learning jQuery - Tips ... http://bit.ly/gZeVED #jQuery [...]

Sorry, but comments for this entry are now closed.