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:
heh heh ;)
ReplyDeleteuseful as always...
Nice! One question: How can I access the foo var in the controller?
ReplyDeleteapp.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.
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:
Delete1. 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);
});
Very clear and concise. Helped me understand the concept of promises in Angular. Thanks!
ReplyDeleteThank you xD
ReplyDeleteNo need for $q if you have a promise, just chain them:
ReplyDeleteapp.factory('myService', function($http) {
return {
getFoo: function() {
return $http.get('foo.json').then(function(response) {
return response.data;
});
}
}
});
Very good point. I guess I like the explicitness of $q. But I hadn't thought of that.
DeleteOkay, 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)
Deletebut it doesnt return a promise to a promise. it returns a promise to the return result of the inner function.
Deleteif 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
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.
DeleteThe 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.
you almost had it. $http().success() returns the response's data property, not the response itself. change foo() to this:
ReplyDeletefoo: function (){
return $http.get('foo.txt').then(function(result){
return result.data === 'true';
});
}
I see, you're using then() rather than success(). Good info. Thanks.
Delete