Wednesday, February 13, 2013

AngularJS: Creating A Service With $http

So I did a talk on AngularJS last night at the Pittsburgh .NET Users' Group. It was a great talk and you guys asked a lot of great questions. One of my friends that attended suggest people use my blog as a resource for Angular, and while I do have quite a few posts about Angular, I don't feel like it's quite as flushed out in that department as I'd like it to be before I wanted my friends plugging it like that. So I've decided to try to fill that gap, at least as far as Angular goes, over the next few weeks, if I can. (Don't worry, PGHNUG attendees, I'll still try to get a well commented Angular/Web API solution up on GitHub soon).


HTTP Service Calls In Angular


One of the most common questions I see on StackOverflow regarding Angular, are questions involving the creation of AJAX-based angular services using $http. Commonly the pattern used is very reminiscent of JQuery, where there's a method with a callback when the data is received.

Common Callback Example


app.factory('myService', function($http) {
   return {
     getFooOldSchool: function(callback) {
       $http.get('foo.json').success(callback);
     }
   }
});

app.controller('MainCtrl', function($scope, myService) {
  myService.getFooOldSchool(function(data) {
     $scope.foo = data;
  });
});


That's fine. It's an easy to understand pattern that is predictable for most other developers using your service and most importantly, it works.


Angular Loves Promises

In Angular, there is a service called $q. It is a deferred/promise implementation built off of Q by Kristopher Kowal. I know I've talked about deferment and promises in JavaScript in the past, but as a very quick refresher, the idea behind this pattern is basically to have a mechanism to signal when one (or sometimes many) asynchronous actions are complete. It's the hub of JQuery's AJAX implementation, Angular $http implementation and Angular's $resource implementation. I like this mechanism so much I've even implemented it in my .NET Event Loop Framework.

EDIT: As of 1.2.0, promises are no longer resolved by templates.

So in code, that means if you return a promise from your service, and put it directly in a scope property, it will asynchronously update that scope property and process the changes.

Since $http methods like get() and post() return promises, we can use that promise's then() method (which also returns a promise) to pull the data out of the result. The return value from the then() method's callback is used to resolve that promise.

Simplified Promise Pattern Example


app.factory('myService', function($http) {
   return {
     getFoo: function() {
       //since $http.get returns a promise,
       //and promise.then() also returns a promise
       //that resolves to whatever value is returned in it's 
       //callback argument, we can return that.
       return $http.get('foo.json').then(function(result) {
           return result.data;
       });
     }
   }
});

app.controller('MainCtrl', function($scope, myService) {
   //DEPRECATED: The commented line below WILL NO LONGER WORK in 1.2.0
   //since promises are no longer resolved by templates.
   //$scope.foo = myService.getFoo();

   //make the call to getFoo and handle the promise returned;
   myService.getFoo().then(function(data) {
       //this will execute when the 
       //AJAX call completes.
       $scope.foo2 = data;
       console.log(data);
   });
};



And because some of you like to play around, here's a bit of code on plunker showing the promise pattern for $http calls in Angular:


Edit: Complex $http calls from within a Service


Because I've been asked by friends what to do in situations where you might have nested or simultaneous async calls in a controller, I think this blog entry is a really good place to show some examples of that, since it falls under the same domain, so to speak.

There are of course scenarios where you might have a service method that requires more than one $http call (or other async call) to be made before you want the service to return. This is where you'd want to use the $q service mentioned above.

Example of dealing with multiple async calls to return simultaneously


app.factory('myService', function ($http, $q){
  return {
    getItems: function (){
      //$q.all will wait for an array of promises to resolve,
      // then will resolve it's own promise (which it returns)
      // with an array of results in the same order.
      return $q.all([
        $http.get('items_part_1.json'),
          $http.get('items_part_2.json')
      ])
        
      //process all of the results from the two promises 
      // above, and join them together into a single result.
      // since then() returns a promise that resolves ot the
      // return value of it's callback, this is all we need 
      // to return from our service method.
      .then(function(results) {
        var data = [];
        angular.forEach(results, function(result) {
          data = data.concat(result.data);
        });
        return data;
      });
    }
  };
});


Example of dealing with nested async calls in a single service call

Technically, we can deal with nested async calls without using $q. ("nested" to say that each call to $http triggers a subsequent call to $http in order to build out some data) This simplifies the code a little, but in my opinion makes it harder to follow.  For example:

 getNestedData: function (){ 
      // get the parents.
      return $http.get('parents.json')
        .then(function(result) {          
          //return a promise to that data.
          return result.data;
        })
        //handle the promise to the parent data
        .then(function(parents) {         
          //get the children.
          return $http.get('children.json')
            //handle the promise to the children.
            .then(function(result) {            
              //add children to the appropriate parent(s).
              var children = result.data;
              angular.forEach(parents, function(parent) {
                parent.children = [];            
                angular.forEach(children, function(child) {
                  if(parent.childIds.indexOf(child.id) >= 0) {
                    parent.children.push(child);
                  }
                });
              });              
              //return the parents
              return parents;
            });
        });
    }


Example of nested calls using $q directly

This is a point where it really doesn't matter which route you go when it comes to what's going to work. However, it is my opinion that using $q directly in complicated async calls or nested async calls, enhances readability. This is simply because it becomes easier to see which promise was returned, when it was created, and where it was resolved:


getNestedDataBetter: function (){
  //create your deferred promise.
  var deferred = $q.defer();
  
  //do your thing.
  $http.get('parents.json')
    .then(function(result){
      var parents = result.data;
      $http.get('children.json')
        .then(function(result) {
          var children = result.data;
            angular.forEach(parents, function(parent) {
              parent.children = [];            
              angular.forEach(children, function(child) {
                if(parent.childIds.indexOf(child.id) >= 0) {
                  parent.children.push(child);
                }
              });
            }); 
            
            //at whatever point in your code, you feel your
            // code has loaded all necessary data and/or
            // resolve your promise.
            deferred.resolve(parents);
        });
    });
    
  //return your promise to the user.
  return deferred.promise;
}


45 comments:

  1. Nice! One question: How can I access the foo var in the controller?

    app.controller('MainCtrl', function($scope, myService) {
    //the clean and simple way
    $scope.foo = myService.getFoo();
    console.log($scope.foo[0].name); // I want to use the first name from json array!
    };

    Thanks.

    ReplyDelete
    Replies
    1. I've updated the post to reflect this, since it's a common scenario... If there's a situation where you need to wait for the value to return, you can either:

      1. use the promise's then() function:

      $scope.getFoo().then(function(data) {
      $scope.foo = data;
      console.log(data[0].name);
      });

      or 2. use a $watch

      $scope.$watch('foo', function(v) {
      console.log(v[0].name);
      });

      Delete
  2. Very clear and concise. Helped me understand the concept of promises in Angular. Thanks!

    ReplyDelete
  3. No need for $q if you have a promise, just chain them:
    app.factory('myService', function($http) {
    return {
    getFoo: function() {
    return $http.get('foo.json').then(function(response) {
    return response.data;
    });
    }
    }
    });

    ReplyDelete
    Replies
    1. Very good point. I guess I like the explicitness of $q. But I hadn't thought of that.

      Delete
    2. Okay, I just ran into the reason why I use $q rather than the method you've outlined. In particular, if you have a web method that returns true or false. For example a check for a username availability, and you return using the method you've described above, you'll run into scenarios where the result will be type coerced to be *true* since it's returning the $http's prior to updating it to the inner deferred result. So it's generally best practice to use $q in these scenarios, so you're not returning a promise to a promise, you're just returning a promise... if that makes sense. (Your method will work fine in some scenarios though)

      Delete
    3. but it doesnt return a promise to a promise. it returns a promise to the return result of the inner function.

      if i have:

      var promise = $http.get('/username/'+username+'/_available').then(function(response){ return response.data; });

      and i do:

      promise.then(function(value){
      console.log(value);
      });


      the console will log the response.data property, not a promise. here's a plunk:

      http://plnkr.co/edit/E6g9RYwRSFPFptZ7iqAK?p=preview

      Delete
    4. Well, a "promise to a promise" isn't accurate, I guess. And in the scenario where you're doing all of your $http calls from within your controller, I suppose what you're doing here is okay. The problem comes in when you try to create something a little more robust by abstracting your $http calls into a custom angular service... which was precisely what this post was about.

      The issue is that without $q, you'll have to use a process the response, then use a callback to handle it in your controller. Callbacks are ugly, inflexible, and unfortunately all too prevalent thanks to older versions of JQuery.

      Here is a demonstration of exactly what I mean: http://plnkr.co/edit/WWAuZqsbAsCXolJAb28l?p=preview

      Using $q to process and return the response from your service will be cleaner, more flexible and overall better than the alternatives. Especially with the slick way that Angular handles returns on $scope properties.

      Delete
  4. you almost had it. $http().success() returns the response's data property, not the response itself. change foo() to this:

    foo: function (){
    return $http.get('foo.txt').then(function(result){
    return result.data === 'true';
    });
    }

    ReplyDelete
    Replies
    1. I see, you're using then() rather than success(). Good info. Thanks.

      Delete
  5. Hi, so then, is there any point of using $q with $http? Thanks.

    ReplyDelete
    Replies
    1. Absolutely. In the event that you want to additionally scrub the results of your response into another model. Or in the event you'd like to have a service method wait for two $http calls (nested or unnested) to return.

      Delete
  6. This is an old post, but I see some recent comments so I'll chime in.

    You actually don't need to use either $q OR nest your $http calls for running multiple $http calls. This will work:

    return $http.get('parents.json')
    .then(function( parents ) {
    return $http.get('children.json');
    })
    .then(function( children ) {
    // do things with children
    });

    Promises are meant to be chainable and any promise returned in a `then` call will be automatically resolved and its result passed to the next `then`.

    ReplyDelete
    Replies
    1. You're absolutely right. However, this has been gone over a few times in the comments, and I updated the article a (if you read the whole thing :P haha) while ago to reflect this. Perhaps I missed something?

      Delete
  7. Small correction:
    return $http.get('foo.json').then(result) {
    return result.data;
    };
    should be:
    return $http.get('foo.json').then(function(result) {
    return result.data;
    });
    in the "Simplified Promise Pattern Example" example.
    Thanks for the article!

    ReplyDelete
    Replies
    1. Heh.. yeah, silly typo on my part... good catch.

      Delete
  8. Hey Benjamin,

    Thanks for the tip! I'm running into a little bit of a problem(I've just started learning Angular, coming from Backbone).

    I have your slick promise example working fine with a little Todo example I'm making. However, when I implement a ng-click and try to access $scope.todos it looks like it is wrapped by a AngularJS object when it should just equal the Todo array returned by my Factory. See the deleteTodo function below:


    var todoApp = angular.module('todoApp', [])
    .factory('todoFactory', function ($http) {
    var factory = {};

    // get from api example using promise pattern
    factory.getTodos = function () {
    return $http.get('api/todo').then(function (result) {
    return result.data;
    }, function (reason) {

    });
    };

    // delete from api example using promise pattern
    factory.deleteTodo = function (id) {
    return $http({ method: 'DELETE', url: 'api/todo', data: { id: id } }).then(function (result) {
    return true;
    }, function (reason) {
    return false;
    });
    };

    return factory;
    }).controller('todoController', function ($scope, todoFactory) {

    init();
    function init() {
    $scope.todos = todoFactory.getTodos();
    }

    // ui event delete example
    $scope.deleteTodo = function ($index) {
    //////////////////////////
    ///////// $scope.todos is wrapped by some AngularJS object(or the promise) instead of just ///////// being the todo array
    ///////// $scope.todos == { $$v: [..the todo array..], then: function(){ ... }
    //////////////////////////
    var todoToDelete = $scope.todos[$index];

    todoFactory.deleteTodo(todoToDelete.id).then(function () {
    $scope.todos.splice($index, 1);
    }, function (reason) {
    alert('there was an error');
    });
    };

    });

    Thank you for the post!
    -alex

    ReplyDelete
    Replies
    1. Well, that's because technically it is wrapped by the angular promise. If you put a promise in a $scope.property in angular, when it binds the value in the view, it will get the value out of the promise. The scope.property itself is still a promise... if that makes sense. If you need access to the data directly, it might be worth handling the promise's then and putting the value in a property or variable.

      I hope that helps.

      Delete
    2. Yep that makes sense.

      I ended up changing my factory to just use the higher level $resource as opposed to $http.

      .factory('todoFactory', function ($resource) {
      return $resource('api/todos/:id');
      });

      No other changes were necessary in my controller. Thank you for your quick help!

      Delete
  9. Thank you so much, just starting out with AngularJS and Javascript and while I had a high-level understanding of promises and how $http works, when it came to actually writing some code for the first time I was a little lost as to how it can be made to work together. Your first examples were just what I was looking for!

    ReplyDelete
  10. Hey Benjamin,
    I am writing a directive to dump a list on a page based on html data I retrieve async from the database server.
    see below:
    I thought that "MenuService.getMenuData().then" would wait for the data but some how the directive ends before the data arrives. I know I could put a timeout delay but that is not good. Do you have a suggestion as to what could be the problem?

    TBApp.directive('tbnavMenu', function ($compile, MenuService) {
    var tbTemplate = '3empty';

    MenuService.getMenuData().then(function (val) {
    tbTemplate = val;
    });


    var getTemplate = function () {
    return tbTemplate;
    }

    var linker = function (scope, element, attrs) {
    ...
    }

    return {
    restrict: "E",
    rep1ace: true,
    link: linker,
    controller: function ($scope, $element) {
    ...
    }
    }

    ReplyDelete
    Replies
    1. Well, this would be a question that would be easier to answer at StackOverflow. But that said, you're misunderstanding how directives work a little. The "linker" function will be called every time a directive is initialized. The function area where you put your call to load data will only ever be called once when the directive is registered... if that makes sense. If you need more help post a question on SO and send me a link, we can hash it out there.

      Delete
  11. the second plunk example can't run result as expected while executing in angularJS release 1.2.0-rc.3, Is there any change in this latest version

    ReplyDelete
    Replies
    1. 1.2.0-rc3 doesn't handle promises the same way 1.0.X did. So you need to handle the promise and put the value in the $scope. I think I may write a new blog entry on this, because I've really changed how I go about doing my $http calls in angular.

      Delete
    2. I was quite glad to find this response, as I had spent some time pointlessly trying to get my promise to be resolved by the template. I'm still a little confused why the angular-phonecat tutorial works using $resource. I'm guessing it does something clever that isn't a standard feature anymore?

      Delete
    3. $resource still works because it actually returns an empty resource the moment you call the method. The values just aren't updated until the call comes back. It's a weird feature to be sure.

      Delete
  12. the second plunk example does not run as expected while executing in the latest AngularJS release 1.2.0-rc.3, Is there
    any change to affect the result in the latest release ?

    ReplyDelete
  13. Hi,

    What about the 'catch' method that appears in the AngularJS documentation (http://docs.angularjs.org/api/ng.$q). I thought it behaved like the .fail in jQuery (cf. my plunk http://plnkr.co/IOpop4) but could'nt make it work.
    Have you used it in your projects?
    Thanks in advance.

    ReplyDelete
  14. Hi,

    I am getting detail data for a bunch of object ids. Instead of querying for each id separately, I can query for all ids at once. Unfortunately the ids are so many that the GET url max length limit of 2048 is exceeded. So I have to loop through multiple GETs, submitting 100 ids each time. I am trying to use $resource which is the right way to talk to a REST API. On $promise resolution of getting the x number of ids, I loop through the array of ids and launch asynchronous $resource GETs of batches of 100 ids to get their details back. Obviously pushing those details to the scope model array on their promise resolution fails. I see solutions out there if you know how many requests you are going to send. But in my case, I do not know what the total numbers of ids is and how many batch requests, I will have to launch. Any help would be greatly appreciated.

    ReplyDelete
  15. who uses black text on a dark grey background?

    ReplyDelete
    Replies
    1. Heh... there's apparently something wrong with that embedded plunker now... I'll see if I can fix it.

      Delete
    2. Ended up just removing the embedded plunk.

      Delete
  16. Hi,

    Thank you so much. Really helped me learn more on the $http features of angular js.
    I have a question though. Suppose if we have to pass dynamic attributes /params to the $httpd service so as to return result based on these parameters, how do we go about it.

    I want to extract the parameter from the $stateProvider service.

    .state('mystate',
    {
    url: '/category/:id', // i want the parameter 'id' to be passed on to myService $http service.
    templateUrl: category.html',
    controller: 'CategoryCrtl'
    })

    app.factory(catService', function($http) {
    return {
    getCategoryDetail: function(callback) {
    $http.get('script.php',{id:'id from $stateProvider service').success(callback);
    }
    }
    });

    app.controller('CategoryCrtl', function($scope, catService) {
    myService.getCategoryDetail(function(data) {
    $scope.foo = data;
    });
    });

    Your help would be humbly appreciated. Thank you.


    ReplyDelete
    Replies
    1. Hi Mesheshu. This question is hard for me to answer without a lot of context, but it should be as straight forward as just putting it your object you're passing to the get method. Why don't you post a question on StackOverflow? it's a better venew for this sort of thing. Post the link back in here if you like and I'll have a look.

      Delete
    2. You are right. Here is the link to the question i just posted on StackOverflow.

      http://stackoverflow.com/questions/22497866/passing-ui-router-stateparams-to-the-http-factory-service

      Please take a look there for more context on the question.

      Delete
  17. I'm just starting with JavaScript in general and Angular JS in particular. I have a basic question. In the context of Angular JS and JSON (via $http) how do we deal with failures? Where do we place our code for that?

    ReplyDelete
    Replies
    1. You can handle that as the second argument to your .then() method. $http.get(url).then(success, failure);

      Delete
  18. Comprehensive article. Thank you.

    ReplyDelete
  19. Great, very helpful post, thanks!

    ReplyDelete
  20. Just learning angular now. The fact that setting a scope variable to a promise is non-block is magic (your first 'clean way' http://plnkr.co/edit/mXcKEAAs31aZ0u8YH7kI?p=preview)

    Thank you for the post and clear examples!

    ReplyDelete
  21. Nice article, something which I was looking for. I still have one question. How can we store the http response in service variable rather than $scope of controller. I want to call $http from my custom angular service and whatever json that service returns I would like to store that in service variable so that later whenever required all controllers can get it from service variable which making more http calls. This would help me share the http returned data across all controllers.

    ReplyDelete
    Replies
    1. You can just put it in a variable in your service by chaining an extra then() in there.

      return $http.get('/foo/bar').then(function(result) { storedVar = result; return result; });

      Other than that detail the rest of the implementation is up to you and your imagination. You could have it cache a value or whatever you like.

      Delete

This form allows some basic HTML. It will only create links if you wrap the URL in an anchor tag (Sorry, it's the Blogger default)