Dan Kanze
Dan Kanze

Reputation: 18595

callback after async forEach AngularJS

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')
    }
});

Upvotes: 20

Views: 50997

Answers (4)

Dan Kanze
Dan Kanze

Reputation: 18595

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);
});

Upvotes: 4

Mark Coleman
Mark Coleman

Reputation: 40863

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.

Upvotes: 45

Rajkamal Subramanian
Rajkamal Subramanian

Reputation: 6944

$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.

Upvotes: 5

user554180
user554180

Reputation:

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

Upvotes: 0

Related Questions