How do I animate in jQuery without stacking callbacks?
Asked Answered
N

7

15

Let's say I have three divs, and I'd like each to animate once the previous one is done. Currently, I write this:

$('div1').fadeOut('slow', function() {
    $('div2').fadeOut('slow', function() {
        $('div3').fadeOut('slow');
    });
});

Which is ugly, but manageable.

Now imagine I have 10 different animations that need to happen one after the other on different elements. Suddenly the code gets so clunky that it's extremely hard to manage...

Here's pseudocode for what I'm looking to do:

$('div1').fadeOut('slow' { delay_next_function_until_done: true } );
$('div2').fadeOut('slow' { delay_next_function_until_done: true } );
$('div3').animate({ top: 500 }, 1000 );

How do I achieve this?

Nerland answered 29/4, 2012 at 6:50 Comment(1)
cdmckay.org/blog/2010/06/22/…Indigested
Q
22

If you're using a recent version of jQuery, use the animation promises:

$('div1').fadeOut('slow').promise().pipe(function() {
    return $('div2').fadeOut('slow');
}).pipe(function() {
    return $('div3').animate({ top: 500 }, 1000 );
});

You can make it generic:

$.chain = function() {
    var promise = $.Deferred().resolve().promise();
    jQuery.each( arguments, function() {
        promise = promise.pipe( this );
    });
    return promise;
};

var animations = $.chain(function() {
    return $('div1').fadeOut('slow');
}, function() {
    return $('div2').fadeOut('slow');
}, function() {
    return $('div3').animate({ top: 500 }, 1000 );
});

$.when( animations ).done(function() {
    // ALL ANIMATIONS HAVE BEEN DONE IN SEQUENCE
});

Still a lot of function closures but that's the very nature of Javascript. However, it's much more natural and a lot more flexible using Deferreds/Promises since you avoid callbacks "inception".

Quechuan answered 29/4, 2012 at 10:39 Comment(2)
Simple and clean - very nice :)Boredom
pipe() is deprecated in favor of then() now -Trigonal
I
4

I do that, with this method you can put all divs as you want, only adding or deleting elements in the list var, also you can reorder them and you don't have to worry about the delay time.

var list = [ '#div1', '#div2', '...' ];
var i = 0;
function fade(cb) {
    if (i < list.length) {
        $(list[i]).fadeOut('slow', function() {
            i++;
            fade(cb);
        });
    } else {
        cb && cb();
    }
}
fade();

you can also add a callback when the process ends

fade(function(){alert('end')});

Demo

Isometropia answered 29/4, 2012 at 7:28 Comment(6)
@jfriend00 your code have a bug with the sync option, at the moment an item have sync you will throw another sequence at a time, i put you an example to undestand it list = [0->sync, 1->nosync, 2->sync, 3->nosync, 4->nosync, 5->nosync, 6->nosync, 7->sync] it will run 0 1 (because 0 has sync) // 2 (throwed by 0), 3 (throwed by 1), 4 (because o2 has sync) // 5 (throwed by 2), 6 (throwed by 3), 7 (throwed by 4) and i suposed that the correct run will be 0 1 // 2 3 // 4 // 5 // 6 // 7Isometropia
The sync option requires that you correctly identify sychronous operations and asychronous operations. Thus .css() is sync: true and all animations are not. Other than that, I'm not sure what "bug" you're talking about. If this was production code, it could have a list of all known animation methods and detect them autoamtically, but I didn't think that heft was appropriate in my answer. Also, why did you put this as a comment on your answer, not mine (I almost didn't see your comment because of that)?Tourbillion
but after an operation with sync, you will throw two operations synchronously, but if you put next operation sync : false, this will be ignores and you will continue throw the next two operations, if you add another sync command, then will be 3 operations at a time and go onIsometropia
You don't seem to understand what the sync is used for. It's used only for operations that run immediately like the .css() command that do not have a completion function. When used for those types of operations, it does not cause any of the problems you describe and works perfectly fine. See my jsFiddle which has an example of that which does not cause any of the problems you describe. You could have ten sync operations in a row and as long as they are actually synchronous operations (things that execute immediately), it would work fine. Do not put sync on an animation.Tourbillion
Ups, read another thing xD, i undertood that you used that for css method, only that i missunderstood a think, I'm sorry. But i would don't use the css var, anf if a method is a not a jquery effect i would detect that and made it sync, just only to prevent to the programmer to use the syn var. But sorry for say that you had a bug.Isometropia
Quick note: Deprecation Notice:As of jQuery 1.8, the deferred.pipe() method is deprecated. The deferred.then() method, which replaces it, should be used instead.Crocodile
T
3

When ever completion functions or callbacks get nested too deep or code is getting repeated over and over again, I tend to think about a data table solution with a common function:

function fadeSequence(list) {
    var index = 0;
    function next() {
        if (index < list.length) {
            $(list[index++]).fadeOut(next);
    }
    next();
}

var fades = ["div1", "div2", "div3", "div4", "div5"];
fadeSequence(fades);

And, if you wanted a different type of animation for some of the items, you could create a array of objects that describe what each successive animation is supposed to be. You could put as much detail in the array of objects as was needed. You can even intermix animations with other synchronous jQuery method calls like this:

function runSequence(list) {
    var index = 0;
    function next() {
        var item, obj, args;
        if (index < list.length) {
            item = list[index++];
            obj = $(item.sel);
            args = item.args.slice(0);
            if (item.sync) {
                obj[item.type].apply(obj, args);
                setTimeout(next, 1);
            } else {
                args.push(next);
                obj[item.type].apply(obj, args);
            }
        }
    }
    next();
}

// sequence of animation commands to run, one after the other
var commands = [
    {sel: "#div2", type: "animate", args: [{ width: 300}, 1000]},
    {sel: "#div2", type: "animate", args: [{ width: 25}, 1000]},
    {sel: "#div2", type: "fadeOut", args: ["slow"]},
    {sel: "#div3", type: "animate", args: [{ height: 300}, 1000]},
    {sel: "#div3", type: "animate", args: [{ height: 25}, 1000]},
    {sel: "#div3", type: "fadeOut", args: ["slow"]},
    {sel: "#div4", type: "fadeOut", args: ["slow"]},
    {sel: "#div1", type: "fadeOut", args: ["slow"]},
    {sel: "#div5", type: "css", args: ["position", "absolute"], sync: true},
    {sel: "#div5", type: "animate", args: [{ top: 500}, 1000]}
];
runSequence(commands);

And, here's a working demo of this second option: http://jsfiddle.net/jfriend00/PEVEh/

Tourbillion answered 29/4, 2012 at 7:29 Comment(2)
LOL, I thought the concept was to make it simpleEleonoreleonora
The first option is very simple. The second one is reusable for any animation sequence across as many objects as you want without writing any new code each time. You just build an animation table. If you'd rather code the sequence I show in the second one manually, feel free, but the OP asked how to do it without all the nested callbacks so I wrote a generic engine that would handle it. Also keep in mind the OP asked how to make it work with 10 or more sequential and different animations on different objects. I don't see any other solutions that address that part of the question.Tourbillion
M
2

One way to do this would be to write your own helper function, like so:

$.fn.sequentialFade = function() {
    if(this.length > 0) {
        var $set = $(this);
        $set.eq(0).fadeOut(function() {
            $set.slice(1).sequentialFade();
        });
    }
}

And use it like so:

$('.div1, .div2. .div3').sequentialFade();

http://jsfiddle.net/JpNgv/

Malonylurea answered 29/4, 2012 at 7:1 Comment(1)
Note: this will fade them in DOM sequence order, not necessarily in the order you list them in because jQuery objects order their items in DOM order.Tourbillion
C
1

try something like:

$( 'div1' ).fadeOut();
$( 'div2' ).delay( 500  ).fadeOut();
$( 'div3' ).delay( 1000 ).fadeOut();

Adjust the timing as necessary

Covell answered 29/4, 2012 at 6:53 Comment(3)
That seems like a really clunky way to do this. Plus, I'd imagine the delay does not guarantee that these events will align, especially when building up a chain of many events.Indigested
@Indigested -- Actually with jQuery it does. jQuery using the timing between the last frame and the current one, so unless the browser locks, they'll only be off by a few ms.Covell
He got 10 elements. instead of writing it 10 times, you can do it with $(...).each and use the index for the delay. check this outWhig
W
1

Use this:

$('#div1, #div2, #div3').each(function(index){
    $(this).delay(1000 * index).hide(1000);
});

If you can give the <div>s a class:

$('.forHide').each(function(index, value){
    $(this).delay(1000 * index).hide(1000);
});​
  • The first element fades out after 1000 * 0 = right away with animation of one second.
  • The second element fades out after 1000 * 1 = One second with animation of one second.
  • The third element fades out after 1000 * 2 = Two seconds with animation of one second.
  • ...
  • ...
  • The n element fades in after 1000 * n = n seconds with animation of one second.

Live DEMO

Whig answered 29/4, 2012 at 6:53 Comment(9)
But, this cannot guarantee the effect will complete in 1000 milliseconds.Eleonoreleonora
@Starx. it is guaranteed now. check it out.Whig
Does the jquery selector guarantee the return order will be the order you put elements in the selector? Just curious...Midweek
@Endophage. It will fade them in the order they are in the DOM. take a look on this fiddleWhig
@gdordon so if you wanted them to fade in an order different to that in the dom, say bottom to top rather than top to bottom, there isn't a clean way to do that?Midweek
@Endophage. If you would only want to reverse the order it was easy, if it based on some logic it wouldn't be clean.Whig
@Endophage: The reverse option: $($('.forHide').get().reverse()).each(function(){...});Whig
@gdordon yeah, aware of reverse, was just coming up with a simple example. You answered the question though. If there was some order other than DOM order or reverse DOM order, it would become messy... Unfortunate, it would be nice if jQuery's selector returned elements in the order they are specified.Midweek
@Endophage. It was better if you could add a parameter to the selector to indicate the order.Whig
E
1

Callback is a friend, dont push it away. There are ways to simplify them. Here is one of them

$('div1').fadeOut('slow', div2)
function div3() { $('div3').fadeOut('slow'); }
function div2() { $('div2').fadeOut('slow', div3); }
Eleonoreleonora answered 29/4, 2012 at 6:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.