AngularJS is a popular JavaScript front-end framework that is mainly maintained by Google. This projects aims to make the HTML more readable by adding custom elements and controllers, so that a developer can understand the flow of the application by just reading the HTML, with no need to dig into the JavaScript code. AngularJS also provides routing capabilities and it is mainly used to create Single Page Applications. After having spent some time reading about this framework, I have decided to implement a very simple front-end application with AngularJS to better understand its potential. The result is a single page application that takes advantage of the New York Times API to fetch and display the top 20 best seller books.
There are several online courses that allow you to learn AngularJS for free. I’d suggest the W3School (an evergreen), Codecademy and Code School to start, but also Thinkster and YouTube, where users dump an impressive amount of step-by-step tutorials.
When I started working on this project, I wanted to focus on AngularJS, in order to learn it in the best possible way. AngularJS is a front-end framework, so I didn’t want to waste my energies in setting up databases and services. But I needed a datasource! After a while I finally found the PublicAPIs directory, which lists an impressive number of APIs that can be potentially used for similar use cases. An interesting alternative is the JSONPlaceholder project, which is a free REST service that can be used to test CRUD operations with fake data.
The little application described in the next sections is available on GitHub, so you may also consider to watch, fork or star the project!
The single page application of this example is divided into three pages. In the first one, the user can select a literary genre, while in the second the top 20 best-seller book of the selected genre are listed. Finally, a short description of the selected book, a link to the corresponding Amazon web page and the book review (if any) are displayed in the last section. A live demo of the web app is available online.
The first thing we need to do is to import the AngularJS library in the main
HTML file. After that, we modify the body tag of our index.html
page to
include the ngApp
directive. This instruction tells AngularJS which is
the root tag of the application.
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.js"</script>
<script src="src/js/app.js"</script>
</head>
<body ng-app="NYTBestSellers">
</body>
</html>
We also need to include a simple app.js
script, where we will initialize
the AngularJS application. In the script, we define a variable called app that
stores the AngularJS module. This module has the same name that we defined
through the ngApp
directive in the HTML file.
(function () {
"use strict";
var app = angular.module("NYTBestSellers", []);
})();
In more recent versions of AngularJS the routing module has been divided from the core library, so we need to import it in the main HTML, at line 4.
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.js"</script>
<script src = "http://ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular-route.js"></script>
<script src="src/js/app.js"</script>
</head>
<body ng-app="NYTBestSellers">
</body>
</html>
We need to define 3 different routes: the list of genres, the list of genre’s
best-sellers, and the details of the selected book. To do so, we create a file
called router.js
in src/js
, and we import this script in the index.html
.
(function () {
"use strict";
var app = angular.module("NYTBestSellers", ["ngRoute"]);
app.config(function ($routeProvider) {
$routeProvider
.when("/genres", {
templateUrl: "src/html/genres.html",
controller: "GenresController",
})
.when("/genres/:genre", {
templateUrl: "src/html/genre.html",
controller: "GenreController",
})
.when("/genres/:genre/:isbn", {
templateUrl: "src/html/book.html",
})
.otherwise({
redirectTo: "/genres",
});
});
})();
We need to import the ngRoute
module to allow users to navigate through the
pages. Routes are defined in the configuration of our AngularJS module,
through the use of the $routeProvider
object. Each route defines the endpoint
of each page (e.g. /genres
), but also the template (through the
templateUrl
instruction) and the controller (e.g. GenresController
)
associated with it. It is also possible to define dynamic parameters for the
route through the use of colons (e.g. :isbn
). The route defined at line 14
by the instruction otherwise will catch any other request and redirect the user
to the genres page.
We will now take advantage of another AngularJS directive to link this piece of
code to the HTML page. In the body section we define three areas: an header,
a footer, and the main content. Header and footer will remain the same in all
the pages, but we want the main content to change according to the user
navigation. To do so, we add the ngView
directive to our main div
.
AngularJS will render the templates defined in the routes.js
script in the
main div
, by leaving the rest of the page unaltered.
<body>
<body ng-app="NYTBestSellers">
<!-- Header. -->
...
<!-- Main content. -->
<div ng-view></div>
<!-- Footer. -->
...
</body>
</body>
In the router.js
file we defined, for each route, a template and a controller.
The latter is in charge of the business logic of the page, while the template
handles the presentation layer. To make the web app consume data through the
NYT API, we need to define its behaviour in the controller. For example,
we can define the GenresController
as follows:
(function () {
"use strict";
var app = angular.module("NYTBestSellers");
app.controller("GenresController", function ($scope, $routeParams, $http) {
var url =
"http://api.nytimes.com/svc/books/v3/lists/names.json?api-key=sample-key";
$http
.get(url)
.then(function (response) {
$scope.genres = response.data.results;
})
.catch(function (e) {
$scope.error = e;
});
});
})();
When it is defined, the controller imports three objects: $scope
,
$routeParams
and $http
. The first is used to exchange data with the
templates, the second to fetch data from the routes (e.g. /:isbn
), while
$http
is used to access remote resources. This object is used to invoke a
URL (and pass parameters to it, if required), and it returns a promise.
The result of the asynchronous call is then stored in the $scope
object, so
that it can be used by the template.
The template makes use of another AngularJS directive: ngRepeat
. In this
example, the template access the genres object from the $scope
, and iterates
over it. At each iteration a portion of HTML is filled with information from
the object (e.g. g.display_name
) and rendered.
<h1>
<div class="row">
<div class="col-lg-12 text-center">
<i class="fa fa-spinner fa-pulse fa-5x" ng-hide="genres.length"></i>
</div>
<div ng-repeat="g in genres">
<div class="col-lg-3">
<a href="#genres/{{g.list_name_encoded}}">
<div class="thumbnail">
<div class="caption">
<h5>{{g.display_name}}</h5>
<p class="date">
From {{g.oldest_published_date | date}} to
{{g.newest_published_date | date}}
</p>
</div>
</div>
</a>
</div>
</div>
</div>
</h1>
This example makes use of several built-in directives of AngularJS: ngApp
,
ngView
, ngRepeat
, and so forth. It is also possible to define custom
directives. The last route of our application shows the details of the
selected book. We want to define a new HTML tag, for example <book>
, that
will render all the information. Therefore, the template defined in the
route configuration will simply look like that:
<book></book>
We need to define the new directive, so we create a new script named book.js
with the following content:
(function () {
"use strict";
var app = angular.module("NYTBestSellers");
app.directive("book", function () {
return {
restrict: "E",
templateUrl: "src/html/directives/book.html",
controller: function ($scope, $routeParams, $http) {
$scope.id = $routeParams.id;
$scope.isbn = $routeParams.isbn;
var url =
"http://api.nytimes.com/svc/books/v3/lists/" +
$routeParams.genre +
".json?sort-by=rank&sort-order=ASC&api-key=sample-key",
i,
b;
$http
.get(url)
.then(function (response) {
for (i = 0; i < response.data.results.books.length; i += 1) {
b = response.data.results.books[i];
if (b.isbns[0].isbn13 === $routeParams.isbn) {
$scope.book = b;
break;
}
}
})
.catch(function (e) {
$scope.error = e;
});
},
};
});
})();
The restrict
option tells AngularJS that we are creating a new element, but
it is also possible to create attributes, comments and classes. The
templateUrl
defines the HTML file that will be rendered inside our new tag,
while the controller
defines the business logic of the new directive.
AngularJS is a very powerful front-end framework that allows you to create single page applications. It lets you modularize your application, define routes and bind together the data and the presentation layers. Everything you may need, in one framework. This can be an advantage, but also a limitation. An alternative solution is to combine several libraries to achieve the same goal of AngularJS, as per the (simplistic) image below.
Handlebars (or Mustache, or Spacebars, or...) could be used for the templating, RequireJS for the modularization, Backbone for the routing, and Q for the promises. Which are the pros and cons of AngularJS compared to the alternative scenario?
Nothing is completely black or white, of course. For example, it is still possible to use AngularJS with Require, and it is probably possible to introduce other technologies as well. But then things start becoming a bit messy, so it would be probably better to chose either AngularJS or a components-based approach. Should I use AngularJS then? Same old song and dance: it depends! On the requirements, time available, resources. But AngularJS is definitely a valid framework, and I strongly suggest you to give it a shot!