The angularjs framework has a built-in $route
service that can be configured to handle route transitions in single-page web applications. It covers all the features that we were trying to handcraft using the $location
service and additionally offers other very useful facilities. We are going to get familiar with those step-by-step.
Note
Starting from Version 1.2, angularjs will have its routing system delivered in a separate file (angular-route.js
) with its own module (ngRoute
). When working with the latest version of angularjs, you will need to remember to include the angular-route.js
file and declare a dependency on the ngRoute
module.
Basic routes definition
Before diving into more advanced usage scenarios, let’s abandon our naïve implementation by converting our route definitions to the syntax used by the $route
service.
In angularjs, routes can be defined during the application’s configuration phase using the $routeProvider
service. The syntax used by the $routeProvider
service is similar to the one we were playing with in our custom $location
based implementation:
angular.module('routing_basics', []) .config(function($routeProvider) { $routeProvider .when('/admin/users/list', {templateUrl: 'tpls/users/list.html'}) .when('/admin/users/new', {templateUrl: 'tpls/users/new.html'}) .when('admin/users/:id', {templateUrl: 'tpls/users/edit.html'}) .otherwise({redirectTo: '/admin/users/list'}); })
The $routeProvider
service exposes a fluent-style API, where we can chain method invocations for defining new routes (when
) and setting up a default route (otherwise
).
Note
Once initialized, an application can’t be reconfigured to support additional routes (or remove existing ones). This is linked to the fact that angularjs providers can be injected and manipulated only in configuration blocks executed during the application’s bootstrap.
Tip
The content of a route can be also specified inline using the template
property of a route definition object. While this is a supported syntax, hardcoding the route’s markup in its definition is not very flexible (nor maintainable) so it is rarely used in practice.
Displaying the matched route’s content
When one of the routes is matched against a URL, we can display the route’s contents (defined as templateUrl
or template
) by using the ng-view
directive. The $location
-based version of the markup looked before as follows:
<div class="container-fluid" ng-include="selectedRoute.templateUrl">
<!-- Route-dependent content goes here -->
</div>
With ng-view
, this can be rewritten as follows:
<div class="container-fluid" ng-view>
<!-- Route-dependent content goes here -->
</div>
As you can see, we simply substituted the ng-include
directive for ng-view
one. This time we don’t need to provide an attribute value, since the ng-view
directive “knows” that it should display the content of the currently matching route.
Matching flexible routes
In the naïve implementation, we were relying on a very simple route matching algorithm, which doesn’t support variable parts in URLs. In fact it is a bit of a stretch to call it an algorithm, we are simply looking up a property on an object corresponding to URL’s path! Due to the algorithm’s simplicity, we were using a URL query parameter in the query string to pass around the user’s identifier as follows:
/admin/users/edit?user={{user.id}}
It would be much nicer to have URLs where the user’s identifier is embedded as part of a URL, for example:
/admin/users/edit/{{user.id}}
With the angularjs router, this is very easy to achieve since we can use any string prefixed by a colon sign (:
) as a wildcard. To support a URL scheme where the user’s ID is part of a URL we could write as follows:
.when('/admin/users/:userid', {templateUrl: 'tpls/users/edit.html'})
This definition will match any URLs with an arbitrary string in place of the :userid
wildcard, for example:
/users/edit/1234
/users/edit/dcc9ef31db5fc
On the other hand, routes with an empty :userid
, or the :userid
containing slashes (/
) won’t be matched.
Tip
It is possible to match routes based on parameters that can contain slashes, but in this case we need to use slightly modified syntax: *id
. For example, we could use the star-based syntax to match paths containing slashes: /wiki/pages/*page
. The route-matching syntax will be further extended in angularjs Version 1.2.
Defining default routes
The default route can be configured by calling the otherwise
method, providing a definition of a default, catch-all route. Please notice that the otherwise
method doesn’t require any URL to be specified as there can be only one default route.
Tip
A default route usually redirects to one of the already defined routes using the redirectTo
property of the route definition object.
The default route will be used in both cases where no path was provided as well as for cases where an invalid URL (without any matching route) triggers a route change.
Accessing route parameter values
We saw that route URLs definition can contain variable parts that act as parameters. When a route is matched against a URL, you can easily access the values of those parameters using the $routeParams
service. In fact, the $routeParams
service is a simple JavaScript object (a hash), where keys represent route parameter names and values represent strings extracted from the matching URL.
Since $routeParams
is a regular service, it can be injected into any object managed by the angularjs Dependency Injection system. We can see this in action when examining an example of a controller (EditUserCtrl)
used to update a user’s data (/admin/users/:userid
) as follows:
.controller('EditUserCtrl', function($scope, $routeParams, Users){ $scope.user = Users.get({id: $routeParams.userid}); ... })
The $routeParams
service combines parameter values from both the URL’s path as well as from its search parameters. This code would work equally well for a route defined as /admin/users/edit
with a matching URL: /admin/users/edit?userid=1234
.
Reusing partials with different controllers
In the approach taken so far, we defined a controller responsible for initializing the partial’s scope inside each partial using the ng-controller
directive. But the angularjs routing system makes it possible to define controllers at the route level. By pulling the controller out of the partial, we are effectively decoupling the partial’s markup from the controller used to initialize the partial’s scope.
Let’s consider a partial providing a screen for editing a user’s data:
<div ng-controller="EditUserCtrl">
<h1>Edit user</h1>
. . .
</div>
We could modify it by removing the ng-controller
directive as follows:
<div> <h1>Edit user</h1> . . . </div>
Instead of that we can define the controller service in the route level as follows:
.when('/admin/users/:userid', {
templateUrl: 'tpls/users/edit.html'
controller: 'EditUserCtrl'})
By moving the controller to the route definition level, we’ve gained the possibility of reusing the same controller for different partials and more importantly, reusing the same partials with different controllers. This additional flexibility comes in handy in several situations. A typical example would be a partial containing a form to edit an item. Usually we want to have exactly the same markup for both adding a new item and editing an existing item, but behavior for new items might be slightly different as compared to the existing ones (for example, creating rather than updating when persisting the item).
Comments