Jacob's Programming Explosions

Managing jQuery Animations With Promises

This post is about managing asynchronous functions in jQuery

The other day, I was at the grocery store with my wife when a graphic designer friend of mine texted me asking for some jQuery help for his website.

text conversation with my friend

Basically, he was doing a couple of jQuery animations and had another animation he wanted to happen only once the others were complete. He couldn’t figure out how to make the last animation wait until all the others were complete. If you are familar with jQuery at all this will make sense to you (and you may want to jump down to the next section). Maybe you have also had this problem. In case you aren’t familar, the problem my friend had was due to the fact that jQuery animation functions (and AJAX funtions!) run asynchronously. For instance:

1
2
3
$('.green-div').hide();
$('.green-div').fadeIn(1000);
$('.green-div').css({'background-color': 'blue'});

In this example the initially hidden green div will take 1000 miliseconds to fade in and its color will be changed to blue. However, you will not see the color of the div change from green to blue. It will fade in as blue. This is because line 3 gets called before line 2’s fadeIn function has completed. This is because .fadeIn() executes asynchronously. Meaning, it gets called and starts doing its thing, but the program does not wait until it has finished before evaluating the next line of code. So again, the reason you would have never seen the div turn blue is because line 3 executed very quickly while line 2 took a second.

For situations as simple as this one, the solution would be to pass a callback function to .fadeIn(). Like so:

1
2
3
4
$('div').hide();
$('div').fadeIn(1000, function(){
  $('div').css({'background-color': 'blue'});
});

This time, we pass an anonymous function to .fadeIn(), which gets called only once .fadeIn() has finished its job. So here we would see the div fade in, then it would snap to blue.

Okay, so what if you have three functions you want to happen sequentially? What if between fading in the div and turning it blue we also wanted to slide it down 50 pixels? Things get a little stickier. But as you may know, we could just pass a callback to the function we want the slide to happen after:

1
2
3
4
5
6
$('div').hide();
$('div').fadeIn(1000, function(){
  $('div').animate({'margin-top': '50px'}, 300, function(){
    $('div').css({'background-color': 'blue'});
  });
});

This works, but we are nearing the gates of Callback Hell. Even in this contrived, simple example our code has become messy looking. And messy looking code is hard to maintain. We could have at least used named functions…I mean c’mon.

Getting Fancy

So back to my friend’s problem. He had some animations he wanted to do, and another animation he wanted to happen only after all the others were finished. The catch is that he wanted the initial set of animations to happen asynchronously and he didn’t know how long the animations would take to finish. Of couse, getting the first set of animations to happen asychronously took no effort; he just called them one after the other.

The real problem was knowing when all of the animations were done. This situation is a lot less linear than the scenario we went over above. We can’t simply pass each animation as a callback for the previous one, as we want the batch of animations to run concurrently. Also, we cannot simply pass the final animation as a callback, since we don’t know which animation will be the final animation. We need a way of saying, “hey, when all of these animations are done, do this animation.”

This is where promises save the day!

Promises help us manage asynchronous operations and their callbacks. Let’s look at an example based on my friend’s problem. For this example we have a <button>, three <div>s and a <p>. When the user clicks the botton, we want to perform a unique animation for each of the <div>s (concurrently!). Once those are all done, we want to append the <p> to notify the user that they are awesome. To accomplish this, we are going to use jQuery’s .when() and .done() functions. I’ll explain more below, but for now, just note that jQuery’s animations (as well as AJAX functions) return a promise.

1
2
3
4
5
6
7
8
9
$('button').click(function(){
  $.when(
    $('.div1').animate({'margin-left': '40px'}),
    $('.div2').animate({'margin-left': '80px'}),
    $('.div2').animate({'margin-left': '120px'})
  ).done(function(){
    $('p').append('You are awesome!');
  });
})

When the button is clicked, we fire the three animations within .when(). Once all three are completed, the function provided to .done() is called. You can check out a live demo here.

This not only solves our problems, it is also beautiful!

I wish I could say that I immediately knew using .when() and .done() was the solution my friend was looking for and that I texted him that while I was wandering up and down the aisles at Associated, but I think my initial reaction was something like, “Hm…it should be easy. Crap, how DO you do that…” The embarrassment of lacking a solution was all the motivation I needed to dive in to the wonderful world of deferreds and promises. And I’m really glad I did, because they’re awesome.

Getting Technical

As I hinted earlier, jQuery’s AJAX functions can also make use of .when() and .done(), as well as a function called .then(). The .done() and .then() functions are fairly similar, and replacing .done() with .then() in our code above would actually give us the same result. One detail to note is that .done() can be passed multiple functions, or an array of functions. Passing multiple callbacks to done is nice if you want some stuff to happen asynchronously. If you have some things you want to do linearly, you can chain .done()’s onto each other. ie: $.when(animateSomething, animateSomethingElse).done(doThis).done(thenThis);

So what is really going on when you use .when() and .then()? Well, .when() is waiting until the state of the promises passed to it (in our case we passed functions that returned promises) are all ‘resolved’ (as opposed to ‘pending’ or ‘rejected’). Once this happens, the .done() callbacks are fired. For a deeper understanding of promises (jQuery’s implementation, specifically), check out the links below.

I’d like to write more technically about promises (and deferreds) in jQuery in the near future. They are especially powerful for managing AJAX transations. So stay tuned!

Resources

http://blog.mediumequalsmessage.com/promise-deferred-objects-in-javascript-pt1-theory-and-semantics http://jquery-part2.codeschool.com/levels/6/challenges/1 https://www.youtube.com/watch?v=zD-JoRDk8ig http://www.htmlgoodies.com/beyond/javascript/making-promises-with-jquery-deferred.html http://api.jquery.com/deferred.promise/ http://api.jquery.com/deferred.done/