Invoking a jQuery function after .each() has completed
Asked Answered
N

13

205

In jQuery, is it possible to invoke a callback or trigger an event after an invocation of .each() (or any other type of iterative callback) has completed.

For example, I would like this "fade and remove" to complete

$(parentSelect).nextAll().fadeOut(200, function() {
    $(this).remove();
});

before doing some calculations and inserting new elements after the $(parentSelect). My calculations are incorrect if the existing elements are still visible to jQuery and sleeping/delaying some arbitrary amount of time (200 for each element) seems like a brittle solution at best.

I can easily .bind() the necessary logic to an event callback but I'm not sure how to cleanly invoke the .trigger() after the above iteration has completed. Obviously, I can't invoke the trigger inside the iteration as it would fire multiple times.

In the case of $.each(), I've considered adding something to the end of the data argument (that I'd manually look for in the iteration body) but I'd hate to be forced to that so I was hoping there was some other elegant way to control the flow with respect to iterative callbacks.

Noenoel answered 1/3, 2010 at 18:48 Comment(3)
Do I correctly understand that it's not so much the ".each()" itself that you want to finish, but rather all the animations launched by the ".each()" call?Washroom
That is a good point. With this particular question, yes, I am mainly concerned about the completion of ".each()" itself ... but I think you raise another viable question.Noenoel
There is lightweight package for this github.com/ACFBentveld/AwaitSpaceport
W
175

An alternative to @tv's answer:

var elems = $(parentSelect).nextAll(), count = elems.length;

elems.each( function(i) {
  $(this).fadeOut(200, function() { 
    $(this).remove(); 
    if (!--count) doMyThing();
  });
});

Note that .each() itself is synchronous — the statement that follows the call to .each() will be executed only after the .each() call is complete. However, asynchronous operations started in the .each() iteration will of course continue on in their own way. That's the issue here: the calls to fade the elements are timer-driven animations, and those continue at their own pace.

The solution above, therefore, keeps track of how many elements are being faded. Each call to .fadeOut() gets a completion callback. When the callback notices that it's counted through all of the original elements involved, some subsequent action can be taken with confidence that all of the fading has finished.

This is a four-year-old answer (at this point in 2014). A modern way to do this would probably involve using the Deferred/Promise mechanism, though the above is simple and should work just fine.

Washroom answered 1/3, 2010 at 19:9 Comment(3)
I like this change, though I'd make the comparison to 0 instead of the logical negation (--count == 0) since you're counting down. To me it makes the intent clearer, though it has the same effect.Cladophyll
As it turns out, your initial comment on the question was spot on. I was asking about .each() and thought that is what I wanted but as you and tvanfosson and now patrick have pointed out - it was the final fadeOut that I was actually interested in. I think we all agree that your example (counting instead of indexes) is probably the safest.Noenoel
@A.DIMO What do you mean by, "too complicated"? What part is complicated?Washroom
S
206

It's probably to late but i think this code work...

$blocks.each(function(i, elm) {
 $(elm).fadeOut(200, function() {
  $(elm).remove();
 });
}).promise().done( function(){ alert("All was done"); } );
Scourge answered 20/1, 2012 at 12:17 Comment(3)
since when each returns a promise ? please check: api.jquery.com/eachEngvall
.promise() returns the promise. This solution worked for me.Mallorca
Does not work for me. The alert appear before the loop is done executing its content.Cultus
W
175

An alternative to @tv's answer:

var elems = $(parentSelect).nextAll(), count = elems.length;

elems.each( function(i) {
  $(this).fadeOut(200, function() { 
    $(this).remove(); 
    if (!--count) doMyThing();
  });
});

Note that .each() itself is synchronous — the statement that follows the call to .each() will be executed only after the .each() call is complete. However, asynchronous operations started in the .each() iteration will of course continue on in their own way. That's the issue here: the calls to fade the elements are timer-driven animations, and those continue at their own pace.

The solution above, therefore, keeps track of how many elements are being faded. Each call to .fadeOut() gets a completion callback. When the callback notices that it's counted through all of the original elements involved, some subsequent action can be taken with confidence that all of the fading has finished.

This is a four-year-old answer (at this point in 2014). A modern way to do this would probably involve using the Deferred/Promise mechanism, though the above is simple and should work just fine.

Washroom answered 1/3, 2010 at 19:9 Comment(3)
I like this change, though I'd make the comparison to 0 instead of the logical negation (--count == 0) since you're counting down. To me it makes the intent clearer, though it has the same effect.Cladophyll
As it turns out, your initial comment on the question was spot on. I was asking about .each() and thought that is what I wanted but as you and tvanfosson and now patrick have pointed out - it was the final fadeOut that I was actually interested in. I think we all agree that your example (counting instead of indexes) is probably the safest.Noenoel
@A.DIMO What do you mean by, "too complicated"? What part is complicated?Washroom
S
157

Ok, this might be a little after the fact, but .promise() should also achieve what you're after.

Promise documentation

An example from a project i'm working on:

$( '.panel' )
    .fadeOut( 'slow')
    .promise()
    .done( function() {
        $( '#' + target_panel ).fadeIn( 'slow', function() {});
    });

:)

Surah answered 21/12, 2011 at 2:17 Comment(1)
.promise() is not available on .each()Uncap
S
29

JavaScript runs synchronously, so whatever you place after each() will not run until each() is complete.

Consider the following test:

var count = 0;
var array = [];

// populate an array with 1,000,000 entries
for(var i = 0; i < 1000000; i++) {
    array.push(i);
}

// use each to iterate over the array, incrementing count each time
$.each(array, function() {
    count++
});

// the alert won't get called until the 'each' is done
//      as evidenced by the value of count
alert(count);

When the alert is called, count will equal 1000000 because the alert won't run until each() is done.

Slogan answered 1/3, 2010 at 20:5 Comment(4)
The problem in the example given is that the fadeOut gets put on the animation queue and doesn't execute synchronously. If you use an each and schedule each animation separately, you still have to fire the event after the animation, not the each finishes.Cladophyll
@Cladophyll - Yeah, I know. According to what Luther wants to accomplish, your solution (and Pointy's) seems like the right approach. But from Luther's comments like... Naively, I'm looking for something like elems.each(foo,bar) where foo='function applied to each element' and bar='callback after all iterations have completed'. ...he seems insistent that it is an each() issue. That's why I threw this answer into the mix.Slogan
You are correct Patrick. I did (mis)understand that .each() was scheduling or queuing my callbacks. I think that invoking fadeOut was indirectly leading me to that conclusion. tvanfosson and Pointy have both provided answers for the fadeOut problem (which is what I was really after) but your post actually corrects my understanding a bit. Your answer actually best answers the original question as stated while Pointy and tvanfosson answered the question I was trying to ask. I wish I could pick two answers. Thanks for 'throwing this answer into the mix' :)Noenoel
@Slogan I thought functions inside each were not guaranteed to be called before alert. could you explain or point me to the reading on this?Marienbad
W
13

I'm using something like this:

$.when(
           $.each(yourArray, function (key, value) {
                // Do Something in loop here
            })
          ).then(function () {
               // After loop ends.
          });
Wooer answered 20/4, 2020 at 9:17 Comment(2)
I get TypeError: $(...).each(...).then is not a functionBarnaba
That's because the then() function is set for $.when().then(), not as $.each().then @BarnabaFerreira
O
5

I found a lot of responses dealing with arrays but not with a json object. My solution was simply to iterate through the object once while incrementing a counter and then when iterating through the object to perform your code you can increment a second counter. Then you simply compare the two counters together and get your solution. I know it's a little clunky but I haven't found a more elegant solution so far. This is my example code:

var flag1 = flag2 = 0;

$.each( object, function ( i, v ) { flag1++; });

$.each( object, function ( ky, val ) {

     /*
        Your code here
     */
     flag2++;
});

if(flag1 === flag2) {
   your function to call at the end of the iteration
}

Like I said, it's not the most elegant, but it works and it works well and I haven't found a better solution just yet.

Cheers, JP

Otherdirected answered 1/3, 2011 at 17:15 Comment(1)
No this doesn't work. It fires all the .each's and then the final thing and everything runs at the same time. Try putting a setTimeout on your .each's and you'll see it just runs the flag1===flag2 right awayUncap
C
1

If you're willing to make it a couple of steps, this might work. It's dependent on the animations finishing in order, though. I don't think that should be a problem.

var elems = $(parentSelect).nextAll();
var lastID = elems.length - 1;

elems.each( function(i) {
    $(this).fadeOut(200, function() { 
        $(this).remove(); 
        if (i == lastID) {
           doMyThing();
        }
    });
});
Cladophyll answered 1/3, 2010 at 19:2 Comment(2)
This would work - although, not what I'd hoped. Naively, I'm looking for something like elems.each(foo,bar) where foo='function applied to each element' and bar='callback after all iterations have completed'. I'll give the question a bit more time to see if we come across some dark corner of jQuery :)Noenoel
AFAIK - you have to trigger it when the "last" animation completes and since they run asynchronously, you'd have to do it in the animation callback. I like @Pointy's version of what I wrote better since it doesn't have the dependency on the ordering, not that I think that would be a problem.Cladophyll
P
0

what about

$(parentSelect).nextAll().fadeOut(200, function() { 
    $(this).remove(); 
}).one(function(){
    myfunction();
}); 
Prosenchyma answered 1/3, 2010 at 20:33 Comment(2)
This definitely invokes myfunction() once (and introduces me to another jQuery concept) but what I was looking for was a guarantee of 'when' it would run - not merely that it would run once. And, as Patrick mentions, that part turns out to be pretty easy. What I was really looking for was a way to invoke something after the last fadeOut logic ran which, I don't think .one() guarantees.Noenoel
I think that the chain does that - since the first part runs before the last part.Prosenchyma
L
0

You have to queue the rest of your request for it to work.

var elems = $(parentSelect).nextAll();
var lastID = elems.length - 1;

elems.each( function(i) {
    $(this).fadeOut(200, function() { 
        $(this).remove(); 
        if (i == lastID) {
            $j(this).queue("fx",function(){ doMyThing;});
        }
    });
});
Leicester answered 21/12, 2010 at 19:4 Comment(0)
E
0

I meet the same problem and I solved with a solution like the following code:

var drfs = new Array();
var external = $.Deferred();
drfs.push(external.promise());

$('itemSelector').each( function() {
    //initialize the context for each cycle
    var t = this; // optional
    var internal = $.Deferred();

    // after the previous deferred operation has been resolved
    drfs.pop().then( function() {

        // do stuff of the cycle, optionally using t as this
        var result; //boolean set by the stuff

        if ( result ) {
            internal.resolve();
        } else {
            internal.reject();
        }
    }
    drfs.push(internal.promise());
});

external.resolve("done");

$.when(drfs).then( function() {
    // after all each are resolved

});

The solution solves the following problem: to synchronize the asynchronous operations started in the .each() iteration, using Deferred object.

Elliellicott answered 5/3, 2017 at 9:42 Comment(1)
You're missing a closing bracket ) before: drfs.push(internal.promise()); at least I think its before..Uncap
S
0

Maybe a late response but there is a package to handle this https://github.com/ACFBentveld/Await

 var myObject = { // or your array
        1 : 'My first item',
        2 : 'My second item',
        3 : 'My third item'
    }

    Await.each(myObject, function(key, value){
         //your logic here
    });

    Await.done(function(){
        console.log('The loop is completely done');
    });
Spaceport answered 21/6, 2018 at 11:25 Comment(0)
M
0
var count = $(parentSelect).length;

$(parentSelect).each(function () {

   //do you task here

   if (!--count) {

     //do the cleanup task

     alert ('Reached the end of each loop')
   }

});
Metamer answered 4/3, 2022 at 8:51 Comment(0)
C
0

The easiest way I found was to convert the $.each to a regular for loop:

From this:

$(".vvp-item-tile").each(function(){
    await whatever($(this));
    console.log($(this));
});
console.log("for loop is completed"); //Does not wait.

To this:

var arrObj = $(".vvp-item-tile"); //Return an array of object
var obj = null;
for(let i = 0; i < arrObj.length; i++){
    obj = $(arrObj[i]);
    await whatever(obj);
    console.log(obj);
}
console.log("for loop is completed");
Cultus answered 23/1 at 19:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.