Make angular.forEach wait for promise after going to next object
Asked Answered
C

7

18

I have a list of objects. The objects are passed to a deferred function. I want to call the function with the next object only after the previous call is resolved. Is there any way I can do this?

angular.forEach(objects, function (object) {
    // wait for this to resolve and after that move to next object
    doSomething(object);
});
Cistern answered 11/3, 2015 at 9:58 Comment(0)
C
27

Before ES2017 and async/await (see below for an option in ES2017), you can't use .forEach() if you want to wait for a promise because promises are not blocking. Javascript and promises just don't work that way.

  1. You can chain multiple promises and make the promise infrastructure sequence them.

  2. You can iterate manually and advance the iteration only when the previous promise finishes.

  3. You can use a library like async or Bluebird that will sequence them for you.

There are lots of different alternatives, but .forEach() will not do it for you.


Here's an example of sequencing using chaining of promises with angular promises (assuming objects is an array):

objects.reduce(function(p, val) {
    return p.then(function() {
        return doSomething(val);
    });
}, $q.when(true)).then(function(finalResult) {
    // done here
}, function(err) {
    // error here
});

And, using standard ES6 promises, this would be:

objects.reduce(function(p, val) {
    return p.then(function() {
        return doSomething(val);
    });
}, Promise.resolve()).then(function(finalResult) {
    // done here
}, function(err) {
    // error here
});

Here's an example of manually sequencing (assuming objects is an array), though this does not report back completion or errors like the above option does:

function run(objects) {
    var cntr = 0;

    function next() {
        if (cntr < objects.length) {
            doSomething(objects[cntr++]).then(next);
        }
    }
    next();
}

ES2017

In ES2017, the async/wait feature does allow you to "wait" for a promise to fulfill before continuing the loop iteration when using non-function based loops such as for or while:

async function someFunc() {
    for (object of objects) {
        // wait for this to resolve and after that move to next object
        let result = await doSomething(object);
    }
}

The code has to be contained inside an async function and then you can use await to tell the interpreter to wait for the promise to resolve before continuing the loop. Note, while this appears to be "blocking" type behavior, it is not blocking the event loop. Other events in the event loop can still be processed during the await.

Cyanogen answered 11/3, 2015 at 10:8 Comment(6)
Your "reduce" solution is elegant, but it doesn't quite work. $q.defer().resolve() resolves a promise, but returns undefined, so p is undefined for the first element.Trin
@Trin - If you know what to suggest, please propose an edit to my answer. I don't keep up with all the different proprietary promise libraries (when there's an ES6 standard for all this now) so if this isn't the correct way to create a resolved promise ion angularjs to start the chain, then I'm not sure what it should be..Cyanogen
elegant and clean solutionIminourea
the initial value $q.dever().resolve()did not work for me (Angular 1.4.3), p was undefined on the first call. But this did work: $q(function(resolve) { resolve();})Shamrock
@Artjom - In Angular 1.5.1, the doc says that .resolve() returns a promise. Perhaps this was added after 1.4.3? In any case, your method works fine too. Also, it needs to be $q.defer().resolve() - you spelled it differently in your comment. I think you could also use $q.when(true).Cyanogen
Added info about using async/await in ES2017.Cyanogen
Z
13

Yes you can use angular.forEach to achieve this.

Here is an example (assuming objects is an array):

// Define the initial promise
var sequence = $q.defer();
sequence.resolve();
sequence = sequence.promise;

angular.forEach(objects, function(val,key){
    sequence = sequence.then(function() {
        return doSomething(val);
    });
});

Here is how this can be done using array.reduce, similar to @friend00's answer (assuming objects is an array):

objects.reduce(function(p, val) {
    // The initial promise object
    if(p.then === undefined) {
        p.resolve(); 
        p = p.promise;
    }
    return p.then(function() {
        return doSomething(val);
    });
}, $q.defer());
Zwart answered 12/1, 2016 at 16:14 Comment(1)
you sir are very right! this should be the accepted answer. you CAN use angular.forEach to achieve this.Bellicose
S
4

check $q on angular:

function outerFunction() {

  var defer = $q.defer();
  var promises = [];

  function lastTask(){
      writeSome('finish').then( function(){
          defer.resolve();
      });
  }

  angular.forEach( $scope.testArray, function(value){
      promises.push(writeSome(value));
  });

  $q.all(promises).then(lastTask);

  return defer;
}
Sheepshank answered 11/3, 2015 at 10:8 Comment(1)
This looks to me like it runs all the operations in parallel and uses q.all() to know when they are all done. I though the OP asked how to sequenc the operations one at a time.Cyanogen
S
2

The easiest way is to create a function and manually iterate over all the objects in the array after each promise is resolved.

var delayedFORLoop = function (array) {
    var defer = $q.defer();

    var loop = function (count) {
        var item = array[count];

        // Example of a promise to wait for
        myService.DoCalculation(item).then(function (response) {

        }).finally(function () {
          // Resolve or continue with loop
            if (count === array.length) {
                defer.resolve();
            } else {
                loop(++count);
            }
        });
    }

    loop(0); // Start loop
    return defer.promise;
}

// To use:
delayedFORLoop(array).then(function(response) {
    // Do something
});

Example is also available on my GitHub: https://github.com/pietervw/Deferred-Angular-FOR-Loop-Example

Syck answered 13/5, 2016 at 6:55 Comment(0)
G
1

I use a simple solution for a connection to a printer that wait till the promise is over to go to the next.

angular.forEach(object, function(data){
    yourFunction(data)
    .then(function (){
        return;
    })
})
Guimond answered 29/11, 2018 at 12:18 Comment(0)
P
0

It might help someone as I tried several of above solution before coming up with my own that actually worked for me (the other ones didn't)

  var sequence;
  objects.forEach(function(item) {
     if(sequence === undefined){
          sequence = doSomethingThatReturnsAPromise(item)
          }else{
          sequence = sequence.then(function(){
               return doSomethingThatReturnsAPromise(item)
                     }); 
                 }
        });
Periodicity answered 26/1, 2017 at 17:45 Comment(0)
V
0

It worked for me like this. I don't know if it is a right approach but could help to reduce lines

function myFun(){
     var deffer = $q.defer();
     angular.forEach(array,function(a,i) { 
          Service.method(a.id).then(function(res) { 
               console.log(res); 
               if(i == array.length-1) { 
                      deffer.resolve(res); 
               } 
          }); 
     });
     return deffer.promise;
}

myFun().then(function(res){
     //res here
});
Venita answered 14/6, 2017 at 10:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.