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?

gdoron
  • 147,333
  • 58
  • 291
  • 367
Yuval Karmi
  • 26,277
  • 39
  • 124
  • 175

7 Answers7

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

kangax
  • 38,898
  • 13
  • 99
  • 135
Julian Aubourg
  • 11,346
  • 1
  • 29
  • 29
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

xgc1986
  • 868
  • 6
  • 8
  • @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 // 7` – xgc1986 Apr 29 '12 at 11:11
  • 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)? – jfriend00 Apr 29 '12 at 18:22
  • 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 on – xgc1986 Apr 30 '12 at 13:16
  • 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. – jfriend00 Apr 30 '12 at 14:43
  • 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. – xgc1986 Apr 30 '12 at 20:20
  • 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. – Zhanger Sep 01 '13 at 18:50
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/

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • 1
    LOL, I thought the concept was to make it simple – Starx Apr 29 '12 at 08:29
  • 2
    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. – jfriend00 Apr 29 '12 at 08:31
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/

Tatu Ulmanen
  • 123,288
  • 34
  • 187
  • 185
  • 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. – jfriend00 Apr 29 '12 at 07:31
1

try something like:

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

Adjust the timing as necessary

  • 1
    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. – Brad Apr 29 '12 at 06:53
  • @Brad -- 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. –  Apr 29 '12 at 06:55
  • 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 out](http://stackoverflow.com/a/10370321/601179) – gdoron Apr 29 '12 at 07:12
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

Community
  • 1
  • 1
gdoron
  • 147,333
  • 58
  • 291
  • 367
  • But, this cannot guarantee the effect will complete in 1000 milliseconds. – Starx Apr 29 '12 at 07:02
  • @Starx. it is guaranteed now. check it out. – gdoron Apr 29 '12 at 07:16
  • Does the jquery selector guarantee the return order will be the order you put elements in the selector? Just curious... – Endophage Apr 29 '12 at 07:47
  • @Endophage. It will fade them in the order they are in the DOM. take a look on [this fiddle](http://jsfiddle.net/RwMy7/10/) – gdoron Apr 29 '12 at 08:08
  • @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? – Endophage Apr 30 '12 at 19:59
  • @Endophage. If you would only want to reverse the order it was easy, if it based on some logic it wouldn't be clean. – gdoron Apr 30 '12 at 20:01
  • @Endophage: The reverse option: `$($('.forHide').get().reverse()).each(function(){...});` – gdoron Apr 30 '12 at 20:32
  • @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. – Endophage Apr 30 '12 at 22:46
  • @Endophage. It was better if you could add a parameter to the selector to indicate the order. – gdoron Apr 30 '12 at 22:49
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); }
Starx
  • 77,474
  • 47
  • 185
  • 261