In this tutorial, we will keep an eye on “Promises in AngularJS”
We first looked at fetching categories from a server by using $http
directly inarticleCtrl
. For this to work, we would have to undo some of the decoupling we previously achieved by having calculateCategoryPercentage
as a factory service with a dependency on availableCategories
.
Let’s now return to that and see if we can extract $http
from articleCtrl
and use it in availableCategories
. If we can achieve that, articleCtrl
andcalculateCategoryPercentage
can become oblivious to how the available categories are obtained. This is a more advanced technique that will help keep code flexible as our applications grow.
We glossed over it in our simple illustration of $http
, but the get
method made the HTTP request (which is inherently asynchronous) and immediately returned a Promise object. A Promise object represents the future value of an asynchronous operation. If the operation succeeds, then the Promise is ‘resolved’; if it fails, then the Promise is ‘rejected’. A Promise object exposes several methods, one of whichthen
allows us to register a resolution and/or rejection handler. In our simple usage, we just registered a resolution handler as follows:
1 2 3 |
$http.get('/categories').then(function(response) { $scope.categoryPercentage = calculateCategoryPercentage($scope.articles, response.data); }); |
Promises can, without doubt, get reasonably complicated. If you want to learn more, I suggest studying the readme for the Q library. AngularJS implements a lightweight version of the Q library.
Now that we’ve briefly introduced Promises, let’s look at how we can use them to get our decoupling back. In the following snippets, I’ve added numbered comments, which we’ll discuss in detail shortly.
Edit controllers.js
as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
angular.module('tutsocean').controller('articleCtrl', function($scope, calculateCategoryPercentage, pageSize, availableCategories) { $scope.categories = []; availableCategories.then(function(categories) { Array.prototype.splice.apply( $scope.categories, [$scope.categories.length, 0].concat(categories) ); }); $scope.articles = [ { title: "HTML Tutorial", categories: ['tutorial', 'hardware'] }, { title: "css Tutorial", categories: ['tutorial', 'graphics'] }, { title: "jQuery Tutorial", categories: ['tutorial'] } ]; $scope.$watch('articles', function(articles) { // 3 calculateCategoryPercentage(articles).then(function(percentage) { // 6 $scope.categoryPercentage = percentage; }); }, true); $scope.containsCategory = function(article, category) { return article.categories.indexOf(category) >= 0; }; $scope.toggleCategory = function(article, category) { var index = article.categories.indexOf(category); if (index == -1) { article.categories.push(category); } else { article.categories.splice(index, 1); } }; $scope.newTitle = ''; $scope.addArticle = function() { $scope.articles.push({ title: $scope.newTitle, categories: [] }); }; $scope.numArticles = pageSize; }); |
Edit services.js
as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
angular.module('tutsocean') .value('pageSize', 1) .factory('availableCategories', function($http) { // 1 return $http.get('/categories').then(function(response) { return response.data; }); }) .factory('calculateCategoryPercentage', function(availableCategories) { // 2 return function calculate(articles) { var uniqueCategories = []; articles.forEach(function(article) { article.categories.forEach(function(category) { if (uniqueCategories.indexOf(category) == -1) { uniqueCategories.push(category); } }); }); // 4 return availableCategories.then(function(categories) { // 5 return Math.floor(100 * (uniqueCategories.length / categories.length)); }); }; }); |
In the above snippets, the numbered comments relate to the following points:
articleCtrl
depends oncalculateCategoryPercentage
which depends onavailableCategories
. The Promise object returned from$http.get('/categpries').then
is registered asavailableCategories
. Note thatÂavailableCategories
has been made a factory service so that it, too, can have a dependency injected (namely,Â$http
).calculateCategoryPercentage
is next in the dependency chain, so the functioncalculate
is registered.articleCtrl
runs as the last step in the dependency chain. It callscalculateCategoryPercentage
each time its articles change (via$scope.$watch
). A Promise object is returned, andarticleCtrl
assigns a resolution handler.- A resolution handler is assigned to the
availableCategories
Promise object. Assigning resolution handlers viathen
returns another Promise object, which allows for chained resolution. availableCategories
is resolved (i.e., a response is received from the server), and the category percentage is calculated and returned.- The chained resolution set in step 4 allows
articleCtrl
to set the category percentage as a$scope
property.
You may wonder about the benefit of this approach over the simpler use of $http
direct in articleCtrl
we had previously. In both approaches, we have had to change how calculateCategoryPercentage
is used in articleCtrl
. In this approach, the change has been to work with Promises. Promises are a very general API. For example, in the future our application could first look in the browser’s local storage for categories before resorting to an HTTP server call. The Promise API thatarticleCtrl
works with wouldn’t change one bit, but behind the scenes, obtaining the categories would be more involved. With Promises, articleCtrl
has no insight into how the categories are obtained for the calculation, just that somehow they are.
$resource
Until now, the initial articles in our blog admin application have been hard-coded in articleCtrl
. This clearly isn’t the most flexible application around; as such, the requirements have changed yet again.
We’re now asked to provide a means of retrieving and creating articles stored on a server. The server has provided us a RESTful API for interacting with articles. Sending an HTTP GET request to the URL ‘/articles’ will return an array of articles, while sending an HTTP POST request will create and return a new article. $resource
is another additional module and is perfect for working with this type of API.
Download angular-resource.js and add another script reference in index.html
in the following order:
1 2 3 4 5 6 |
<script src="angular.js"></script> <script src="angular-resource.js"></script> <script src="angular-mocks.js"></script> <script src="app.js"></script> <script src="controllers.js"></script> <script src="services.js"></script> |
As with $http
previously, the simplest way to get up and running with $resource
is to use it directly in articleCtrl
. We could encapsulate $resource
in another factory service so that articleCtrl
isn’t aware of how articles are retrieved and created. For our purposes, the first approach allows us to focus on the detail of using $resource
, but in a larger real-world application, I would certainly consider the latter approach.
Edit app.js
as follows:
1 2 3 |
angular.module('tutsocean', ['ngResource', 'ngMockE2E']).run(function($httpBackend) { $httpBackend.whenGET('/categories').respond(['tutorial', 'hardware', 'graphics']); }); |
The change in the above snippet is a simple one: we’ve just added an additional dependency for the ‘udemyAdmin’ module, namely ‘ngResource’.
Edit controllers.js
as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
angular.module('tutsocean').controller('articleCtrl', function($scope, calculateCategoryPercentage, pageSize, availableCategories, $resource) { var Article = $resource('/articles'); $scope.categories = []; availableCategories.then(function(categories) { Array.prototype.splice.apply( $scope.categories, [$scope.categories.length, 0].concat(categories) ); }); $scope.articles = Article.query(); $scope.$watch('articles', function(articles) { calculateCategoryPercentage(articles).then(function(percentage) { $scope.categoryPercentage = percentage; }); }, true); $scope.containsCategory = function(article, category) { return article.categories.indexOf(category) >= 0; }; $scope.toggleCategory = function(article, category) { var index = article.categories.indexOf(category); if (index == -1) { article.categories.push(category); } else { article.categories.splice(index, 1); } }; $scope.newTitle = ''; $scope.addArticle = function() { var newArticle = new Article({ title: $scope.newTitle }); newArticle.$save().then(function(article) { $scope.articles.push(article); }); }; $scope.numArticles = pageSize; }); |
In the above snippet, $resource
is declared as a dependency of articleCtrl
and subsequently injected in. We create a resource object Article
by invoking $resource
with our URL.
We then invoke Article.query
to retrieve an array of articles from the server. This results in an HTTP GET request to ‘/articles’. As this is an asynchronous operation, it does not block the application waiting for the server to respond, but what value would be useful to return in the meantime? $resource
immediately returns an empty array, which will be filled when the server responds. This is a neat trick, as combined with setting the array as a property on $scope
, any binding we make in our HTML will automatically update.
Continuous
We haven’t looked at the contents of index.html
for a while, but the usage of ng-repeat
doesn’t need to change, even though we’ve switched to $resource
and$scope.articles
is, at first, an empty array.
Returning to the snippet of articleCtrl
above, when $scope.addArticle
is invoked we now create a new instance of Article
with $scope.newTitle
as its title. We then invoke$save
resulting in an HTTP POST request to ‘/articles’. A Promise object is returned, which is resolved by the server responding with an HTTP 200 status code and body. A instance of Article
is created from the server response for us, and we simply push it onto $scope.articles
. The usage of $scope.addArticle
and$scope.newTitle
in index.html
does not need to change.
$httpBackend again
As $resource
is an extension of $http
, we can also use $httpBackend
to configure how requests to the URL ‘/articles’ should be handled.
Edit app.js
as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
angular.module('tutsocean', ['ngResource', 'ngMockE2E']).run(function($httpBackend) { $httpBackend.whenGET('/categories').respond(['tutorial', 'hardware', 'graphics']); var articles = [ { title: "HTML Tutorial", categories: ['tutorial', 'hardware'] }, { title: "CSS Tutorial", categories: ['tutorial', 'graphics'] }, { title: "jQuery Tutorial", categories: ['tutorial'] } ]; $httpBackend.whenGET('/articles').respond(articles); $httpBackend.whenPOST('/articles').respond(function(method, url, data) { var article = angular.fromJson(data); article.categories = article.categories || []; articles.push(article); return [200, article, {}]; }); }); |
In the above snippet, we’ve simply moved the hard-coded array of articles fromarticleCtrl
and returned it whenever an HTTP GET request is made to ‘/articles’. An HTTP POST request to ‘/articles’ pushes another article onto the array, meaning that any subsequent GET requests would include it.