111

Since I added some scrollTop-animation, some parts of my callback get called twice:

$('html, body').animate({scrollTop: '0px'}, 300,function() {
    $('#content').load(window.location.href, postdata, function() {                 
        $('#step2').addClass('stepactive').hide().fadeIn(700, function() {
            $('#content').show('slide',800);                    
        });
    });
});

It only seems to repeat the .show(), at least I don't have the impression that the load() or the .fadeIn() get called a second time too. The .show() gets repeated as soon as it has finished for the first time. Setting the scrollTop animation-speed to 0 didn't help by the way!

I assume it has something to do with the animation-queue, but I can't figure out how to find a workaround and especially why this is happening.

Rory McCrossan
  • 331,213
  • 40
  • 305
  • 339
Anonymous
  • 3,679
  • 6
  • 29
  • 40

2 Answers2

178

To get a single callback for the completion of multiple element animations, use deferred objects.

$(".myClass").animate({
  marginLeft: "30em"
}).promise().done(function(){
  alert("Done animating");
});

When you call .animate on a collection, each element in the collection is animated individually. When each one is done, the callback is called. This means if you animate eight elements, you'll get eight callbacks. The .promise().done(...) solution instead gives you a single callback when all animations on those eight elements are complete. This does have a side-effect in that if there are any other animations occurring on any of those eight elements the callback won't occur until those animations are done as well.

See the jQuery API for detailed description of the Promise and Deferred Objects.

Kevin B
  • 94,570
  • 16
  • 163
  • 180
  • This is solved problem about what we want to do on finished the animation but issue about twice call and got +1 for that, but i don't know if animate process is called twice or not?! – QMaster May 01 '14 at 11:17
  • 1
    The animate process isn't called twice, the callback is called once for each selected element. So, if you select 8 list items and animate them to the left, the callback will be executed 8 times. My solution gives you a single callback for when all 8 are finished. – Kevin B May 01 '14 at 14:11
  • @KevinB, good solution, and it helped resolved my own callback issues as well. Thanks. – lislis Mar 31 '15 at 18:05
  • I thought this was an intriguing answer, but unfortunately it has unintended consequences. The promise doesn't just delay callback execution until the elements of the jQuery set have finished the current animation (which would be great). It is resolved only after _all pending animations_ of the set are done. So if a second animation is set up while the first is still running, the callback intended for the first animation does not run until after the second animation is over. See [this bin](http://jsbin.com/haboso/2/edit?js,output). – hashchange May 12 '15 at 16:51
  • 1
    That is correct, it waits for *all current* animations to complete for the selected elements. It is not smart enough to only wait for the ones started in this chain(nor should it be) – Kevin B May 12 '15 at 16:52
  • I just got an upvote on my answer, was curious what it was, saw that it wasn't "getting" the reason for doing `html, body` and didn't mention `promise`, and added that. Then I saw your answer. Going back to edit in a link to yours from mine. – T.J. Crowder Feb 23 '16 at 14:22
173

animate calls its callback once for each element in the set you call animate on:

If supplied, the start, step, progress, complete, done, fail, and always callbacks are called on a per-element basis...

Since you're animating two elements (the html element, and the body element), you're getting two callbacks. (For anyone wondering why the OP is animating two elements, it's because the animation works on body on some browsers but on html on other browsers.)

To get a single callback when the animation is complete, the animate docs point you at using the promise method to get a promise for the animation queue, then using then to queue the callback:

$("html, body").animate(/*...*/)
    .promise().then(function() {
        // Animation complete
    });

(Note: Kevin B pointed this out in his answer when the question was first asked. I didn't until four years later when I noticed it was missing, added it, and...then saw Kevin's answer. Please give his answer the love it deserves. I figured as this is the accepted answer, I should leave it in.)

Here's an example showing both the individual element callbacks, and the overall completion callback:

jQuery(function($) {

  $("#one, #two").animate({
    marginLeft: "30em"
  }, function() {
    // Called per element
    display("Done animating " + this.id);
  }).promise().then(function() {
    // Called when the animation in total is complete
    display("Done with animation");
  });

  function display(msg) {
    $("<p>").html(msg).appendTo(document.body);
  }
});
<div id="one">I'm one</div>
<div id="two">I'm two</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
Community
  • 1
  • 1
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • 2
    Yeah that was the reason and actually I can't recall why I ever wrote `html, body`. Time to turn my brain back on. Thanks – Anonymous Jan 09 '12 at 15:34
  • 21
    Probably because animating just html won't work in Webkit, and just body won't work in Opera. Using both will ensure it always works, but will trigger the callback twice in Firefox. (I may have gotten the browsers wrong...) – Jon Gjengset Jun 05 '12 at 15:09
  • 2
    `html` is required for IE, and the callback will fire twice for most other browsers. I'm trying to work a solution to the same problem. – Simon Robb Jul 07 '13 at 11:58
  • 9
    I dealt with it by creating a flag: `var ranOne = false; $('body,html').animate({ scrollTop: scrollTo }, scrollTime, 'swing', function () { if (ranOne) { ...action... ranOne = false; } else { ranOne = true; } });` It feels hacky, but having to use "body,html" is kind of hacky in the first place, so. (ech sorry for lack of line breaks, guess comments don't show them) – spinn Aug 12 '13 at 14:03
  • 1
    Unbelievable! I spent hours trying to make my scrolling animation work properly with $("html,body") so that it wouldn't fire twice, and this answer is what saved me! Thanks! – Vasily Hall Oct 24 '16 at 17:58