If you use $http Promises in your Angular services you may find that from time to time you need to return some data conditionally either based on an HTTP call, or from data that is cached. In pseudo code the idea is something like this from an Angular service (this code won’t work):
function getAlbums(noCache) {
// if albums exist just return
if (!noCache && service.albums && service.albums.length > 0)
return service.albums;
return $http.get("../api/albums/")
.success(function (data) {
service.albums = data;
})
.error(onPageError);
}
The idea is that if the data already exists, simply return the data, if it doesn’t go get it via an $http call and return the promise that calls you back when the data arrives.
Promise me
The problem with the above code is that you can’t return both straight data and a promise from the same method if you expect to handle the result data consistently in one place.
Most likely you’d want to write your controller method using code like this:
vm.getAlbums = function() {
albumService.getAlbums()
.success(function(data) {
vm.albums = data;
})
.error(function(err) {
vm.errorMessage='albums not loaded');
});
}
The code is expecting a promise – or even more specifically an $http specific promise which is different than a standard $q promise that Angular uses. $http object promises have .success() and .error() methods in addition to the typical .then() method of standard promises. I’ve covered this topic in some detail a few weeks back in another blog post.
So in order to return a consistent result we should return an $http compatible promise. But because of the special nature of $http promises the following code that creates a promise and resolves it also doesn’t quite work:
function getAlbums(noCache) {
// if albums exist just return
if (!noCache && service.albums && service.albums.length > 0) {
var def = $q.defer();
def.resolve(service.albums);
return def.promise;
}
return $http.get("../api/albums/")
.success(function (data) {
service.albums = data;
})
.error(onPageError);
}
While the code works in that it returns promise, any client that tries to hook up .success() and .error() handlers will also fail with this code. Even if the consumer decided to use .then() (which both $http and plain $q promises support) the values returned to the success and error handlers are different for the $q and $http callbacks.
So to get this to work properly you really have to return an $http compatible promise.
Some Helpers to make it Easier
Because this seems to be a common scenario that I run into, I created a couple of helpers to facilitate this scenario with a couple of helper functions that can fix up an existing deferred and/or create a new completed promise directly.
(function(undefined) {
ww = {};
var self;
ww.angular = {
// extends deferred with $http compatible .success and .error functions
$httpDeferredExtender: function(deferred) {
deferred.promise.success = function(fn) {
deferred.promise.then(fn, null);
return deferred.promise;
}
deferred.promise.error = function(fn) {
deferred.promise.then(null, fn);
return deferred.promise;
}
return deferred;
},
// creates a resolved/rejected promise from a value
$httpPromiseFromValue: function($q, val, reject) {
var def = $q.defer();
if (reject)
def.reject(val);
else
def.resolve(val);
self.$httpDeferredExtender(def);
return def.promise;
}
};
self = ww.angular;
})();
.$httpDeferredExtender() takes an existing, traditional promise and turns it into an $http compatible promise, so that it has .success() and .error() methods to assign to.
Using this extender you can now get the code that manually creates a $q deferred, to work like this:
function getAlbums(noCache) {
// if albums exist just return
if (!noCache && service.albums && service.albums.length > 0) {
var def = $q.defer();
def.resolve(service.albums);
ww.angular.$httpDeferredExtender(def);
return def.promise;
}
return $http.get("../api/albums/")
.success(function (data) {
service.albums = data;
})
.error(onPageError);
}
It works, but there’s a slight downside to this approach. When both the success and error handlers are hooked up two separate promises are attached. Both are called because you can attach multiple handlers to a single promise but there’s a little bit of extra overhead for the extra mapping.
Moar Simpler
Because the most common scenario for this is to actually return a resolved (or rejected) promise, an even easier .$httpPromiseFromValue() helper allows me to simply create the promise directly inside of the helper which reduces the entire code to a single line:
function getAlbums(noCache) {
if (!noCache && service.albums && service.albums.length > 0)
return ww.angular.$httpPromiseFromValue($q, service.albums);
return $http.get("../api/albums/")
.success(function (data) {
service.albums = data;
})
.error(onPageError);
}
This really makes it easy to return cached values consistently back to the client when the client code expects an $http based promise.
Related Resources
Other Posts you might also like