0

Working on an mobile cordova/angular project. Below is a simple service call:

this.getSomeData = function (businessId) {
    var deferred = $q.defer();
    var query = "SELECT * FROM Stuff";
    $cordovaSQLite.execute(db, query).then(function (res) {
        deferred.resolve(res.rows);
    }, function (err) {
        deferred.reject(err);
    });

    return deferred.promise;
};

The issue is simple:

for (var k = 0; k < count; k++) {

    myService.getSomeData($scope.model.stuff[k].id, k).then(function (data) {
        // whatever
    }
);

getSomeData is async, so by the time it returns, the k of the for cycle is far from correct.

I thought of passing k to the service method as a parameter:

for (var k = 0; k < count; k++) {

    myService.getSomeData($scope.model.stuff[k].id, k).then(function (data) {
        // whatever
    }
);

And change the service method accordingly:

this.getSomeData = function (id, index) {
    var deferred = $q.defer();
    var query = "SELECT * FROM Stuff";
    $cordovaSQLite.execute(db, query).then(function (res) {
        deferred.resolve(res.rows, index);
    }, function (err) {
        deferred.reject(err);
    });

    return deferred.promise;
};

But that second parameter is ignored and is always undefined.

How to overcome this?

chiapa
  • 4,362
  • 11
  • 66
  • 106
  • 2
    First of all, [avoid the deferred antipattern](http://stackoverflow.com/q/23803743/1048572) – Bergi Sep 06 '16 at 10:41
  • @JLRishe Why did you reopen this? – Bergi Sep 06 '16 at 10:45
  • @Bergi Because the question you indicated is not the answer to this problem. The answer is `$q.all()`. – JLRishe Sep 06 '16 at 10:45
  • 1
    @JLRishe: No, the problem with `k` in the callback is mitigated in your answer by using `map` where [the closure is created](http://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example) implicitly. This hasn't anything to do with `all`. – Bergi Sep 06 '16 at 10:46
  • @Bergi The code OP has provided doesn't have a closure over a loop variable. One may be present in the code OP hasn't shown us (the text does seem to suggest that), and while closure over a loop variable may be the root problem here, I think it is better to provide a solution that is idiomatic to promises rather than directing OP to cobble together a solution by solving the closure issue. – JLRishe Sep 06 '16 at 10:49

2 Answers2

0

It sounds like you're running into a problem called "closure over the loop variable" and this issue is discussed in detail here:

JavaScript closure inside loops – simple practical example

In your case, however, the clean solution is to combine Array#map with $q.all():

$q.all($scope.model.visits.map(function (stuff) {
    return myService.getSomeData(stuff.id);
})).then(function (results) {
    // results is an array of the results of all the calls to getSomeData() in the correct order
});

Also, as Bergi points out, avoid the deferred antipattern:

this.getSomeData = function (id) {
    var query = "SELECT * FROM Stuff";

    return $cordovaSQLite.execute(db, query).then(function (res) {
        return res.rows;
    });
};
Community
  • 1
  • 1
JLRishe
  • 99,490
  • 19
  • 131
  • 169
  • 1
    @chiapa Because that's how promises work, it's 4 lines shorter, and leaves less room for human error. – JLRishe Sep 06 '16 at 10:57
  • I did `promisesArray.push(myService.getSomeData($scope.model.stuff[k].id));` inside the for loop, so I am storing the values inside an array. Now, I want to access them once all of them ran - how? – chiapa Sep 06 '16 at 11:06
  • @chiapa: `$q.all` does that for you already. Have you checked its docs? – Bergi Sep 06 '16 at 11:11
  • I am trying to make it work but I get "TypeError: Object [object Array] has no method 'then'" - looks like it should return a promise like object, which has a `then` method – chiapa Sep 06 '16 at 12:46
  • @chiapa My answer was missing a parenthesis. It should be `$q.all($scope.model.visits.map(function (stuff) { return myService.getSomeData(stuff.id); }))` (two closing parentheses after the function), followed by `.then(...)` – JLRishe Sep 06 '16 at 13:02
0

This is how I got it working. I tried using @JLRishe's suggestion but it wouldn't work. Turns out, I managed to pass more than one parameter through to the service method and back to the controller as well (by building an object than contains as many parameters I need).

myService.getSomeData().then(
    function (stuff) {
        // whatever
    }
).then(function () {
    for (var i = 0; i < $scope.model.stuff.length; i++) {

        // HERE I SEND TWO PARAMETERS TO THE SERVICE METHOD
        myService.getSomeMoreData($scope.model.stuff[i].id, i).then(
            function (data) {
                // whatever
            }
        );
    }
});

this.getSomeMoreData = function (id, index) {
    var deferred = $q.defer();
    var query = "SELECT * FROM stuff";

    $cordovaSQLite.execute(db, query).then(function (res) {
        var moreStuff = [];

        for (var i = 0; i < res.rows.length; i++) {
            var junk = res.rows.item(i);
            moreStuff.push(junk);

        }

        // HERE I RESOLVE AN OBJECT INSTEAD OF TWO PARAMETERS
        deferred.resolve({
            moreStuff: moreStuff,
            index: index
        });

    }, function (err) {
        deferred.reject(err);
    });

    return deferred.promise;
};
chiapa
  • 4,362
  • 11
  • 66
  • 106