20

How do I add a callback function after an async forEach Loop?

Here is some better context:

$scope.getAlbums = function(user, callback) {
    $scope.albumsList.forEach(function (obj, i) {
        $scope.getAlbum(user, obj.id, function(value){
            $scope.albums.push(value);
        });
    });
    // callback(); ???
};

$scope.getAlbums('guy123', function(){
    // forEach loop is all done!
    console.log($scope.albums)
});

Controller:

.controller('Filters', ['$scope','Imgur', function($scope, Imgur) {

    $scope.getAlbum = function(user, id, callback) {
        Imgur.album.get({user:user, id:id},    
            function(value) {
                return callback(value.data);
            }
        );
    }

    $scope.getAlbums = function(user, callback) {
        // Callback function at end of this forEach loop?
        // Request is async...
        $scope.albumsList.forEach(function (obj, i) {
            $scope.getAlbum(user, obj.id, function(value){
                $scope.albums.push(value);
            });
        });
    };
)]};

Service:

.factory('Imgur', function($resource) {
    return {
        album : $resource('https://api.imgur.com/3/account/:user/album/:id')
    }
});
Dan Kanze
  • 18,485
  • 28
  • 81
  • 134

4 Answers4

45

As Andrew said usage of $q and the deferred object should allow you to accomplish your goal.

You want to use $q.all() This will make sure all of your promise objects are resolved and then you can call your call back in .then()

function MyCtrl($scope, $q, $http) {

    $scope.albumsList = [{
            id: 1,
            name: "11"
        }, {
            id: 2,
            name: "22"
        }
    ];
    $scope.albums = [];
    $scope.getAlbum = function(user, id, callback) {
        return $http.get("https://api.imgur.com/3/account/" + user + "/album/" + id).success(
            function(value) {
                return callback(value.data);
            }
        );
    }
    $scope.getAlbums = function (user, callback) {
        var prom = [];
        $scope.albumsList.forEach(function (obj, i) {
            prom.push($scope.getAlbum(user, obj.id, function(value){
                $scope.albums.push(value);
            }));
        });
        $q.all(prom).then(function () {
            callback();
        });
    };
    $scope.getAlbums('guy123', function () {
        alert($scope.albums.length);
    });
}

Example with this on jsfiddle

Works but not with $http calls

With the deferred object you gain access to a promise where you can change successive then() calls together. When you resolve the deferred object it will execute the foreach and then execute your call back function you supplied. I tried to simplify your example a bit further so it would work in jsfiddle.

function MyCtrl($scope, $http, $q) {

    $scope.albumsList = [{
        id: 1,
        name: "11"
    }, {
        id: 2,
        name: "22"
    }];
    $scope.albums = [];
    $scope.getAlbums = function (user, callback) {
        var deferred = $q.defer();
        var promise = deferred.promise;
        promise.then(function () {
            $scope.albumsList.forEach(function (obj, i) {
                $scope.albums.push(obj);
            });
        }).then(function () {
            callback();
        });
        deferred.resolve();
    };
    $scope.getAlbums('guy123', function () {
        alert($scope.albums.length);
    });
}

Example on jsfiddle

A bit more reading on deferred and promises.

Mark Coleman
  • 40,542
  • 9
  • 81
  • 101
  • This is exactly what I needed, however, when I try with external resource data --- it doesn't hold it's promise ;) ... Could you please provide an example with any API data like twitter? – Dan Kanze May 08 '13 at 00:39
  • Since you are using the `$resource` that also returns a promise as well, you will need to modify your code so it chains the promises together and calling `resolve()` starts up the whole mechanism. – Mark Coleman May 08 '13 at 01:05
  • See update with example on jsfiddle that does simulated ajax post. – Mark Coleman May 08 '13 at 01:41
  • I've implimented your solution and pushed latest changes to project @ https://github.com/gigablox/angular-imgur-gallery/blob/master/app/js/controllers.js however the code I added still does not defer execution of those `$resource` calls I'm afraid :/ ... Did I miss something? – Dan Kanze May 08 '13 at 16:06
  • I misread the api documentation, `$resource` does not in fact return a promise object at all, you will need to use `$http` so you can utilize `$q.all()` I read `$resource` is going to gain promise support but not until a later release. – Mark Coleman May 08 '13 at 16:22
  • Ahhh, I see. I'm going to give it a shot using the `$resource` promise support PR here: https://github.com/ashtuchkin/angular.js/commit/37dc1d3f74f81e9b0a4a7d9010bab8b266b49da3 Related: http://stackoverflow.com/questions/15531117/angularjs-1-1-3-resource-callback-error-and-success – Dan Kanze May 08 '13 at 16:45
  • That worked, you can see the new format in the answer I left if you're curious. Thanks for all your help! – Dan Kanze May 08 '13 at 20:23
5
$scope.getAlbums = function(user, callback) {

        var promiseArr = [];
        $scope.albumsList.forEach(function (obj, i) {
            var anHttpPromise = 
            $scope.getAlbum(user, obj.id, function(value){
                $scope.albums.push(value);
            });
            promiseArr.push(anHttpPromise);
        });

        $q.all(promiseArr).then(function(){
            // This callback function will be called when all the promises are resolved.    (when all the albums are retrived)      
        })
    };

    $scope.getAlbum = function(user, id, callback) {
        var anHttpPromise = Imgur.album.get({user:user, id:id},    
            function(value) {
                return callback(value.data);
            }
        );
        return anHttpPromise;
    }

In the above code:

  1. The getAlbum is made to return an promise.
  2. Collecting an promise for each iteration of the getAlbums list
  3. Once all the promises are collected, the promise array is passed to $q.all
  4. The $q.all method instead returns an final promise whose callback function will be triggered once all the promises in the array are resolved.
Rajkamal Subramanian
  • 6,884
  • 4
  • 52
  • 69
  • I've implimented your solution and pushed latest changes to project @ https://github.com/gigablox/angular-imgur-gallery/blob/master/app/js/controllers.js However it still does not defer promise. Mabye I missed something? – Dan Kanze May 08 '13 at 13:52
4

Using the $resource promise PR commit slated for 1.1.3, I was able to wrap $resource calls with $q and control the flow of their async behavoir.

$scope.getAlbum = function(user, id, callback) {
    var promise = Imgur.album.get({user:user, id:id}).$promise.then(
        function( value ){
            return callback(value.data);
        },
        function( error ){
            //Something went wrong!
        }
    )
    return promise;
}

$scope.getAlbums = function(user, callback) {
    var prom = [];
    $scope.albumsList.forEach(function (obj, i) {
        var promise =
        $scope.getAlbum(user, obj.id, function(value){
            $scope.albums.push(value);
        });
        prom.push(promise);

    });

    $q.all(prom).then(function () {
        callback();
    });
};

$scope.getAlbums(user, function(){
    // Totally works, bro.
    console.log($scope.albums);
});
Dan Kanze
  • 18,485
  • 28
  • 81
  • 134
0

It looks like deferred http://docs.angularjs.org/api/ng.$q and specifically chaining promises could be useful here.