2

I have an animation loop used to manipulate individual letters. It's wrapped in a timer in order to create a delayed offset. Each letter animates 100 ms later than the previous. I need to figure out how I can tell when the full animation is complete but I'm having some trouble because of the different types of nesting in use.

I've tried a few different things including trying to return a value from the animation, then the timer, and then the $.each function, but I'm sure this is off. I also was thinking I might be able to use the promise provided by jQuery's animate function, but not sure exactly how to implement this. Any advice here would be appreciated :] thank you

Here is my current code:

var offset = 200;
//drop individual letters down out of view
    function dropLetters($letters){

        var len = $letters.length - 1;

        $letters.each(function(i){

            var $letter = $(this);

            setTimeout(function(){

                $letter.animate({ top: offset + 'px' }, 300, function(){
                    if( i >= len ){
                        return $(this).promise();
                    }
                });

            }, 100 * i );

        });

    }

Edit: Sorry I realize I have omitted the offset variable. I added this back - it was just set to a value of 200.

Also, I realize this question is similar to another, but it also seems to differ from it. The answers provided here give a couple of different approaches that aren't present in the other question.

sm1215
  • 437
  • 2
  • 14
  • Possible duplicate of [Jquery - defer callback until multiple animations are complete](http://stackoverflow.com/questions/5220878/jquery-defer-callback-until-multiple-animations-are-complete) – nem035 Mar 04 '16 at 22:39
  • @nem That example does not show a good solution because the OP is calling `setTimeout` around the call to `animate` – Ruan Mendes Mar 04 '16 at 22:55

4 Answers4

3

An approach utilizing .promise() $.when() , Function.prototype.apply(), $.map(), .delay(). Note substituted chaining .apply() to .promise() for $.when() to return this as jQuery object containing elements instead of array containing jQuery objects at .then()

var offset = 100, duration = 300, delay = 100, curr = 0;

function dropLetters(elems) {
  return $.fn.promise.apply(elems, $.map(elems, function(el) {
    return $(el).delay(curr += delay).animate({top: offset + "px"}, duration)
  }))
}

dropLetters($("button")).then(function() {
  console.log("complete", this)
})
button {
  position: relative;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js">
</script>
<button>
  0
</button>

<button>
  1
</button>

<button>
  2
</button>

Alternatively, using $.when() , .queue()

var offset = 100, duration = 300, delay = 100, curr = 0;

function dropLetters(elems) {
  return $.when(elems.queue(function (next) { 
    $(this).delay(curr += delay).animate({top:offset + "px"}, duration, next())}))
}

dropLetters($("button")).then(function() {
  console.log("complete", this)
})
button {
  position: relative;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js">
</script>
<button>
  0
</button>

<button>
  1
</button>

<button>
  2
</button>
guest271314
  • 1
  • 15
  • 104
  • 177
  • There it is! A much better looking jQuery solution :) I still prefer to use jQuery only when necessary ;) – Ruan Mendes Mar 04 '16 at 23:23
  • @JuanMendes Have one even briefer – guest271314 Mar 04 '16 at 23:27
  • The suspense is killing me! – Ruan Mendes Mar 04 '16 at 23:28
  • This is not quite what the OP wants, each letter waits for each animation to finish before the next button drops. See my example – Ruan Mendes Mar 04 '16 at 23:29
  • Yes, you mentioned that earlier. The animations at OP does this as well; or appears to from this vantage . `i + duration` will always be `duration` more than previous iteration . Also , linked Question approach should return expected results when substituting `.delay()` for `setTimeout` – guest271314 Mar 04 '16 at 23:31
  • @JuanMendes https://jsfiddle.net/ukqceuL7/1/ . Also possible using `.done()`, `.ready()`; though they each could have issues with chaining `.then()` outside of array of functions passed as parameter being called before promises in array completes – guest271314 Mar 04 '16 at 23:44
  • Very nice, looks even better – Ruan Mendes Mar 04 '16 at 23:51
  • Thank you, this is a totally different approach that I wasn't familiar with and it works out well – sm1215 Mar 07 '16 at 16:11
2

You can make a promise out of each setTimeout call and you don't need to track by yourself that all the async operations have finished.

function dropLetters($letters){
  var promises = [];
  $letters.each(function(i){
    var $letter = $(this);
    promises.push(new Promise(function(resolve, reject) {
      setTimeout(function(){
        $letter.animate({ top: offset + 'px' }, 300, function(){
            resolve();
        });
      }, 100 * i );
    });               
  });
  return Promise.all(promises);
}

Note that you may need a Promise polyfill See http://caniuse.com/#feat=promises

One of the benefits of my answer over the existing ones that check index is that you would have to modify the code in two places if you changed the animation to be backwards. See my version below where the letters fly out backwards (and forward).

function dropLetters($letters, backwards) {
  var promises = [];
  $letters.each(function(i) {
    var $letter = $(this);
    promises.push(new Promise(function(resolve, reject) {
      setTimeout(function() {
        $letter.animate({
          top: '-100px'
        }, 300, function() {
          resolve();
        });
      }, 100 * (backwards ? $letters.length - i : i));
    }));
  });
  return Promise.all(promises);
}

dropLetters($('p')).then(function() {
  alert('finished')
});
  
dropLetters($('span'), true).then(function() {
  alert('finished')
});
p, span {
  position: relative;
  float: left;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>
<p>A</p>

<hr style="clear: both"/>

<span>B</span>
<span>B</span>
<span>B</span>
<span>B</span>
<span>B</span>
<span>B</span>
<span>B</span>
<span>B</span>
<span>B</span>
<span>B</span>
<span>B</span>
<span>B</span>
<span>B</span>
<span>B</span>
<span>B</span>
<span>B</span>
Ruan Mendes
  • 90,375
  • 31
  • 153
  • 217
1

You could use $.Deferred() , .resolveWith()

function dropLetters($letters) {

  var len = $letters.length - 1;

  var dfd = $.Deferred();

  $letters.each(function(i) {

    var $letter = $(this);

    setTimeout(function() {

      $letter.animate({
        top: offset + 'px'
      }, 300, function() {
        if (i >= len) {
          dfd.resolveWith($(this));
        }
      });

    }, 100 * i);

  });

  return dfd.promise()

}
guest271314
  • 1
  • 15
  • 104
  • 177
  • @JuanMendes Yes, several ways to achieve expected results. Would generally utilize `.queue()` for such a process. Why use `Promise` constructor at each iteration ? No value appears set at `resolve()` ? Should `$letters` be resolved as `Promise` value ? Though was not certain if only last element should be resolved or collection `$letters` ; used last element at post – guest271314 Mar 04 '16 at 22:47
  • 1
    A new promise will still work even if they finish out of order (if the OP changes the code so that the first one takes longer than the last). – Ruan Mendes Mar 04 '16 at 22:51
  • @JuanMendes Above post could probably be re-written using `$.when()` , `.apply()` , `$.map()` or jQuery `.promise()` ; attempted to adjust original post as little as possible at present possible solution – guest271314 Mar 04 '16 at 22:55
  • My first answer also modified it as little possible. Then I realized that what the OP was doing was duplicating the work that `Promise` does for you. I wanted to stay away from jQuery specific syntax, I know the OP is using it, but I abhor using jQuery when standard solutions exist. I would enjoy seeing a nicer looking jQuery solution though – Ruan Mendes Mar 04 '16 at 22:59
  • JuanMendes _"I would enjoy seeing a nicer looking jQuery solution though"_ What is lacking using single `$.Deferred()` ? – guest271314 Mar 04 '16 at 23:00
  • You are still checking the index to make sure all have finished – Ruan Mendes Mar 04 '16 at 23:02
  • @JuanMendes _"You are still checking the index to make sure all have finished"_ See alternative approach at new Answer – guest271314 Mar 04 '16 at 23:19
1

As thers have stated you may use $.Deferred()` Just to illustrate i added an example based on given code. ;-)

//drop individual letters down out of view
    function dropLetters($letters){
        var deferred = jQuery.Deferred();
        
        $letters.each(function(i,elem){
            var $letter = $(elem);

            var timer = setTimeout(function(){        

                $letter.animate({ top: $letter.offset().top-100 }, 300, function(f){
                   if(i+1>=$letters.length){// last letter was animated
                       deferred.resolve();
                   }
                });
              
            }, 300*i );

        });
        return deferred;

    }
dropLetters($('p')).then(function(){alert('finished')});
p{
  position:relative;
  float:left;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p><p>A</p>
J.K.Lauren
  • 316
  • 1
  • 3
  • This is not an optimal solution, if you're using Promises, you should not track by yourself when the asynchronous operations finished – Ruan Mendes Mar 04 '16 at 23:01
  • How is your solution different from `js` at http://stackoverflow.com/a/35807369/ ? – guest271314 Mar 04 '16 at 23:01
  • @guest271314 it uses `resolve` and yours uses `resolveWith`? IMHO it's clearer (at least for me) but overall, pretty much the same thing. It also fails if you remove the letters backwards instead of forwards. Just saying... my answer works in both cases.... – Ruan Mendes Mar 04 '16 at 23:04
  • Good point reversing order would break functionallity. So i'm with @Juans answer. – J.K.Lauren Mar 04 '16 at 23:16
  • @JuanMendes Not following you here _" It also fails if you remove the letters backwards instead of forwards"_ – guest271314 Mar 04 '16 at 23:18
  • @guest271314 It will say it's finished after the first letter unless you change two places in the code. See my example – Ruan Mendes Mar 04 '16 at 23:32