AngularJS is an amazing JavaScript framework for building rich client-side applications but the truth is many developers don’t use the built-in router. They instead use the AngularUI project’s UI-Router because it has two important features: multiple views and nested views. This article explains these features and why they are important and shows you a real-world example of using these features together.
Why You Should Use UI-Router
Multiple Views
Most applications can be broken up into regions. At a minimum, applications usually have a header, a main content area, and a footer.
Commonly, applications may have an additional sidebar on the left or right side of the page as shown below.
In most use cases, all of these regions (views) are shown on the page at the same time. With the built-in AngularJS router, ngRoute
, only one view (ng-view
) is allowed per page. This limitation causes people to use includes (ng-include) or other workarounds to create a layout or master page for their application. UI-Router supports multiple views and each can have it’s own corresponding Controller so that each of these regions can be encapsulated and reused throughout the application if needed.
Nested Views
The common example of a nested view in applications is a master/detail or, more specifically, a list/detail page. Many applications show a list of items then when you click on an item you see the detail for that item. Taking this example further, you might then click an edit link when viewing the item’s details that takes you to an editable form for the item (see the diagram below to visualize).
This scenario is easily achieved with the built-in AngularJS router, ngRoute
, if the list and detail are on separate pages (or views as they are called in AngularJS). However, if you want the list to remain on the page while you show the detail to the right or below the list this becomes more challenging. To be clear, this requirement can be achieved with ngRoute
by sharing a single view with two controllers: one for the list and one for the detail and hiding and showing the detail as needed. The result is not ideal because we would like the list and detail to each have their own controller and view with only one responsibility (showing a list or showing item details). By encapsulating these user interface areas in their own view we can have a more composable UI that allows us to bring the pieces together or break them apart as needed to meet requirements. Nested views enable us to not only bring these views together at the same time but also to nest a view inside another view as is done in the list/detail example.
History
When AngularJS was first released ngRoute
had a similar feature set as other routers at the time, such as the router included in the Backbone.js library as well as the stand-alone routing libraries History.js and Sammy.js. In summary, they mapped a route or URL to JavaScript code that needed to be run when the URL changes and added entries to the browsers history appropriately so that the back button didn’t break.
Eventually competing JavaScript MV* frameworks like Ember.js and Durandal.js innovated and came out with more robust routers that supported multiple views and nested views and implemented the state machine design pattern internally.
AngularJS responded to this by removing
ngRoute
out of the core angular.js download in version 1.1.6 (most people just say version 1.2). It is still available for download from the
AngularJS site but is no longer in the core.
The AngularJS community responded and the most popular library that emerged was the AngularUI project’s UI-Router.
The AngularJS team which for several months (as a consultant) included Rob Eisenberg, the Durandal.js and Aurelia (nextgen Durandal) creator, has been working on a rewrite of the Router for the future version of AngularJS 2.0 and have stated it will eventually be ported back into a point release of AngularJS version 1.3 which was just recently released.
Install
To use the UI-Router with version 1.2.x or 1.3.x of AngularJS you can do one of the following to get the javascript source code:
download
bower install
$ bower install angular-ui-router
|
npm install
$ npm install angular-ui-router
|
Include script tag
Include angular-ui-router.js or angular-ui-router.min.js in your index.html, after the Angular script tag (see below)
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>
<script src="js/angular-ui-router.min.js"></script>
|
Include Dependency
Include the ‘ui.router’ dependency in your main AngularJS module.
var myApp = angular.module('myApp', ['ui.router']);
|
Notice the module name is ui.router
not ui-router
(using the hyphen is a common mistake).
Router as State Machine
UI-Router introduces a state machine design pattern abstraction on top of a traditional router. Routes are referred to as states
and the URL becomes simply a property of the state.
var app = angular.module('demo', ['ui.router']);
app.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/');
$stateProvider
.state('home', {
url:'/',
templateUrl: 'templates/home.html',
controller: 'HomeController'
})
.state('about', {
url:'/about',
templateUrl: 'templates/about.html',
controller: 'AboutController'
})
}]);
|
When you create links you can simply refer to the state
name inside a ui-sref
directive instead of using the URL
.
So this:
<a ui-sref="home">Home</a>
|
Renders:
In the example above ui-sref
can be understood as follows: ui
is the directive prefix for all AngularUI project directives and sref
is a play on the traditionalhref
of an HTML anchor tag and stands for state ref.
In the Controller
Here is an example of how to do a redirect in a Controller to a state
.
$scope.redirectToAbout = function(){
$state.go('about');
}
|
$routeProvider becomes $stateProvider
The AngularJS service injected to provide routing which is $routeProvider
with ngRoute
becomes $stateProvider
when using the UI-Router.
$urlRouterProvider
The $urlRouterProvider
is there for two main purposes. To establish a default route for URLs that don’t have a specific route.
app.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/');
...
}]);
|
To allow developers to listen for a window location change and redirect to a route that has a state
defined.
app.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
$urlRouterProvider
.when('/legacy-route', {
redirectTo: '/'
});
}]);
|
In summary, $urlRouterProvider
lets you handle cases where the state machine abstraction of the $stateProvider doesn’t make sense.
So now that you’ve got the basics of the UI-Router lets get back to those features that make it better than ngRoute
.
UI-Router in Action
We will look at an example of nested views first then we’ll look an example of multiple views. After we get our heads around each we’ll put the two features together to show how they can be used in a real-world application.
Nested Views with UI-Router
Here is a list/detail example of nested views using UI-Router. The example displays a list of TV shows.
If you click on a row you’ll see a detailed description for the selected show.
Application Shell (index.html)
AngularJS applications are single-page application where views are inserted into a shell page. Here is our shell page index.html.
<!doctype html>
<html id="data-ng-app" data-ng-app="demo">
<head>
<meta charset="utf-8">
<title>ui router demo</title>
<style type="text/css">
.selected{background-color: #efefef; width:120px; }
.detail{width: 300px;margin: 30px;border-top: 1px solid #efefef;}
</style>
<!-- IE8-HTML5: https://code.google.com/p/html5shiv/ -->
<script src="js/libs/html5shiv.js"></script>
</head>
<body id="index">
<!-- Angular UI Router Directive for template insertion -->
<div id="content" ui-view></div>
<script src="js/libs/angular.js"></script>
<script src="js/libs/underscore.js"></script>
<script src="js/libs/angular-ui-router.js"></script>
<script src="js/main.js"></script>
</body>
</html>
|
The <div id="content" ui-view></div>
will have the first level or parent view (in our example shows.html) placed inside of it by the UI-Router.
Home page view (templates/shows.html)
The list view is shows.html.
<ul>
<li ui-sref-active="selected" ng-repeat="show in shows">
<a ui-sref="shows.detail({id: show.id})">{{show.name}}</a>
</li>
</ul>
<div class="detail" ui-view></div>
|
As mentioned before the index.html page has a ui-view
attribute directive into which this view (shows.html) is rendered when the corresponding route is requested.
Notice how there is another ui-view
nested inside this shows.html view. Thisui-view
is where a child view of the parent ‘shows’ view is rendered. In this example shows-detail.html is the child view.
Shows detail view (templates/shows-detail.html)
The detail view is shows-detail.html
<h3>{{selectedShow.name}}</h3>
<p>
{{selectedShow.description}}
</p>
</code>
|
Controllers
And there is a corresponding controller for each view.
ShowsController
The ShowsController
loads an in-memory array of shows from theShowsService
.
app.controller('ShowsController', ['$scope','ShowsService', function($scope, ShowsService) {
$scope.shows = ShowsService.list();
}]);
|
ShowsDetailController
The ShowsDetailController
looks up the show by id using theShowsService
and sets it as the selectedShow
on the $scope
.
app.controller('ShowsDetailController', ['$scope','$stateParams', 'ShowsService', function($scope, $stateParams, ShowsService) {
$scope.selectedShow = ShowsService.find($stateParams.id);
}]);
|
Configuration
We need to configure the UI-Router using the $stateProvider
.
When we define a state as 'parentstatename.childstatename'
the convention of simply adding the period when defining the state name tells UI-Router that the child state is nested under the parent state.
app.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/shows');
$stateProvider
.state('shows', {
url:'/shows',
templateUrl: 'templates/shows.html',
controller: 'ShowsController'
})
.state('shows.detail', {
url: '/detail/:id',
templateUrl: 'templates/shows-detail.html',
controller: 'ShowsDetailController'
});
}]);
|
What is really great about nested views is that the list controller just has implementation details regarding the list and the detail controller is only concerned with showing details.
To show how decoupled these are we only need to change the routing configuration to not nest the details and we have two separate virtual pages (one for list and one for details). More specifically, we’ll change the state name'shows.detail'
to 'detail'
.
$stateProvider
.state('shows', {
url:'/shows',
templateUrl: 'templates/shows.html',
controller: 'ShowsController'
})
.state('detail', {
url: '/detail/:id',
templateUrl: 'templates/shows-detail.html',
controller: 'ShowsDetailController'
});
...
|
And change the link to the state from <a ui-sref="shows.detail({id: show.id})">{{show.name}}</a>
to <a ui-sref="detail({id: show.id})">{{show.name}}</a>
Now our example will display the views separately as if they are two separate pages.
Service
The ShowsService
is our data access layer in this example and just keeps an in-memory array and uses underscore.js to easily work with the collection.
app.factory('ShowsService',function(){
var shows = [{
id: 1,
name: 'Walking Dead',
description: 'The Walking Dead is an American post-apocalyptic horror drama television series developed by Frank Darabont. It is based on the comic book series of the same name by Robert Kirkman, Tony Moore, and Charlie Adlard. It stars Andrew Lincoln as sheriff\'s deputy Rick Grimes, who awakens from a coma to find a post-apocalyptic world dominated by flesh-eating zombies.'
},
{
id: 2,
name: 'Breaking Bad',
description: 'Breaking Bad is an American crime drama television series created and produced by Vince Gilligan. The show originally aired on the AMC network for five seasons, from January 20, 2008 to September 29, 2013. The main character is Walter White (Bryan Cranston), a struggling high school chemistry teacher who is diagnosed with inoperable lung cancer at the beginning of the series.'
},
{
id: 3,
name: '7D',
description: 'The 7D is an American animated television series produced by Disney Television Animation, and broadcast on Disney XD starting in July 7, 2014. It is a re-imagining of the titular characters from the 1937 film Snow White and the Seven Dwarfs by Walt Disney Productions'
}];
return {
list: function(){
return shows;
},
find: function(id){
return _.find(shows, function(show){return show.id == id});
}
}
});
|
Multiple Views with UI-Router
Below is an example of several regions on a page including a header, content, and footer being managed on a page with UI-Router using multiple views.
There is some primary navigation and various content is filled into the regions depending on the virtual page the user navigates to in the application.
Application Shell (index.html)
<!DOCTYPE html>
<html class="no-js">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Index</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body ng-app="demo">
<div ui-view="header"></div>
<div ui-view="content"></div>
<div ui-view="footer"></div>
<script src="/js/bower_components/angular/angular.js"></script>
<script src="/js/bower_components/angular-ui-router/release/angular-ui-router.js"></script>
<script src="/js/main.js"></script>
</body>
</html>
|
Notice the ui-view
attribute directives have names assigned to them: header, content, and footer. These names are referenced when we configure the router to say which view/template and controller to use for each region of the page.
Templates/Views
The templates are straight-forward to keep the example simple. Header.html has some navigation that uses the ui-sref
directive to navigate to various application states/routes.
partials/header.html
<div class="ul">
<li><a ng-href="/">Home</a></li>
<li ui-sref-active="active"><a ui-sref="dashboard">Dashboard</a></li>
<li ui-sref-active="active"><a ui-sref="campaigns">Campaigns</a></li>
</div>
|
partials/content.html
<p>This is the default content.</p>
|
partials/footer.html
<p>This is the footer.</p>
|
partials/dashboard.html
partials/campaigns.html
The dashboard and campaigns view templates are used to replace the default content in content.html when the corresponding routes are requested.
Configuration
As in the previous example we use the $stateProvider to configure the states (routes).
The key takeaway below is to notice that instead of having one templateUrl
and controller
per URL
you instead have a collection of views
each with its own templateUrl
and controller.
So this:
.state('home',{
url: '/',
templateUrl: '/templates/partials/header.html',
controller: 'HomeController'
})
|
Becomes:
.state('home',{
url: '/',
views: {
'header': {
templateUrl: '/templates/partials/header.html',
controller: 'HeaderController'
},
'content': {
templateUrl: '/templates/partials/content.html',
controller: 'ContentController'
},
'footer': {
templateUrl: '/templates/partials/footer.html',
controller: 'FooterController'
}
}
})
|
Here is the full code for the example (note: controllers are not needed for the example I just put them in above for completeness).
var app = angular.module('demo', ['ui.router']);
app.config(function($stateProvider, $urlRouterProvider){
$urlRouterProvider.otherwise('/');
$stateProvider
.state('home',{
url: '/',
views: {
'header': {
templateUrl: '/templates/partials/header.html'
},
'content': {
templateUrl: '/templates/partials/content.html'
},
'footer': {
templateUrl: '/templates/partials/footer.html'
}
}
})
.state('dashboard', {
url: '/dashboard',
views: {
'header': {
templateUrl: '/templates/partials/header.html'
},
'content': {
templateUrl: 'templates/dashboard.html',
controller: 'DashboardController'
}
}
})
.state('campaigns', {
url: '/campaigns',
views: {
'content': {
templateUrl: 'templates/campaigns.html',
controller: 'CampaignController'
},
'footer': {
templateUrl: '/templates/partials/footer.html'
}
}
})
});
|
The other thing to notice is that if I don’t fill in a region with a view then it will not be shown when the user navigates to that route. This is not ideal and makes us repeat ourselves quite a bit so in the next section we will look at how to remove this repetition by using the nested views we saw earlier.
Multiple Views and Nested View with UI-Router
Now that we understand each of these powerful features lets bring them together to give you a better idea of how you might want to set-up a real-world application.
Configuration
We’ll start with the configuration since the view templates are the same as in the multiple views example.
var app = angular.module('demo', ['ui.router']);
app.config(function($stateProvider, $urlRouterProvider){
$urlRouterProvider.otherwise('/');
$stateProvider
.state('app',{
url: '/',
views: {
'header': {
templateUrl: '/templates/partials/header.html'
},
'content': {
templateUrl: '/templates/partials/content.html'
},
'footer': {
templateUrl: '/templates/partials/footer.html'
}
}
})
.state('app.dashboard', {
url: 'dashboard',
views: {
'content@': {
templateUrl: 'templates/dashboard.html',
controller: 'DashboardController'
}
}
})
.state('app.campaigns', {
url: 'campaigns',
views: {
'content@': {
templateUrl: 'templates/campaigns.html',
controller: 'CampaignController'
}
}
})
.state('app.subscribers', {
url: 'subscribers',
views: {
'content@': {
templateUrl: 'templates/subscribers.html',
controller: 'SubscriberController'
}
}
})
.state('app.subscribers.detail', {
url: '/:id',
/*
templateUrl: 'templates/partials/subscriber-detail.html',
controller: 'SubscriberDetailController'
*/
views: {
'detail@app.subscribers': {
templateUrl: 'templates/partials/subscriber-detail.html',
controller: 'SubscriberDetailController'
}
}
});
});
|
We create a default state (route) at /
named app
. In this app
state we can define the default content for our header and footer regions. Then if we make every other page in the application a nested view under the app
by using the dot syntax for example app.campaigns
. Notice we only need to replace thecontent
region (ui-view='content'
) unless we need to change the header or footer because these views are nested under the default app state.
State names
The most difficult concept to grasp in the code above is the state
name syntax with the @
in the middle. The syntax for the state name can be explained as follows:
Two questions need to be answered when writing a state-name:
- What is the name of the view (region) I want to replace with my template when this route is requested: view-name? More specifically, this is the value of the ui-view attribute directives. Here are some examples of ui-view directives and their corresponding view-name:
ui-view='content'
= content
ui-view='header'
= header
ui-view='footer'
= footer
- Where can I find the ui-view with that view-name?
- this location is not expressed as a templateUrl but instead as the state that contains that template
- when the template ui-view with the view-name is in the application shell template (index.html) because index.html is not defined in any state you should leave name the state as empty string (”) or nothing
Putting this together, the syntax is question1@question2
or more specificallyview-name@state-name
.
So if you need to find the content view-name in index.html it would be:
'content@'
- See the second bullet under #2 about leaving the state blank when the view-name is in the shell page (index.html).
If you need to find the content view-name in subscribers.html it would be:
'detail@app.subscribers'
Application Shell (index.html)
The shell page doesn’t change from the previous example and simply defines named views for each region of the page: header, footer, and content.
Views/Templates
Header (partials/header.html)
The header has updated ui-sref
references to the nested states for example.campaigns
with the period not campaigns
. Note the parent state is inferred when we say .campaigns
.
<div class="ul">
<li><a ng-href="/">Home</a></li>
<li ui-sref-active="active"><a ui-sref=".dashboard">Dashboard</a></li>
<li ui-sref-active="active"><a ui-sref=".campaigns">Campaigns</a></li>
<li ui-sref-active="active"><a ui-sref=".subscribers">Subscribers</a></li>
</div>
|
Below are the other new subscriber templates in the example.
partials/subscribers.html
<h2>Subscribers</h2>
<ul>
<li ng-repeat="subscriber in subscribers">
<a ui-sref=".detail({id: subscriber.id})" > {{subscriber.name}}</a>
{{subscriber.email}}
</li>
</ul>
<div ui-view="detail"></div>
|
partials/subscriber-detail.html
Conclusion
The syntax for the state names is difficult to grasp but the benefits of having a robust router which allows you to compose your user interface from well encapsulated view/controller pairs is worth it in my mind. So please feel free to take the last example as a starting point for the shell of your application and run with it to build an amazing and much more maintainable application. Let me know if you are or are not already using the UI-Router and what other questions you have about it.
Code