7

The Setting: I want to have a service that multiple controllers can query for data pulled using $http. The initial solution was to use promises as suggested here.

The Problem: Each time a controller queries the service, the service then returns an $http promise, resulting in multiple queries that just pulls the same data from a remote server, over and over again.

A Solution: The service function returns either data or a promise like below. And it is up to the controller to check and act accordingly.

app.factory('myService', function($http) {
    var items = [];
    var myService = {
        getItems: function() {
            // if items has content, return items; otherwise, return promise.
            if (items.length > 0) {
                return items;
            } else {     
                var promise = $http.get('test.json').then(function (response) {
                    // fill up items with result, so next query just returns items.
                    for(var i=0;i<response.data.length;i++){
                        items.push(response.data[i]);
                    }
                    return items;
                });
                // Return the promise to the controller
                return promise;
            }
     };
     return myService;
});

So when a controller needs that data, the controller just does something like this:

app.controller('MainCtrl', function( myService,$scope) {
    var promiseOrData = myService.async();
    // Check whether result is a promise or data.
    if ( typeof promiseOrData.then === 'function'){
        // It's a promise.  Use then().
        promiseOrData.then( function(data ){
            $scope.data = data;
        });
    } else {
        // It's data.
        $scope.data = data;
    }
});

So the question is: Is there a better way of doing this? With many controllers, this method would have a lot of duplicate code. Ideally, the controllers will just query the service for data directly.

Thanks!

Community
  • 1
  • 1
RVC
  • 420
  • 1
  • 4
  • 12

2 Answers2

12

$http returns a promise, we can use that instead of creating a new one with $q. Once the promise is resolved, we can keep returning it.

.factory('myService', ['$http','$q', function($http, $q) {
    var items = [];
    var last_request_failed = true;
    var promise = undefined;
    return {
        getItems: function() {
            if(!promise || last_request_failed) {
                promise = $http.get('test.json').then(
                function(response) {
                    last_request_failed = false;
                    items = response.data;
                    return items;
                },function(response) {  // error
                    last_request_failed = true;
                    return $q.reject(response);
                });
            }
            return promise;
        },
    };
}])

In your controller:

myService.getItems().then( 
    function(data) { $scope.data = data; }
);
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • Nice work. Even better it reduces the amount of code needed in the controller. – Jonathan Palumbo Aug 09 '13 at 01:28
  • Any way to make this more DRY on the services side? I have several http endpoints that at I want to do this for and repeating this code for each service/http endpoint would get out of hand – Kyle Kochis Aug 26 '13 at 16:37
  • @KyleKochis, the only thing I've been able to do is create a single [HTTP interceptor](http://stackoverflow.com/a/11957760/215945) to catch HTTP errors for all of my services, so the above code no longer needs an error handler. – Mark Rajcok Aug 26 '13 at 17:22
  • @MarkRajcok would `var last_request_failed = false` be better? If it's declared as true then would a call to .getItems(), before $http.get().then() has run and passed a response to one of it's functions, call $http.get() again? – paulhhowells Oct 10 '13 at 10:31
  • @paulhhowells, if you have caching enabled, "If there are multiple GET requests for the same URL that should be cached using the same cache, but the cache is not populated yet, only one request to the server will be made and the remaining requests will be fulfilled using the response from the first request." -- [ref](http://docs.angularjs.org/api/ng.$http) So I think this is okay, but I'm not certain. – Mark Rajcok Oct 10 '13 at 15:27
  • I know this thread is really old, but the answer above was the closest I got to sorting my problem... only issue is, I want the .getItems() method to check if the $http request has been made in the last hour, if so to use the cached data, and if not, to run the request again. Can anyone help with this? – Paulos3000 May 24 '16 at 10:00
  • Controller should be `myService.getItems().then( function(data ){ $scope.data = data; });` instead of `$scope.data = myService.getItems();` right? – T J Jul 14 '16 at 11:16
  • 1
    @TJ, yes I will update. Originally, AngularJS templates could handle the promise, but at some point (I forget when), they deprecated that functionality. – Mark Rajcok Jul 14 '16 at 14:55
1

Create your own promise that resolves to either the cached data or the fetched data.

app.factory('myService', function($http, $q) {
    var items = [];
    var myService = {
        getItems: function() {
            var deferred =  $q.defer();

            if (items.length > 0) {
                 //resolve the promise with the cached items
                deferred.resolve(items);
            } else {     
                $http.get('test.json').then(function (response) {
                    // fill up items with result, so next query just returns items.
                    for(var i=0;i<response.data.length;i++){
                        items.push(response.data[i]);
                    }
                    //resolve the promise with the items retrieved
                    deferred.resolve(items);
                },function(response){
                   //something went wrong reject the promise with a message
                   deferred.reject("Could not retrieve data!"); 
                });


            }
         // Return the promise to the controller
         return deferred.promise;
     };
     return myService;
});

Then consume the promise in your controller.

app.controller('MainCtrl', function( myService,$scope) {
    var promiseOrData = myService.getItems();

        promiseOrData.then( function(data){
            $scope.data = data;
        },
        function(data){
            // should log "Could not retrieve data!"
            console.log(data)
        });

});
Jonathan Palumbo
  • 6,851
  • 1
  • 29
  • 40