2

Ok, this is a re-posting of a previous question which I'd confused by over-simplifying the code...

I have an angularjs factory function which queries a parse database, and returns a promise ... so far, pretty simple... however, my controller has a foreach loop which calls the function for each of a range of ids, which are meant to then resolve concurrently. The resolve doesn't work, however, and what I get back appears to be a garbled mixture of the api calls.

Here's a simplified version of my code...

app.factory('MyFactory', function($http, $q ) {
  return {
    testFunction: function (ingredientList) {

      var deferred = $q.defer()

      ingredientIndex=-1

      var regulariseIngredient=function() {

        ingredientIndex++

        if (ingredientIndex>ingredientList.length-1) {
          deferred.resolve(ingredientList);
          return
        }

        (new Parse.Query("Ingredient"))
        .equalTo("type", ingredientList[ingredientIndex].type)
        .equalTo("name", ingredientList[ingredientIndex].name)
        .include("parent")
        .find().then(function(result) {
          result=result[0]
          if(result.get("parent")) result=result.get("parent")                        
          ingredientList[ingredientIndex]=result
          regulariseIngredient();
         })                               

       }
       regulariseIngredient()
       return deferred.promise
    }
  }
}

app.controller('TestController', function($scope,MyFactory) {
  recipes = [ An array of objects, each with an ingredients array]
  angular.forEach(recipes, function(recipe) { 
    MyFactory.testFunction(recipe.ingredients).then(function(result) {
      console.log(result)
    })
  })
} 

Bit more info on what the service actually does...

my app has a collection of recipes, and a collection of ingredients. Each ingredient has a 'parent' which substitutes it in the 'rationalised' version. The test function loops through each ingredient, compiles the outcome and then returns a collection of substituted ingredients. The controller is looping through the recipes, so that I can check the ingredients on hand of all recipes.

John Slegers
  • 45,213
  • 22
  • 199
  • 169
Mark Peace
  • 71
  • 2
  • 11
  • without a loop of list, nothing will change your `ingredientIndex`. If it does change you don't resolve based on results – charlietfl Aug 10 '14 at 11:47

2 Answers2

2

@MarkPeace, it appears that you have hit on the anti-pattern "outlawed" here under the heading "The Collection Kerfuffle".

The espoused pattern (for performing an asynchronous task, in parallel, on each member of an array of items) is :

function workMyCollection(arr) {
    return q.all(arr.map(function(item) {
        return doSomethingAsync(item);
    }));
}

In your case, doSomethingAsync() is (new Parse.Query("Ingredient"))...find();

In full, the code will be something like :

app.factory('MyFactory', function($http, $q) {
    return {
        testFunction: function (ingredientList) {
            return $q.all(ingredientList.map(function(item) {
                return (new Parse.Query("Ingredient"))
                    .equalTo("type", item.type)
                    .equalTo("name", item.name)
                    .include("parent")
                    .find().then(function(result) {
                        return result[0].get("parent") || result[0];//phew, that's a bit more concise
                    });
            }));
        }
    }
}); 

testFunction() will thus return a promise of an array of results (whatever they happen to be).

In the controller, you can use the same pattern again to loop through the recipes :

app.controller('TestController', function($scope, MyFactory) {
    var recipes = [ An array of objects, each with an ingredients array ];
    $q.all(recipes.map(function(recipe) {
        return MyFactory.testFunction(recipe.ingredients).then(function(results) {
            console.dir(results);
            return results;
        });
    })).then(function(arr) {
        //all done-and-dusted
        //If everything has been successful, arr is an array of arrays of results
        //Do awesome stuff with the data.
    })['catch'](function(err) {
        //report/handle error here
    });
});

Of course, exactly how much you do in the controller and how much in the factory (or factories) is up to you. Personally, I would choose to keep the controller somewhat simpler.

Roamer-1888
  • 19,138
  • 5
  • 33
  • 44
  • Thanks for this, it's useful - and feels like its heading in the right direction (and helping me better understand conciseness in angular). I'm having a bit of a problem with 'spread' ... it seems to be generating this exception: Uncaught TypeError: Object # has no method 'spread' ... is there something else I'm missing? – Mark Peace Aug 11 '14 at 08:11
  • I confess to not really being an Angular person, just someone who understands javascript in the round. My understanding was that `.spread` was available but from what you say, it would appear that I'm wrong. The selected answer [here](http://stackoverflow.com/questions/23984471/how-do-i-use-bluebird-with-angular), by @BenjaminGruenbaum, allows you to use Bluebird promises, with complete integration with Angular's digest cycle. Then you will definitely have a `.spread` method. How on earth Benjamin worked out the integration of Bluebird into Angular is anybody's guess - and only 7 lines! – Roamer-1888 Aug 11 '14 at 08:37
  • If you report back here that Bluebird integration works, then I'll edit the answer. – Roamer-1888 Aug 11 '14 at 08:39
  • Hurray - working! Thanks for your help (p.s. Bluebird integration didn't work - though I didn't persist with it and, as you say, I don't need it) – Mark Peace Aug 11 '14 at 13:01
1

Using $q.all() which takes an array of promises as argument and returns a new promise which is:

  • resolved if all promises of the array are resolved
  • rejected if any of the promise of the array is rejected

from the doc:

Returns a single promise that will be resolved with an array/hash of values, each value corresponding to the promise at the same index/key in the promises array/hash. If any of the promises is resolved with a rejection, this resulting promise will be rejected with the same rejection value.

For the loop, using Array.forEach is cleaner:

testFunction:     function (ingredientList) {
    var promiseArray = [];

    ingredientList.forEach(function (ingredient) {
        var promise = (new Parse.Query("Ingredient"))
                .equalTo("type", ingredient.type)
                .equalTo("name", ingredient.name)
                .include("parent")
                .find().then(function(result) {
                    result = result[0]
                    if(result.get("parent")) {
                        result = result.get("parent");
                    }
                    return result;
                });
        promiseArray.push(promise);
    });

    return $q.all(promiseArray);
}
apairet
  • 3,162
  • 20
  • 23
  • I've tried doing this, resolving a promise array of calls before passing back the original deferred promise object, but I'm still getting the tangled mess of responses when multiple calls have been made – Mark Peace Aug 10 '14 at 20:05