1

I'm currently trying to find a kind of best practice to make multiple calls to $resource in function of the return of a first one.

Consider the following schema : we have a database of authors, that have books, that have multiples tags (sf,thriller,horror,...).

angular.module('myApp',[ngResource])
    .controller('myCtrl',['$scope', function($scope){
        $scope.results = [];

        apiService.Authors.getAll().$promise                                         //return array of authors
            .then(function(authors){
                $scope.results.push(authors);
                angular.forEach(authors,function(akey,author){
                    apiService.BooksBy.getAll({authorID:author.id}).$promise         //return array of books
                        .then(function(books){
                            $scope.results[akey].books = [];
                            $scope.results[akey].books.push(books);
                            angular.forEach(books,function(bkey,book){
                                apiService.Tags.getAll({bookID:book.id}).$promise    //return array of tags
                                    .then(function(tags){
                                        $scope.results[akey].book[bkey].tags = [];
                                        $scope.results[akey].book[bkey].tags.push(tags);
                                    });
                            });
                        });
                });
            });
    }]);

Is this the only way to make multiple nested calls ?

I was thinking of something more flattened and more readable, something like a workaround with chained promises.

Any ideas would be welcomed, as I'm unable to find how to do this in a better way.

Regards !

Freezystem
  • 4,494
  • 1
  • 32
  • 35
  • Does your api return data with 'foreign keys' in it? That is, if you were to bundle a bunch of `Tags.getAll().$promise` 's into an array and pass to `$q.all([])`, you'd get an array of resolved results in the same order that you added the promises. However, if the data in each of those array elements didn't include a foreign key identifying which book the request was made for, it might still be tricky to correctly associate the results with the correct book if you're not nesting and tracking those keys in a closure... – JcT Jun 07 '15 at 15:55
  • unfortunately no. There is no foreign keys in the base. Thanks for your reply. – Freezystem Jun 07 '15 at 19:36

2 Answers2

0

You can make multiple calls by using $q. Its best way to call multiple calls and it ends if all calls are return promises

If you are calling multiple call based on call on previous result then its best way to do this.

function httpCall()
{
     var promises=[];
      promises.push(yourhttpCall1())
     return $q.all(promises);
}

$q.all([httpCall]).then(function(result){});
ReeganLourduraj
  • 863
  • 6
  • 9
  • actually this is one of the things I've tried with [$q.allSettled](http://www.codeducky.org/q-allsettled/). But syntactically the $q.all isn't chained with the initial promise. I wonder if there is a better way to keep code clear and all my promises chained. Maybe there is no way to do it differently, that's why I was asking. – Freezystem Jun 07 '15 at 19:44
0

Here is one possible solution: If you utilise $q.all, but for each promise you push to the array you add a little 'sub'-.then, you can wrap the result of the each individual api call and pass it along with the key (or whatever else) you require to be consumed by your next .then-handler.

I have tried to fairly closely mirror your original code example, with a few changes:

  • I've created a little mockApiService, but I've fudged a couple of the calls for simplicity
  • I've replaced the inline .then-handlers with named functions, to more clearly demonstrate that we've flattened out the chain
  • I'm using angular.extend to update a json structure on scope, which is being displayed within <pre> tags. Your original code sample was doing something with arrays, but I'm not sure it was going to work as intended - my use of angular.extend is meant to approximate what I thought you were going for.
  • In my applyBooksGetTags function, we're trying to pass both the akey and the bkey along with the result. However, the akey is actually gotten from the previous result - accessed as result.akey. This exists in scope outside the angular.forEach loop we're using to compile our promise array - and by the time the forEach function executes, result.akey will have been changed by the outside for loop. For this reason, I'm wrapping the call to forEach in a self-executing function - a closure, to ensure the value of result.akey is maintained by the time forEach does its thing.
  • The angular.forEach calls in the original sample may have swapped the order of the (value, key) params - so have swapped back in the below snippet
  • There are a lot of } //end blah blah comments... this is mostly because the snippet editor 'tidy' function made the code a little tricky to follow otherwise; sorry!

(function() {
  "use strict";

  angular.module('myApp', [])
    .controller('myCtrl', ['$scope', '$q', 'apiService', MyCtrl])
    .service('apiService', ['$timeout', mockApiService]);

  function MyCtrl($scope, $q, apiService) {
      $scope.results = {};

      apiService.Authors.getAll().$promise
        .then(applyAuthorsGetBooks)
        .then(applyBooksGetTags)
        .then(applyTags);

      function applyAuthorsGetBooks(authors) {
          var promises = [];
          angular.extend($scope.results, authors);

          angular.forEach(authors, function(author, akey) {
            promises.push(apiService.BooksBy.getAll({
                authorID: author.id
              }).$promise
              .then(function(books) {
                return $q.when({ // < MAGIC HERE: return the books, but also the aKey
                  books: books,
                  akey: akey
                }); //end return
              }) //end then
            ); //end push
          }); //end foreach

          return $q.all(promises);
        } // end authorsGetBooks

      function applyBooksGetTags(results) {
          var promises = [];

          for (var i = 0; i < results.length; i++) {
            var result = results[i];
            $scope.results[result.akey].books = angular.extend({}, result.books);

            (function(akey) { //anon func to wrap the current value of akey in a closure, so when inner loop accesses it hasn't been changed by outer loop
              angular.forEach(result.books, function(book, bkey) {
                promises.push(apiService.Tags.getAll({
                    bookID: book.id
                  }).$promise
                  .then(function(tags) {

                    return $q.when({ // < MAGIC HERE, again: return the tags, but also the bkey and akey
                      tags: tags,
                      akey: akey,
                      bkey: bkey
                    }); //end return
                  }) //end then
                ); //end push

              }); //end foreach
            })(result.akey) //pass our current akey value to our anon func

          } //end for
          return $q.all(promises);
        } //end booksGetTags

      function applyTags(results) {
        for (var i = 0; i < results.length; i++) {
          var result = results[i];
          $scope.results[result.akey].books[result.bkey].tags = angular.extend({}, result.tags);
        } //end for
      }

    } //end MyCtrl

  function mockApiService($timeout) {
      function _simulateResource(data) {
          return {
            $promise: $timeout(function timeoutHandler() {
                return data;
              }, 1000) //end $timeout
          }; //end return
        } //end _simulateResource()

      return {
        Authors: {
          getAll: function authorsGetAll() {
              return _simulateResource({
                Author1: {
                  id: 'Author 1'
                },
                Author2: {
                  id: 'Author 2'
                }
              }); //end return
            } //end getAll
        }, //end Authors
        BooksBy: {
          getAll: function booksByGetAll(params) {
              var authorId = params.authorID;
              return _simulateResource({
                Book1: {
                  id: 'Book 1 by ' + authorId
                },
                Book2: {
                  id: 'Book 2 by ' + authorId
                }
              }); //end return
            } //end getAll
        }, //end BooksBy
        Tags: {
          getAll: function tagsGetAll(params) {
              var bookId = params.bookID;
              return _simulateResource({
                Rom: {
                  id: 'Rom'
                },
                Zom: {
                  id: 'Zom'
                },
                Com: {
                  id: 'Com'
                }
              }); //end return
            } //end getAll
        } //end Tags
      }; //end return
    } // end MockService API
})();
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.28/angular.min.js"></script>
<div ng-app="myApp">
  <pre ng-controller='myCtrl'>{{results|json}}</pre>
</div>
JcT
  • 3,539
  • 1
  • 24
  • 34
  • Regarding the closure inside the nested loops, you can see here for further info (although I haven't exactly solved it in the most highly recommended fashion!): http://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example – JcT Jun 10 '15 at 15:37
  • Finally this is exactly what I have done in my script even before having seen this answer. Thanks for this precise reply, I think this is the better way for now to keep things flat. – Freezystem Jun 10 '15 at 17:49