By writing this I'm taking a risk of looking like an idiot who has failed to read the docs. So please be gentle.
AngularJS uses a promise module called $q
. It originates from this beast of a project.
You use it like this for example:
angular.module('myapp')
.controller('MainCtrl', function($scope, $q) {
$scope.name = 'Hello ';
var wait = function() {
var deferred = $q.defer();
setTimeout(function() {
// Reject 3 out of 10 times to simulate
// some business logic.
if (Math.random() > 0.7) deferred.reject('hell');
else deferred.resolve('world');
}, 1000);
return deferred.promise;
};
wait()
.then(function(rest) {
$scope.name += rest;
})
.catch(function(fallback) {
$scope.name += fallback.toUpperCase() + '!!';
});
});
Basically you construct a deferred object and return its promise. Then you can expect the .then
and .catch
to be called back if all goes well (or not).
There are other ways you can use it too but let's stick to the basics to drive home this point to come.
Then there's the $http
module. It's where you do all your AJAX stuff and it's really powerful. However, it uses an abstraction of $q
and because it is an abstraction it renames what it calls back. Instead of .then
and .catch
it's .success
and .error
and the arguments you get are different. Both expose a catch-all function called .finally
. You can, if you want to, bypass this abstraction and do what the abstraction does yourself. So instead of:
$http.get('https://api.github.com/users/peterbe/gists')
.success(function(data) {
$scope.gists = data;
})
.error(function(data, status) {
console.error('Repos error', status, data);
})
.finally(function() {
console.log("finally finished repos");
});
...you can do this yourself...:
$http.get('https://api.github.com/users/peterbe/gists')
.then(function(response) {
$scope.gists = response.data;
})
.catch(function(response) {
console.error('Gists error', response.status, response.data);
})
.finally(function() {
console.log("finally finished gists");
});
It's like it's built specifically for doing HTTP stuff. The $q
modules doesn't know that the response body, the HTTP status code and the HTTP headers are important.
However, there's a big caveat. You might not always know you're doing AJAX stuff. You might be using a service from somewhere and you don't care how it gets its data. You just want it to deliver some data. For example, suppose you have an AJAX request cached so that only the first time it needs to do an HTTP GET but all consecutive times you can use the stuff already in memory. E.g. Something like this:
angular.module('myapp')
.controller('MainCtrl', function($scope, $q, $http, $timeout) {
$scope.name = 'Hello ';
var getName = function() {
var name = null;
var deferred = $q.defer();
if (name !== null) deferred.resolve(name);
$http.get('https://api.github.com/users/peterbe')
.success(function(data) {
deferred.resolve(data.name);
}).error(deferred.reject);
return deferred.promise;
};
// Even though we're calling this 3 different times
// you'll notice it only starts one AJAX request.
$timeout(function() {
getName().then(function(name) {
$scope.name = "Hello " + name;
});
}, 1000);
$timeout(function() {
getName().then(function(name) {
$scope.name = "Hello " + name;
});
}, 2000);
$timeout(function() {
getName().then(function(name) {
$scope.name = "Hello " + name;
});
}, 3000);
});
And with all the other promise frameworks laying around like jQuery's you will sooner or later forget if it's success()
or then()
or done()
and your goldfish memory (like mine) will cause confusion and bugs.
So is there a way to make $http.<somemethod>
return a $q
like promise but with the benefit of the abstractions that the $http
layer adds?
Here's one such possible solution maybe:
var app = angular.module('myapp');
app.factory('httpq', function($http, $q) {
return {
get: function() {
var deferred = $q.defer();
$http.get.apply(null, arguments)
.success(deferred.resolve)
.error(deferred.resolve);
return deferred.promise;
}
}
});
app.controller('MainCtrl', function($scope, httpq) {
httpq.get('https://api.github.com/users/peterbe/gists')
.then(function(data) {
$scope.gists = data;
})
.catch(function(data, status) {
console.error('Gists error', response.status, response.data);
})
.finally(function() {
console.log("finally finished gists");
});
});
That way you get the benefit of a one same way for all things that get you data some way or another and you get the nice AJAXy signatures you like.
This is just a prototype and clearly it's not generic to work with any of the shortcut functions in $http
like .post()
, .put()
etc. That can maybe be solved with a Proxy
object or some other hack I haven't had time to think of yet.
So, what do you think? Am I splitting hairs or is this something attractive?
Comments
Post your own commentNice post! I am not an Angular Boss yet so I cannot comment on the code itself but thanks to you I am having a universal http construction which looks the same for all my http requests. For example I was using the http success/error/finally construction but proved not to work with Angular's Bootstrap typeahead. So I had to use the 'then' method, but I did not know how to use my original error/finally methods.
Thank you for this post.
I was trying to figure out how I could return a promise for a cached response in stead of a $http. I'm going to try and implement this. Since I'm only using get() it should work.
I don't understand why angularjs makes $http use a different promise callback.
By the way I think the last solution has a small typo:
.error(deferred.resolve); => .error(deferred.reject);
It seems that your main motivation is to avoid having one method return different types of promises (or data) depending on what happens, as in your example with an AJAX request the first time and loading from some cache on subsequent requests - is that correct?
If so, there's a much easier way to accomplish this: just chain then-calls on your promises, and reshape the data, until the client code can use the output consistently. For example, consider the following service method which takes an url and either gets the results from a server, or loads them from cache:
function loadData(url) {
var deferred = $q.defer();
if (isInCache(url)) {
deferred.resolve(getFromCache(url));
} else {
$http.get(url).success(function(data) { deferred.resolve(data); });
}
return deferred.promise;
}
This gets even easier if your cache service is promise-aware:
function loadData(url) {
if (isInCache(url)) {
return getFromCache(url); // returns a then-able promise
} else {
return $http.get(url).success(function (data) { return data; }); // also a then-able promise
}
}
My point is, if you build your services to have a consistent API, you don't need to hack around with proxies or wrappers around $http at all.
I love it!
I actually used this in another project since after I published this blog post :)
I think that the when method from $q does exaclty what you want:
https://docs.angularjs.org/api/ng/service/$q (search for when(value);)
What I ended up doing is simulating the '.success' and '.error' promises of $http, and using an $http-like API everywhere for my service.
I'm not sure if this is bad, but I did this when returning a cached object:
return {success: function(f){
f(cachedObject.response);
},
error: function(f){
}}
When the item is NOT cached, I simply return the original $http promise. So I can use my service like an $http promise everywhere...which is what I want to do because it is convenient.
I am working on an AngularJS Highcharts example based almost exactly on your blog post. Once the data is pulled from the service into the controller it's ready to use —however, the objects are inaccessible outside their scope. Please see my jsfiddle: https://jsfiddle.net/47ronin/y5c9cm5g/1/ —The challenge here is, Highcharts breaks if $scope.data and $scope.options are moved into an enclosing function. How can you "broadcast" the objects from within a function to an outside scope, as needed in my example. Thanks, and great blog post!
Hey Glenn,
Just curious if you found an answer to this issue.
Actually yes, scope was solved. Sorry for the late reply!
http://jsfiddle.net/rtyw81zw/2/
as referenced by Highcharts author:
https://github.com/gevgeny/ui-highcharts/issues/3#issuecomment-116047457
Everytime i used $http.get() / $http.post() to retrieve some json data i had to make something like this to be sure everything was json valid and error free from server,
app.controller('MainCtrl', function($scope, httpq) {
$http.get('https://www.mysite.com/users/')
.success(function(response){
if (response != undefined && typeof response == "object") {
$scope.users = response.users;
} else {
alert("MainCtrl -> Users: Result is not JSON type");
}
})
.error(function(data) {
alert("MainCtrl -> Users: Server Error");
});
$http.get('https://www.mysite.com/news/')
.success(function(response){
if (response != undefined && typeof response == "object") {
$scope.news= response.news;
} else {
alert("MainCtrl -> News: Result is not JSON type");
}
})
.error(function(data) {
alert("MainCtrl -> News: Server Error");
});
});
That's because entering the .success() does not guarantee its a json object, you can return from server a simple print "hello world"; and it will still enter .success()
Therefore, I was looking for something more practical, i ended on this page and after some reading i endend with this:
var app = angular.module('myapp');
app.factory('httpq', function($http, $q) {
function createValidJsonRequest(httpRequest) {
return {
errorMessage: function (errorMessage) {
var deferred = $q.defer();
httpRequest
.success(function (response) {
if (response != undefined && typeof response == "object") {
deferred.resolve(response);
} else {
alert(errorMessage + ": Result is not JSON type");
}
})
.error(function(data) {
deferred.reject(data);
alert(errorMessage + ": Server Error");
});
return deferred.promise;
}
};
}
return {
getJSON: function() {
return createValidJsonRequest($http.get.apply(null, arguments));
},
postJSON: function() {
return createValidJsonRequest($http.post.apply(null, arguments));
}
}
});
app.controller('MainCtrl', function($scope, httpq) {
httpq.getJSON('https://www.mysite.com/users/')
.errorMessage("MainCtrl -> Users")
.then(function(response) {
$scope.users = response.users;
});
httpq.getJSON('https://www.mysite.com/news/')
.errorMessage("MainCtrl -> News")
.then(function(response) {
$scope.news = response.news;
})
.catch(function(result){
// do something in case of error
});
httpq.postJSON('https://www.mysite.com/news/', { ... })
.errorMessage("MainCtrl -> addNews")
.then(function(response) {
$scope.news.push(response.new);
});
});
Now i can use something like this knowing:
- It will be json valid.
- It will handle server errors.
- It will automatically display alerts showing a custom error message
- If it enters .then() everything went as planned.
Thanks Peter
hmmm probably I'm in need of some javascript knowledge because, how can this piece of code not fire three Ajax requests?
var getName = function() {
var name = null;
var deferred = $q.defer();
if (name !== null) deferred.resolve(name);
$http.get('https://api.github.com/users/peterbe')
.success(function(data) {
deferred.resolve(data.name);
}).error(deferred.reject);
return deferred.promise;
};
When called three times with the $timeout, the name variable is local to the function and it gets set every call to null. So that would mean that this condition is always false?
if (name !== null) deferred.resolve(name);
Meaning no caching is happening here, I know it's not the main subject of this article but still.
Correct me if I'm wrong.
Thanks J
If you make that 'var name' global instead and assign to it inside the success callback you'll be fine.
But also, you'll need to put the $http.get in an else block.
I am curious how to use .then with $q or if it is even necessary. I grew accustomed to .success and .error but those have been depreciated.
thanks i didnt knew angular $http has success and error callbacks
I tried to do something like this about 6 months ago but didn't really have a good handle on promises and on using promises in services. We had other work to do so we just make our required call synchronouse with a quick class I created. Ugly, yes. Worked, yes. Fast forward and I finally have a chance to revisit the ugly code I wrote and did a quick search on promises and found this. After quite a bit more experience with promises your simple example above explains exactly the piece I was missing with defferred promises.
Fixing my ugly code now - thanks!
Great post! I 'extended' my CRM code with a variant of this.
Many thanks for the idea.
Hi,
note .success and .error "methods" wa removed from the release 1.6 of Angular.js
You're awesome bruuh.
I just fixed my code.Thanks dude for this part:
.then(function(response) {
$scope.gists = response.data;
})