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.

$routeProvider

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.

$location

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.

Controller

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.

Hashbang

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.

HTML5 History

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.

templateUrl

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.

otherwise

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.

Events

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.

Registering an event handler

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.

RESTful resources

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.

redirectTo

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.

$routeParams

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.

Conclusion

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.

I hope you have been enjoying Angular Basics.
You can join the mailing list to hear about updates to the book.
Please also let me know what you liked and what I can improve.
And please share the word using the social buttons below!