0

I am having a problem with promises in an angular service. I have a service with a method getArea which is supposed to return the name of a service-area. The service gets the service-areas from the API. When getArea gets the service-areas, it finds the name of the requested area, and should return it. However, my code does not work - I get into an infinite loop. I guess I have misunderstood how to use promises?

SupplierService:

var servicePromise;
var getServices = function(){
    if( !servicePromise ){
        servicePromise = $http.get('/api/services')
            .then(function(res){
                return res.data.data;
            });
    }
    return servicePromise;
};


var myService = {

    getServices : getServices,

    getArea : function(questionnaireId){
        getServices().then(function(services){
            // ...
            return "hello world";
        });
    }
};

return myService;

Controller:

$scope.supplierService = SupplierService;

View:

<div>
    <b>Area:</b> {{ supplierService.getArea(r.questionnaireId) }}
</div

I expect the view to show "Area: hello world", but gets into an infinite loop.


Update 1: I have added getServices as a public function in the service, and can access it from the controller like this:

SupplierService.getServices().then(function(d){
    $scope.services = d;
});

Therefore I guess the problem is in the getArea method?


Update 2: I was inspired by this answer https://stackoverflow.com/a/12513509/685352. I want to cache the result.


Update 3: Here is a plunker. If you try accessing supplierService.getArea(100) from the view - the browser will not respond.

Community
  • 1
  • 1
swenedo
  • 3,074
  • 8
  • 30
  • 49

2 Answers2

2

Your service should look more like this:

var getServices = function(){
    var deferred = $q.deferred();
    $http.get('/api/services')
            .then(function(res){
                deferred.resolve(res.data)
            });
    return deferred.promise;
};

Notice when you create a deferred you must return the deferred.promise (the actual promise) and then when you're async call returns you must call deferred.resolve or deferred.rejected as appropriate (to trigger the success or error functions respectively)

Minor addition I have a plunkr showing a few ways of getting data from a service into your controllers since this is a common issue for devs coming into Angular

http://plnkr.co/edit/ABQsAxz1bNi34ehmPRsF?p=info

It's not absolute best practices since I tried to keep it as simple as possible, but basically showing three different ways to "share" your data keep in mind some of these methods rely on angular.copy which means the property of the service you store the data on must be an Object or an Array (primitive types won't work since the reference can't be shared).

Here's a rewrite including the function inline:

var myService = {
    var dataLoaded = false;
    var data = {}; //or = [];
    getServices : function(){
        var deferred = $q.defer();
        if( !dataLoaded ){
            $http.get('/api/services').then(function(res){
                angular.copy(res.data, myService.data);
                deferred.resolve(myService.data);
            }, function(err){
                deferred.reject("Something bad happened in the request");
            });
        }
        else
        {
            deferred.resolve(myService.data);
        }
        return deferred.promise;
    }
};

return myService;

To explain, I create a new promise using the $q service which you'll need to inject to the service function. This allows me to either resolve that promise with data I already have or to make the call to the service and resolve that data but in both cases when this is being used it's assumed you will just get a promise back and are therefore dealing with an async operation. If you have multiple data sets to load you can use an object to store the flags instead of a single boolean.

shaunhusain
  • 19,630
  • 4
  • 38
  • 51
  • 1
    +1 $q service, Angular's lean promise service, would be injected. You could of course just return __$http.get('/api/services')__ as it already is a promise. – cbayram Dec 17 '13 at 18:47
  • Good point cbayram, I tend to do it this way and use an object to check if I've already loaded the data (store a property for each service call that I'll make in a service to know if the data is loaded), I think I just got used to doing it "explicitly" but like you say using the HttpPromise returned from the get I imagine is a fine solution too. – shaunhusain Dec 17 '13 at 18:50
  • Isn't the problem in the `getArea` method? I actually also return the getServices method, and I get the services in the controller when the async call return. – swenedo Dec 17 '13 at 18:58
  • @swenedo not sure what else is missing here are you still running into an issue with the last version of the code, can you post a plunkr or jsfiddle showing your issue in action? – shaunhusain Dec 17 '13 at 20:22
  • @shaunhusain I have posted a plunkr, with my version of the code. It shows that my `getServices()` works, but `getArea` does not. – swenedo Dec 17 '13 at 22:11
  • @swenedo so the problem is basically you are trying to make an asynchronous call look as though it's synchronous which really doesn't work out, I put together a possible option here: http://plnkr.co/edit/C9P9FyH6YJ2innxtgHFO?p=preview – shaunhusain Dec 17 '13 at 23:33
  • Thanks @shaunhusain, how would you call `getArea` from the view? I use `getArea` inside a ng-repeat and want the view to request the name of each area from the service. When the async call returns the view should be updated. – swenedo Dec 18 '13 at 10:31
0

i think if you return the $http callback?

//$http.get('/someUrl').success(successCallback);


var getServices = function(){
    return $http.get('/api/services');

};

getServices.success(function(services){
// ...
           return "hello world";
        });
   }
Sotos
  • 416
  • 4
  • 4