Diving deep into the AngularUI Router

AngularJS is packed with out-of-the-box features that we can use to build an expressive AngularJS app without relying on separate libraries; however, the very active AngularJS community has also built some great libraries that we can take advantage of to maximize the power of our apps.

This post is the first post in our mini-series where we’re covering professional components of the AngularUI library. In this post, we’ll walk through the ui-router.

The AngularUI library has been broken out into several modules so that, rather than including the entire suite, we can pick and choose the components that we’re interested in using.

Quick links: we’ve included two major demos in this article that we are constantly asked about in our classes.

UI-Router

The ui-router library is one of the most useful that the AngularUI library gives us. It’s a routing framework that allows us to organize our interface by a state machine, rather than a simple URL route.

This library provides for a lot of extra control in our views. We can created nested views, use multiple views on the same page, have multiple views that control a single view, and more. For finer grain control and more complex applications, the ui-router is a great library to harness.

Installation

To install the ui-router library, we can either download the release or use Bower:

1
$ bower install angular-ui-router --save

We need to link the library to our view:

1
<script type="text/javascript" src="app/bower_components/angular-ui-router/release/angular-ui-router.js"></script>

And we need to inject the ui.router as a dependency in our app:

1
angular.module('myApp', ['ui.router'])

Now, unlike the built-in ngRoute service, the ui-router can nest views, as it works based off states, not just a URL.

Instead of using the ng-view directive as we do with the ngRoute service, we’ll use the ui-view directive with ngRoute.

When dealing with routes and states inside of ui-router, we’re mainly concerned with which state the application is in as well as at which route the web app currently stands.

1
2
3
<div ng-controller="DemoController"> <div ui-view></div> </div>

Just like ngRoute, the templates we define at any given state will be placed inside of the <div ui-view></div> element. Each of these templates can include their own ui-view as well, which is how we can have nested views inside our routes.

To define a route, we use the .config method, just like normal, but instead of setting our routes on $routeProvider, we set our states on the $stateProvider.

1
2
3
4
5
6
7
.config(function($stateProvider, $urlRouterProvider) { $stateProvider .state('start', { url: '/start', templateUrl: 'partials/start.html' }) });

This step assigns the state named start to the state configuration object. The state configuration object, or the stateConfig, has similar options to the route config object, which is how we can configure our states.

template, templateUrl, templateProvider

We can set up templates on each of our views using one of the three following options:

For instance:

1
2
3
$stateProvider.state('home', { template: '<h1>Hello {{ name }}</h1>' });

Controller

Just like in ngRoute, we can either associate an already registered controller with a URL (via a string) or we can create a controller function that operates as the controller for the state.

If there is no template defined (in one of the previous options), then the controller will not be created.

Resolve

Using the resolve functionality, we can resolve a list of dependencies that we can inject into our controller. In ngRoute, the resolve option allows us to resolve promises before the route is actually rendered. Inside angular-route, we have a bit more freedom as to how we can use this option.

The resolve option takes an object where the keys are the names of the dependency to inject into the controller and the values are the factories that are to be resolved.

If a string is passed, then angular-route will try to match an existing registered service. If a function is passed, then the function is injected, and the return value of the function is the dependency. If the function returns a promise, it is resolved before the controller is instantitated and the value (just like ngRoute) is injected into the controller.

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
$stateProvider.state('home', { resolve: { // This will return immediately as the // result is not a promise person: function() { return { name: "Ari", email: "[email protected]" } }, // This function returns a promise, therefore // it will be resolved before the controller // is instantiated currentDetails: function($http) { return $http({ method: 'JSONP', url: '/current_details' }); }, // We can use the resulting promise in another // resolution facebookId: function($http, currentDetails) { $http({ method: 'GET', url: 'http://facebook.com/api/current_user', params: { email: currentDetails.data.emails[0] } }) } }, controller: function($scope, person, currentDetails, facebookId) { $scope.person = person; } })

URL

The url option will assign a URL that the application is at to a specific state inside our app. That way, we can get the same features of deep linking while navigating around the app by state, rather than simply by URL.

This option is similar to the ngRoute URL, but can be considered a major upgrade, as we’ll see in a moment.

We can specify the basic route like so:

1
2
3
4
5
$stateProvider .state('inbox', { url: '/inbox', template: '<h1>Welcome to your inbox</h1>' });

When the user navigates to /inbox, then the app will transition into the inbox state and fill the main ui-view directive with the contents of the template (<h1>Welcome to your inbox</h1>).

The URL can take several different options, which makes it incredibly powerful. We can set the basic parameters in the URL like we do in ngRoute:

1
2
3
4
5
6
7
8
$stateProvider .state('inbox', { url: '/inbox/:inboxId', template: '<h1>Welcome to your inbox</h1>', controller: function($scope, $stateParams) { $scope.inboxId = $stateParams.inboxId; } });

Now we’ll capture the :inboxId as the second component in the URL. For instance, if the app transitions to /inbox/1, then $stateParams.inboxId becomes 1 (as $stateParams will be {inboxId: 1}).

We can also use a different syntax:

1
url: '/inbox/{inboxId}'

The path must match the URL exactly. Unlike ngRoute, if the user navigates to /inbox/, the above path will work; however, if she navigates to /inbox, the above state will not be activated.

The path also enables us to use regex inside of parameters so that we can set a rule to match against our route. For instance:

1
2
3
4
5
6
7
// Match only inbox ids that contain // 6 hexidecimal digits url: '/inbox/{inboxId:[0-9a-fA-F]{6}}', // Or // match every url at the end of `/inbox` // to `inboxId` (a catch-all) url: '/inbox/{inboxId:.*}'

Note, we cannot use capture groups inside the route: The route resolver is not able to resolve the route.

We can even specify query parameters in our route:

1
2
3
// will match a route such as // /inbox?sort=ascending url: '/inbox?sort'

Nested routing

We can use the url parameter to append routes in order to provide for nested routes. Using it in this way enables us to support having multiple ui-views inside our page and our templates. For instance, we can nest individual routes inside of our /inbox route above.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$stateProvider .state('inbox', { url: '/inbox/:inboxId', template: '<div><h1>Welcome to your inbox</h1>\ <a ui-sref="inbox.priority">Show priority</a>\ <div ui-view></div>\ </div>', controller: function($scope, $stateParams) { $scope.inboxId = $stateParams.inboxId; } }) .state('inbox.priority', { url: '/priority', template: '<h2>Your priority inbox</h2>' });

Our first route will match as we expect from above. We now also have a second route, a child route that matches under the inbox route: Our syntax (.) indicates that it is a child.

/inbox/1 matches the first state, and /inbox/1/priority matches the second state. With this syntax, we can support a nested URL inside of the parent route. The ui-view inside of the parent view will resolve to the priority inbox.

Params

The params option is an array of parameter names or regexes. This option cannot be combined with the url option. When the state becomes active, the app will populate the $stateParams service with these parameters.

Views

We can set multiple named views inside of a state. This feature is a particularly powerful one in ui-router: Inside of a single view, we can define multiple views that we can reference inside of a single template.

If we set the views parameter, then the state’s templateUrl, template, and templateProvider will be ignored. If we want to include a parent template in our routes, we’ll need to create an abstract template the contains a template.

Let’s say we have a view that looks like:

1
2
3
4
5
<div> <div ui-view="filters"></div> <div ui-view="mailbox"></div> <div ui-view="priority"></div> </div>

We can now create named views that fill each of these individual templates. Each of the subviews can contain their own templates, controllers, and resolve data.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$stateProvider .state('inbox', { views: { 'filters': { template: '<h4>Filter inbox</h4>', controller: function($scope) {} }, 'mailbox': { templateUrl: 'partials/mailbox.html' }, 'priority': { template: '<h4>Priority inbox</h4>', resolve: { facebook: function() { return FB.messages(); } } } } });

In this example, we have two named views embedded inside an abstract view.

See it

Multiple named views

Full source

Abstract

We can never directly activate an abstract template, but we can set up descendants to activate.

Abstract templates can provide a template wrapper around multiple named views, or they can pass $scope objects to descendant children. We can use them to pass around resolved dependencies or custom data or simply to nest several routes under the same ‘url’ (e.g., have all routes under the /admin URL).

Setting up an abstract template is just like setting up a regular state, except that we’ll set the abstract property:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$stateProvider .state('admin', { abstract: true, url: '/admin', template: '<div ui-view></div>' }) .state('admin.index', { url: '/index', template: '<h3>Admin index</h3>' }) .state('admin.users', { url: '/users', template: '<ul>...</ul>' });

onEnter, onExit

Our app calls these callbacks when we transition into or out of a view. For both options, we can set a function we want called; these functions have access to the resolved data.

These callbacks give us the ability to trigger an action on a new view or before we head out to another state. It’s a good way to launch an “Are you sure?” modal view or request the user log in before they head into this state.

Data

We can attach arbitrary data to our state configObject. This data is similar in the resolve property, except that this data will not be injected into the controller, nor will promises be resolved.

Attaching data in this way is particularly useful when passing data to child states from a parent state.

Events!

Like the ngRoute service, the angular-route service fires events at different times during the state lifecycle. We can also attach actions to these events inside of our application by listening on the $scope.

All of the following events fire on the $rootScope, so we can listen to these events on any of our $scope objects:

State change events

We can listen to the events as follows:

1
2
3
4
5
$scope.$on('$stateChangeStart', function(evt, toState, toParams, fromState, fromParams), { // We can prevent this state from completing evt.preventDefault(); });

The events that fire are as follows:

$stateChangeStart

This event fires when the transition from one state to another begins.

$stateChangeSuccess

This event fires when the transition from one state to another is complete.

$stateChangeError

This event fires when an error occurs during the transition. Such errors are usually due to either a template that cannot be resolved or a resolve promise that fails to resolve.

View load events

The ui-router also provides events at the view loading stage:

$viewContentLoading

This event fires when the view begins loading and occurs before the DOM is rendered.

We can listen to this event like so:

1
2
3
4
5
6
$scope.$on('$viewContentLoading', function(event, viewConfig){ // Access to all the view config properties. // and one special property 'targetView' // viewConfig.targetView });
$viewContentLoaded

This event fires after the view has been loaded and after the DOM is rendered.

$stateParams

Above, we used the $stateParams to pick out the different params options from the url parameters. This service is how we’ll hand data different components of our url.

For instance, if we have a URL in our inbox state that looks like:

1
url: '/inbox/:inboxId/messages/{sorted}?from&to'

and our user finds their way to this route:

1
/inbox/123/messages/ascending?from=10&to=20

then our $stateParams object will result in:

1
{inboxId: '123', sorted: 'ascending', from: 10, to: 20}

$urlRouterProvider

Just like ngRoute, we have access to a route provider that we can use to build rules around what happens when a particular URL is activated.

The states that we create activate themselves at different URLs, so the $urlRouterProvider is not necessary for managing activating and loading states. It does come in handy when we want to manage events that happen outside of the scope of our states, such as with redirection or authentication.

We can use the $urlRouterProvider in our module’s config function.

when()

The when function takes two parameters: the incoming path that we want to match and the path that we want to redirect to (or a function that is invoked when we match the path).

To set up redirection, we’ll set the when method to take a string. For instance, if we want to redirect an empty route to our /inbox route:

1
2
3
.config(function($urlRouterProvider) { $urlRouterProvider.when('', '/inbox'); });

If we pass a function, then it will be invoked when we match the path. The handler can return one of three values:

otherwise()

Just like the otherwise() method in ngRoute, the otherwise() method here redirects a user if no other routes are matched. This method is a good way to create a default URL, for instance.

The otherwise() method takes a single parameter: either a string or a function.

If we pass in a string, then any invalid or unmatched routes will be redirected to the string as a specified URL.

If we pass in a function, it will be invoked if no other route is matched, and we’ll be responsible for handling the return.

1
2
3
4
5
6
7
8
.config(function($urlRouterProvider) { $urlRouterProvider.otherwise('/'); // or $urlRouterProvider.otherwise( function($injector, $location) { $location.path('/'); }); });
rule()

If we want to bypass any of the URL matching or want to do some route manipulation before other routes, we can use the rule() function.

We must return a valid path as a string when using the rule() function:

1
2
3
4
5
6
app.config(function($urlRouterProvider){ $urlRouterProvider.rule( function($injector, $location) { return '/index'; }); })

Create a sign-up wizard

Why does it matter if we use a new, more powerful router than the built-in ngRoute provider?

A useful case for using the ui-router is when we want to create a sign-up wizard to walk our users through the process of signing up for our service.

Using the ui-router, we’ll create a quick signup service with a single controller that can handle the signup.

First, we’ll create a view for the app:

1
2
3
4
<div ng-controller="WizardSignupController"> <h2>Signup wizard</h2> <div ui-view></div> </div>

Inside of this view, we’ll house our signup views. Next, in our signup wizard we’ll need three stages:

In a real app, the finish stage would likely send the registration data back to a server and handle real registration. Here, we have no back end, so we’ll just show the view.

Our signup process will depend on a wizardapp.controllers module that houses our WizardSignupController.

1
2
3
4
angular.module('wizardApp', [ 'ui.router', 'wizardapp.controllers' ]);

Our WizardSignupController simply houses the $scope.user object that we’ll carry with us through the signup process, as well as the signup action.

1
2
3
4
5
6
angular.module('wizardapp.controllers', []) .controller('WizardSignupController', ['$scope', '$state', function($scope, $state) { $scope.user = {}; $scope.signup = function() {} }]);

Now, the wizard process logic houses the majority of the work. Let’s set up this logic in the config() function of our app:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
angular.module('wizardApp', [ 'ui.router', 'wizardapp.controllers' ]) .config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) { $stateProvider .state('start', { url: '/step_1', templateUrl: 'partials/wizard/step_1.html' }) .state('email', { url: '/step_2', templateUrl: 'partials/wizard/step_2.html' }) .state('finish', { url: '/finish', templateUrl: 'partials/wizard/step_3.html' }); }]);

With these options set up, we have our basic flow all done. Now, if the user navigates to the route /step_1, they are brought to the beginning of the flow. Although it might make sense that our entire flow takes place at the root URL (i.e., /step_1), we might want to house that flow in a sublocation (/wizard/step_1, for instance).

To do so, we’ll set up an abstract state that houses the rest of our steps:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) { $stateProvider .state('wizard', { abstract: true, url: '/wizard', template: '<div><div ui-view></div></div>' }) .state('wizard.start', { url: '/step_1', templateUrl: 'partials/wizard/step_1.html' }) .state('wizard.email', { url: '/step_2', templateUrl: 'partials/wizard/step_2.html' }) .state('wizard.finish', { url: '/finish', templateUrl: 'partials/wizard/step_3.html' }); }]);

Now, instead of having our routes at a top level, we can nest them safely inside our /wizard URL.

To navigate between our different states, we’ll use the ui-router directive ui-sref on our links. This directive simply translates the href on the link to the next state.

For instance, our step_1.html looks like:

1
2
3
4
5
6
<!-- step_1.html --> <h3>Step 1</h3> <form ng-submit=""> <input type="text" ng-model="user.name" placeholder="Your name" /> <input type="submit" class="button" value="Next" ui-sref="wizard.email"/> </form>

We also want to attach an action that happens at the end of the signup process that calls our signup function on our parent controller WizardSignupController. We’ll set a controller on the final step of the wizard process that simply calls the function on the $scope. Because our entire wizard is encapsulated in our WizardSignupController, we’ll be able to use the nested scope property of scopes like normal.

1
2
3
4
5
6
7
.state('wizard.finish', { url: '/finish', templateUrl: 'partials/wizard/step_3.html', controller: function($scope) { $scope.signup(); } });

See it

Signup wizard

Full source

In this post, we covered the ui-router in-depth and almost the entirety of the features. We find the library incredibly useful and we hope you do too.

Feel free to ping us with any questions, comments, or just to say hey and stay tuned for news about our upcoming AngularJS book. It covers this topic and much much more. Sign up on the mailing list below to receive a free sample chapter of the book.

Get the weekly email all focused on AngularJS. Sign up below to receive the weekly email and exclusive content.
We will never send you spam and it's a cinch to unsubscribe.

Download a free sample of the ng-book: The Complete Book on AngularJS

ng-book: The Complete Book on AngularJS is the canonical AngularJS book available today.

It's free, so just enter your email address and the PDF will be sent directly to your inbox. Mailchimp can take up to an hour to deliver the free sample chapter, but if you don't receive it within the hour, send us an email and we'll manually send them to you!

We'll send you updates about the book, when it updates and other free content.

We will never send you spam and it's a cinch to unsubscribe.

Comments

comments powered by Disqus