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 The "Classic" Way

(old and busted)

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

(the new hotness)

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.

Promises get special treatment in Angular however, because when a promise is resolved, Angular will trigger a digest, and any scope properties that contain a promise will be evaluated for that promise's resolved value. 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.

Promise Pattern Example


app.factory('myService', function($http, $q) {
   return {
     getFoo: function() {
       //create our deferred object.
       var deferred = $q.defer();

       //make the call.
       $http.get('foo.json').success(function(data) {
          //when data is returned resolve the deferment.
          deferred.resolve(data);
       }).error(function(){
          //or reject it if there's a problem.
          deferred.reject();
       });

       //return the promise that work will be done.
       return deferred.promise;
     }
   }
});

app.controller('MainCtrl', function($scope, myService) {
   //the clean and simple way
   $scope.foo = myService.getFoo();

   //OR if you need to wait for the value to return 
   //to do something with it...
   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:


12 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