While some Angular applications are just widgets in a traditional Web page, the vast majority are single-page applications, or SPAs, that replace browser-based navigation of Web pages with their own views and transitions. This approach can provide the user with a wonderfully responsive experience. However, if we write SPAs in a naive way, we lose something of great value in browser-based applications: the URL. In a simplistic SPA, there is only a single URL, and no way to share links to individual resources, such as a specific comment on a specific blog post. By contrast, a traditional, server-side web application that follows the RESTful style will typically manage its views as a hierarchy of URLs that are well-organized, easy-to-read, and accurately capture the current state of the application. These URLs offer the user a way to return to an application state at a later time, or share that state with others.
In traditional web applications, routing is the mapping of HTTP methods (GET, POST, and so on) and URL patterns to controllers. If you are unfamiliar with server-side routing, the guide to Express routing provides a fairly easy-to-follow introduction that is in JavaScript. However, client-side routing flips the situation on its head. Instead of reacting to actions that are communicated via the browser, we need to keep the browser updated as our application reacts directly to user input. For example, how can we keep the browser's navigation bar updated with accurate links? And how do we respond to links that are entered into the navigation bar or loaded from bookmarks? Fortunately, the ngRoute
module exists to help us with the job.
In order to use the module, we need to load the angular-route.js
file in addition to the main Angular library file. We will also load the Bootstrap CSS framework for styling.
<base href="/">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-route.js"></script>
Note the declaration of a base
element on the first line of the example above. This element identifies the base URL for our Angular application, and is required by Angular's support for HTML5 history API (covered below). If you are wondering why the href
attribute is set to the root path, rather than to the path for this page, it is because each example in this book is served in its own iframe
.
<body ng-app="app">
<!-- Other examples in this chapter will be inserted here. -->
</body>
Next, we need to require ngRoute
as we initialize our root module, as was covered in depth in the earlier chapter on Modules.
angular.module('app', ['ngRoute']);
We now have the ngRoute
module loaded into our application.
In order to use ngRoute
, we need to map one or more relative URL paths to handlers. The module.config
function allows us to configure modules such as ngRoute
as they are loaded by Angular. The trick is to know the name of the configuration service, or provider, for the module. In the case of ngRoute
, the provider is named $routeProvider
. With this name in our config
callback, we can chain route mappings using the when
function. The first argument to when
is the relative path for the route. The second is a configuration object that specifies how to render content for the route.
angular.module('app')
.config(function($routeProvider) {
$routeProvider
.when('/', {
template: "<a href='#/about'>About</a>"
})
.when('/about', {
template: "<a href='#/'>Home</a>"
});
});
The template
option shown above is similar to the one used to configure directives. In this example there is no dynamic content, just static HTML. Each template provides a link to the relative path for the other, so that in the example we will be able to toggle between the two. You may be wondering where the rendered content will be placed. Good question. The ngRoute
module provides a special directive, ng-view
, that must be present in our main template in order for the module to work. For the examples in this chapter, an empty div
element with ng-view
is the only content in our main template. Everything else will be rendered by the handler for the current route.
<div ng-view></div>
Click on the link in the example above to render the link for the other resource. Pretty slick, right? Well, there is one important thing missing. If you glance at your browser's navigation bar while clicking the links, you won't see it change to reflect the correct relative paths. In fact, there is nothing wrong with the example code, and in a real-life application you would see the change. The issue is merely with the interactive environment that hosts the examples in this book.
Each interactive example in this book is sandboxed within its own iframe. (Take a look at Codecademy/stuff.js if you're curious how this is done.) This is great for isolating the programming environment, but it prevents you from viewing the URL for the example in the navigation bar of your browser. Fortunately, there is a workaround that we can apply, with the added benefit of learning about a helpful Angular service for inspecting the current URL, the $location
service.
angular.module('app')
.controller('LocationController', function($scope, $location) {
$scope.location = $location.absUrl();
});
The example above declares a simple controller that receives the $location
service via dependency injection, and uses it to access the current absolute URL for the application. The value is a combination of the static URL root belonging to the enclosing page and the path extension that is dynamically managed by the ngRoute
module.
Although we could load the LocationController
shown above in the standard way using the familiar ng-controller
directive, it is also possible to configure a controller as an option. We will also add the location
scope property to our templates.
angular.module('app')
.config(function($routeProvider) {
$routeProvider
.when('/', {
controller: 'LocationController',
template: '<div class="well well-sm" ng-bind="location"></div>\
<a href="#/about">About</a>'
})
.when('/about', {
controller: 'LocationController',
template: '<div class="well well-sm" ng-bind="location"></div>\
<a href="#/">Home</a>'
});
});
There we go: We now have a simulated browser navigation bar for our examples. Unfortunately, you can't modify it in order to see how the ngRoute
module handles changes by rendering and display new content. That is something you'll have to experience in your own applications.
By default, the ngRoute
module expects relative URL paths to begin with a hash character (#
). You can easily change this to the hashbang prefix (#!
) by adding configuration for the $locationProvider
services, as shown below. Note that we must also add a !
character to the links in our templates.
angular.module('app')
.config(function($locationProvider) {
$locationProvider.hashPrefix('!');
})
.config(function($routeProvider) {
$routeProvider
.when('/', {
controller: 'LocationController',
template: '<div class="well well-sm" ng-bind="location"></div>\
<a href="#!/about">About</a>'
})
.when('/about', {
controller: 'LocationController',
template: '<div class="well well-sm" ng-bind="location"></div>\
<a href="#!/">Home</a>'
});
});
The hashbang prefix style is still endorsed by Google in its Webmasters' guide, in the section Making AJAX Applications Crawlable, which states that "hash fragments have to begin with an exclamation mark." However, as long as you take the steps necessary to ensure adequate SEO for your site, you may decide that your application is best served by the prefix-free style that is now enabled by HTML5.
The window.history
object offers navigation control in modern browsers, providing a seamless experience for the user.
angular.module('app')
.config(function($locationProvider) {
$locationProvider.html5Mode(true);
});
We can write our href
values as simple paths, without #
or #!
prefixes.
angular.module('app')
.config(function($routeProvider) {
$routeProvider
.when('/', {
controller: 'LocationController',
template: '<div class="well well-sm" ng-bind="location"></div>\
<a href="/about">About</a>'
})
.when('/about', {
controller: 'LocationController',
template: '<div class="well well-sm" ng-bind="location"></div>\
<a href="/">Home</a>'
});
});
Using HTML5 history is a great approach if your application is not required to support legacy browsers.
Let's take a minute to refactor our small application. Just as with directives, we can make our codebase more manageable by extracting our templates (even small ones) to separate files. We just need to replace the template
properties in our configuration with templateUrl
properties.
angular.module('app')
.config(function($routeProvider) {
$routeProvider
.when('/', {
controller: 'LocationController',
templateUrl: '/views/index.html'
})
.when('/about', {
controller: 'LocationController',
templateUrl: '/views/about.html'
});
});
Our About page remains unchanged.
<div class="well well-sm" ng-bind="location"></div>
<a href="/">Home</a>
<h4>About</h4>
Our next topic will be handling invalid URL paths, so let's add one now.
<div class="well well-sm" ng-bind="location"></div>
<a href="/about">About</a> |
<a href="/bad-path">Bad path</a>
<h4>Home</h4>
Click Bad path
in the example above. Ooops, the fun is over, and the only way to restore the example is to refresh this page.
What can we do to avoid bad endings like this? Not providing links to invalid paths would seem to be one answer, but in a real application we can't stop the user from typing into the navigation bar. The server can be configured to redirect any bad path to the root of our Angular application, but typically this means a full-page refresh that will restart our app again from scratch. How do we implement a client-side redirect for invalid paths?
Assuming that we would prefer to let the user know that the path is bad, rather than just redirect to the root path, the first thing we need is the view that will be shown for any invalid path. We'll call this template 404.html
to adhere to convention, but it could be named anything.
<div class="well well-sm" ng-bind="location"></div>
<a href="/">Home</a>
<h4 class="text-danger">404 - Not Found</h4>
Another when
statement is all we need to add the /404
route. Then, to route any unmatched paths to /404
, we simply append a call to otherwise
that lists the route in its redirectTo
property. In this example, the config
call below will be run in addition to the configuration shown above. (Yes, you can register as many config
callbacks to a provider as you like.)
angular.module('app')
.config(function($routeProvider) {
$routeProvider
.when('/404', {
controller: 'LocationController',
templateUrl: '/views/404.html'
})
.otherwise({
redirectTo: '/404'
});
});
Click Bad path
again. Although the example environment does not allow you to enter an arbitrary path in the browser bar, you can edit href="/bad-path"
in the template example to any value you like and see it handled. You should probably always add a similar catch-all handler to your routing configuration.
Angular provides an implementation of publish-subscribe messaging for application events. It is based on three functions: $emit
, $broadcast
, and $on
. While $emit
and $broadcast
exist to enable the publication of custom events, we can use the last of these methods, $on
, to register handlers for the events that are published by ngRoute
. A successful route change, for example, will result in the publication of the following events:
These events are listed in their order of appearance during the lifecycle of a route change. But don't take my word for it. Let's prove it.
Let's use $on
to register a simple handler for route events that will collect their names in an array. Later, we will print the event names to the page in the order that they were logged.
angular.module('app')
.value('eventsLog', [])
.factory('logEvent', function(eventsLog) {
return function(event) {
eventsLog.push(event.name);
};
});
In this example, we will record events as we transition from the root path (served by the HomeController
) to the /events
path (served by the EventsController
). In order to also see the invocations of the controller functions, each controller will also add its name to the eventsLog
.
angular.module('app')
.controller('HomeController', function($scope, $location, $rootScope, eventsLog, logEvent) {
$scope.location = $location.absUrl();
$scope.link = {path: '/events', title: 'Events'};
eventsLog.push("HomeController: " + $location.path());
$rootScope.$on('$routeChangeStart', logEvent);
$rootScope.$on('$locationChangeStart', logEvent);
$rootScope.$on('$locationChangeSuccess', logEvent);
$rootScope.$on('$routeChangeSuccess', logEvent);
$scope.eventsLog = eventsLog;
});
The EventsController
adds its name to the eventsLog
, then exposes the log to the scope so that we can see it in the page.
angular.module('app')
.controller('EventsController', function($scope, eventsLog, $location) {
$scope.location = $location.absUrl();
$scope.link = {path: '/', title: 'Home'};
eventsLog.push("EventsController: " + $location.path());
$scope.eventsLog = eventsLog;
});
The same view will work for both controllers. In addition to a dynamic navigation link, it displays the log contents using ng-repeat
.
<div class="well well-sm" ng-bind="location"></div>
<a ng-href="{{link.path}}">{{link.title}}</a>
<ol>
<li ng-repeat="event in eventsLog track by $index">
{{event}}
</li>
</ol>
As a side note, Angular's built-in json
filter is handy for debugging and logging. If we wanted to see more than just the event's name
property, we could use it to display the entire contents of the event object.
To complete the example, we just need to write a straightforward configuration for the two routes.
angular.module('app')
.config(function($routeProvider) {
$routeProvider
.when('/', {
controller: 'HomeController',
templateUrl: '/views/events/index.html'
})
.when('/events', {
controller: 'EventsController',
templateUrl: '/views/events/index.html'
});
});
Click the navigation link in the example above to see the events logged on the page.
The RESTful style of Web application development offers a set of conventions for organizing CRUD operations on both collections and individual resources. Angular does not require you to take a RESTful approach to routing, unless you load the ngResource
module, which is not covered by this book. However, to gain experience with some practical issues in routing, we will conclude this chapter by implementing just a bit of RESTful routing.
To begin with, we need a resource collection. The next chapter, HTTP, will demonstrate how to load data from a backend server. For now, let's just inject an array of item
objects into a controller.
angular.module('app')
.factory('Item', function(filterFilter) {
var items = [
{id: 1, name: 'Item 1', color: 'red'},
{id: 2, name: 'Item 2', color: 'blue'},
{id: 3, name: 'Item 3', color: 'red'},
{id: 4, name: 'Item 4', color: 'white'}
];
return {
query: function(params) {
return filterFilter(items, params);
},
get: function(params) {
return this.query(params)[0];
}
};
});
Each item has a unique id
property, enabling us to handle it as an individual resource. The service returned by our factory
encapsulates access to the items
array with two handy functions, query
for the resource collection and get
for the individual resources. The query
function uses the unfortunately-named filter
filter (injected with filterFilter
) to perform a query by example using the params
argument. The get
function piggy-backs on the functionality of query
to return a single resource. (See the Filters chapter if you missed it for more information on Angular filters.)
For our RESTful application, the first thing we want is an index
or list route that exposes the entire items
collection. All the controller needs to do is to expose the entire contents of items
by calling query
with no arguments.
angular.module('app')
.controller('ItemsController', function($scope, $location, Item) {
$scope.location = $location.absUrl();
$scope.items = Item.query();
});
The view for this controller is equally simple, using ng-repeat
to display the items.
<div class="well well-sm" ng-bind="location"></div>
<a ng-href="/">Home</a>
<ol>
<li ng-repeat="item in items">
{{item.name}} - {{item.color}}
</li>
</ol>
In this initial example, our routing configuration maps the root path (/
) to the items
collection view.
angular.module('app')
.config(function($routeProvider) {
$routeProvider
.when('/', {
controller: 'ItemsController',
templateUrl: '/views/items/index.html'
});
});
Because the path is not the pluralized resource name (/items
), this first example is not really very RESTful. The problem is that we need to serve something at the root path, which is where the Angular application loads by default. The solution, shown in the next example, is to immediately redirect from the root path to /items
.
In order to redirect automatically from one path to another, simply use the redirectTo
property instead of a controller and template.
angular.module('app')
.config(function($routeProvider) {
$routeProvider
.when('/', {
redirectTo: '/items'
})
.when('/items', {
controller: 'ItemsController',
templateUrl: '/views/items/index.html'
});
});
As you can see in the location bar above, the items
collection is now displayed at /items
. No matter how many times you click the Home
link, you will never stay at the root path (/
), but will always land at /items
. This is a simple usage of the redirectTo
option. If you need to apply some logic to your redirect, you can supply a function instead of a string path.
As discussed at the beginning of this chapter, URL query strings provide a representation of application state (such as the filtering of a resource) that can be easily saved and shared. How can we handle a query string that requests only those items that have a color
value of red
?
angular.module('app')
.config(function($routeProvider) {
$routeProvider
.when('/', {
controller: 'LocationController',
template: '<div class="well well-sm" ng-bind="location"></div>\
<a ng-href="/items?color=red">Red items</a>'
})
.when('/items', {
controller: 'ItemsController',
templateUrl: '/views/items/index.html'
});
});
In this example, our root path view contains the navigation link with the query string color=red
. We can easily retrieve this parameter value using the $location
service's search
function.
angular.module('app')
.controller('ItemsController', function($scope, $location, Item) {
$scope.location = $location.absUrl();
$scope.items = Item.query({color: $location.search().color});
});
Click the Red items
link above. You can now see how the query
method that we defined earlier in our Items
service uses the filter
filter to query by example.
The RESTful style typically embeds unique resource identifiers in the path rather than in the query string. How can we correctly handle a path that contains a unique identifier? Specifically, how do we extract the unique identifier 3
from the path /items/3
?
Let's update our collection view to include this style of navigation link to each individual resource.
<div class="well well-sm" ng-bind="location"></div>
<p class="lead">Items</p>
<ol>
<li ng-repeat="item in items">
<a ng-href="/items/{{item.id}}">
{{item.name}}
</a>
</li>
</ol>
The $routeParams
service provides convenient access to elements of the path, exposing them as named properties. The unique identifier for a singular RESTful resource is the last segment of the path. For example, the path /items/3
should return a representation of the Item
resource with unique identifier 3
.
In our routing configuration, we can use the special prefix :
to identify dynamic named parameters that should be extracted from the path. By convention, the identifier parameter for a resource is typically named :id
, so our path string is /items/:id
.
angular.module('app')
.config(function($routeProvider) {
$routeProvider
.when('/', {
redirectTo: '/items'
})
.when('/items', {
controller: 'ItemsController',
templateUrl: '/views/items/linked-index.html'
})
.when('/items/:id', {
controller: 'ItemController',
templateUrl: '/views/items/show.html'
});
});
The module places the extracted path parameters into the $routeParams
service, which we must inject into the ItemController
along with our own Item
service.
angular.module('app')
.controller('ItemController', function($scope, $location, Item, $routeParams) {
$scope.location = $location.absUrl();
$scope.item = Item.get({id: $routeParams.id});
});
Using the value that has been set for us at $routeParams.id
, we call Item.get
, which again uses the filter
filter to locate the correct model. (The core library function Array.prototype.find()
may offer a better way to do this once it reaches widespread adoption.)
Displaying the individual item
resource is straightforward. We'll include a link back to /items
so that we can return to the list view.
<div class="well well-sm" ng-bind="location"></div>
<a ng-href="/items">Items</a>
<p class="lead">{{item.name}}</p>
<p>Color: {{item.color}}</p>
Both $routeParams
and $location.search()
are quite simple to use, but together provide important building blocks that will be used in any approach to routing, including the RESTful style.
In this chapter we learned the basics of the Angular project's home-grown solution for routing. Although ngRoute
does not offer some of the more sophisticated routing features, such as support for nested resources, a first-class state machine, and generated URL paths, it does provide a great introduction to routing with Angular. Many real-world Angular projects use UI-Router from the AngularUI project instead, which won't be covered in this book. Instead, our final chapter will provide an introduction to loading data from a backend server, using Angular's $http
service.