greenie2600
greenie2600

Reputation: 1699

How to structure an Angular service so it can handle asynchronous calls?

In my Angular application, I have two controllers which both need access to the same data.

Toward that end, I've created a service which will be responsible for holding and providing access to that data:

angular.module("SomeModule").factory( "SomeService", function( $http ) {

    var svc = {};
    var data = {};

    // on initialization, load data from the server
    $http.get( "somefile.json" )
        .success( function( data ) {
            svc.data = data;
        } );

    svc.getItem = function( id ) {
        // find the specified item within svc.data, and return its value
    };

    return svc;

} );

...and I've injected that service into each of the two controllers:

angular.module("SomeModule").controller( "SomeController", function( $routeParams, SomeService ) {

    var ctrl = this;

    ctrl.item = null; // set an initial value

    // load the item that was requested in the URL
    ctrl.item = SomeService.getItem( $routeParams.id );

} );

This almost works - but it has one big flaw. If SomeController calls SomeService.getItem() before SomeService finishes loading somefile.json, then SomeService won't have any data to return.

In practice, if I load the app a few times, some loads will work (i.e., SomeService will finish loading somefile.json first, and the controller will present the data as desired), and other loads don't (i.e., SomeController will try to retrieve data from SomeService before the data has actually been loaded, and everything will crash and burn).

Obviously, I need to find some way to defer the execution of getItem() until SomeService is actually ready to process those calls. But I'm not sure of the best way to do that.

I can think of a some rather hairy solutions, such as building my own call queue in SomeService, and wiring up a bunch of complicated callbacks. But there's gotta be a more elegant solution.

I suspect that Angular's $q service could be useful here. However, I'm new to promises, and I'm not sure exactly how I should use $q here (or even whether I'm barking up the right tree).

Can you nudge me in the right direction? I'd be super grateful.

Upvotes: 1

Views: 1106

Answers (5)

z0r0
z0r0

Reputation: 666

try this code

angular.module("SomeModule").factory("SomeService", function ($http) {
    var svc = {};

    svc.getList = function () {
       return $http.get("somefile.json");
    };

    svc.getItem = function (id) {
        svc.getList().then(function (response) {
            // find the specified item within response, and return its value
        });
    };
    return svc;
});

Upvotes: 2

Okazari
Okazari

Reputation: 4597

Here is how i did it in my own project.

Your Service

angular.module("SomeModule").factory( "SomeService", function( $http ) {

    var svc = {};
    svc.data = {};

    // on initialization, load data from the server
    svc.getData = function(){
        return $http.get( "somefile.json" );
    };

    return svc;

} );

Your Controllers

angular.module("SomeModule").controller( "SomeController", function( $routeParams, SomeService ) {

    ctrl.items = null; // set an initial value

    // load the item that was requested in the URL
    SomeService.getData().success(function(data){
        ctrl.items = data;
    }).error(function(response){
        console.err("damn");
    });

} );

Important point : Promises

In my humble opinion, the responsibility for processing asynchronous call is due to the controller. I always return a $http promiss whenever i can.

 svc.getData = function(){
    return $http.get( "somefile.json" );
 };

You can add some logic in your service but you always have to return the promise. (To know : .success() on a promise return the promise)

The controller will have the logic to know how to behave depending to the response of your asynchronous call. He MUST know how to behave in case of success and in case of error.

If you have more question feel free to ask. I hope it helped you.

Upvotes: 1

JMK
JMK

Reputation: 28069

I would recommend making better use of AngularJS' routing capabilities, which allow you to resolve dependencies, along with the $http services cache, and structuring your application accordingly.

I think you need to, therefore, get rid of your service completely.

Starting with the example below, taken straight from the Angular documentation:

phonecatApp.config(['$routeProvider',
  function($routeProvider) {
    $routeProvider.
      when('/phones', {
        templateUrl: 'partials/phone-list.html',
        controller: 'PhoneListCtrl'
      }).
      when('/phones/:phoneId', {
        templateUrl: 'partials/phone-detail.html',
        controller: 'PhoneDetailCtrl'
      }).
      otherwise({
        redirectTo: '/phones'
      });
  }]);

So PhoneListCtrl and PhoneDetailCtrl both need the data from somefile.json. I would inject that data into each controller like so:

(function(){
        angular.module('phonecatApp').controller('PhoneListCtrl', ['somefileJsonData', function(somefileJsonData){
            this.someFileJsonData = someFileJsonData;
        }]);
})();

The same idea for PhoneDetailCtrl.

Then update your routing like so:

phonecatApp.config(['$routeProvider',
  function($routeProvider) {
    $routeProvider.
      when('/phones', {
        templateUrl: 'partials/phone-list.html',
        controller: 'PhoneListCtrl',
        resolve:{ somefileJsonData: ['$http',function($http){
            return $http.get("somefile.json", { cache: true });
        }] }
      }).
      when('/phones/:phoneId', {
        templateUrl: 'partials/phone-detail.html',
        controller: 'PhoneDetailCtrl',
        //same resolve
      }).
      otherwise({
        redirectTo: '/phones'
      });
  }]);

This way, you are letting angular take care of resolving this dependency as part of the routing process.

Setting cache to true will also cache it so you aren't doing the same Get request twice, and Angular will only show your view when the dependency is resolved.

So, in your app, where SomeController is paired with a view as part of the routing process, use resolve to resolve item, and inject this into the controller.

Upvotes: 2

svarog
svarog

Reputation: 9847

Here's how we do it, we use $q to defer whatever the async call will provide your service, I then take the data part of the response and resolve it, it sends the required data to the controller (without status, headers...).

I use a try catch statement in my service, to keep error handling away from the controller.

angular.module("JobsService", [])
    .factory("JobsService", ['$q', '$http', '$log', function ($q, $http, $log) {

        var serviceName = "JobsService";
        var url = "http://localhost:8080/path/to/resource/";

        var service = {};

        service.getAll = function () {

            var deferred = $q.defer();

            try {
                $http.get(url + "/jobs")
                        .success(function (response, status) {
                            $log.debug("GET response in " + serviceName + " returned with status " + status);
                            deferred.resolve(response);
                        })
                        .error(function (error, status) {
                            deferred.reject(error + " : " + status);
                        });
            } catch (err) {
                $log.error(err);
                deferred.reject();
            }

            return deferred.promise;
        };

        return service;
    }]);

then in controller

JobsService.getAll()
         .then(function (response) {

             $scope.jobs = response;
             // records are stored in $scope.jobs
         }, function (response) {
             $scope.jobs = undefined;
         })
             .finally(function () {
              // will always run
         });

Upvotes: 0

A.B
A.B

Reputation: 20445

ther are 2 good options you can

  1. use callback

  2. use $q return promise

Using Callback:

svc.getItem = function( id,callback ) {
      $http.get( "somefile.json" )
        .success( function( data ) {
            svc.data = data;
            callback(svc.data)
        } );
    };

in controller

SomeService.getItem( $routeParams.id,function(data){
 ctrl.item =  data
 } );

Using Promise:

  svc.getItem = function( id) {

   var deferred = $q.defer();

      $http.get( "somefile.json" )
        .success( function( data ) {
            svc.data = data;
            deferred.resolve(svc.data);
        } )
        .error(function (error) {
        deferred.reject(error);
         });

      return deferred.promise;
     ;
    };

in controller

SomeService.getItem( $routeParams.id).then(function (data) {
     ctrl.item =  data
},
function (error) {
    //do something with error
});

Upvotes: 0

Related Questions