Angular ui-router

Home / Angular ui-router

I’ve been asked a few times why would you use the ui-Router instead of the built-in ngRoute.

I, not so succinctly, explained that it was because it allows for states and nested/hierarchical views.


Here is a snippet from the Angular team’s ui-router design document:

Objective

There are many known limitations with ngRoute in AngularJS 1.x. The purpose of this doc is to summarize the issues with the current router, and plan the features, requirements, and overarching design for the router in Angular 2.0.

Note: Although the target for this is Angular 2.0, it might be possible to backport this new router to Angular 1.x.

Background

The initial Angular router was designed to handle just a few simple cases. As Angular grew, we’ve slowly added more features. However, the underlying design is ill-suited to be extended much further.

It’s very easy for developers to write apps that ignore URLs and break the back button with Angular. Angular should make it easier for developers to create apps with deeplinking.

So, here is an attempt to explain its benefit, how I’ve used it, and its general usage a bit more.

Think about a typical web application with multiple views (regions). A good example is an application my team and I wrote a few weeks ago.  It’s for our call center and lets them queue up calls to customers that they would like to make.  Without getting into too many specifics of the application, its Admin View is a good example of a nested view scenario.  The Admin view has a top-header, a left nav bar, and a content area. Handling just a top-level nav bar is easy enough, regardless of your router, but once you start adding additional navigation panes, things get interesting and can be difficult to manage.

Untitled

 

With ngRoute, focusing on the side-bar and content area, we can achieve this by sharing a single view with multiple controllers – or, via directives. This isn’t ideal, though, for obvious reasons. The view/page is less composable in this scenario. We would have to start sharing data across controllers and, effectively, manage our own state, and deep-linking. Otherwise, controllerA (nav) would have no way to tell controllerB (content) what to display. Further, managing templates within our content area would become a burdensome pain. A route change will destroy the controllers and redraw the entire view – further complicating things like page refresh.

ui-router makes this very easy, though. As a side-note, I do still keep the top-level navigation independent of the router view handling – simply because I consider it “state less.”

First, we can think of all different ways that a page can be rendered as “states.” ui-router uses this paradigm to associate nested views. So, I typically define an “abstract state” with ui-router’s $stateProvider that defines our application level view. Basically, I define this as a simple div element with the ui-view attribute. This lets ui-router know what view is rendered for this state.

ui-router uses the ui-view directive/attribute to define precisely what/where a state is rendered. I define a ui-view section in my main Index.cshtml where the “main view” is injected by Angular, but then I define a ui-view within main where all of its child states will subsequently be rendered.

This state looks like this:

$stateProvider
    .state('main', {
        abstract: true,
        url: '/',
        views: {
            'main': { template: '<div id="tabs-views" data-ui-view></div>' }
        }
    })

Breaking this down, all we have done is defined a ui-view that will be used for all child states.

Next, lets look at some of our top-level routes which can be accessed by our top-nav bar:

.state('main.callRecord', {
    url: 'callRecord?id&showAssociated',
    templateUrl: '/Template/?name=_CallRecord',
    controller: 'callRecordCtrl as vm',
    reloadOnSearch: false
})
.state('main.search', {
    url: 'search',
    templateUrl: '/Template/?name=_Search',
    controller: 'searchCtrl as vm'
})
.state('main.admin', {
    url: "admin",
    templateUrl: '/Template/?name=_Admin',
    controller: 'adminCtrl as vm'
})

ui-router uses the state name’s “dot notation” (tabs.callRecord, for example) to relate states as a nested hierarchy. Deep-linking is facilitated in this way. Looking at this then, we know that callRecord, search, and admin are child states of main. This is effectively defining what gets rendered in our content area.

It’s not very impressive at this point, but where it gets interesting is our Admin view that has a nav bar and its own child states.

Those states are defined as such:

.state('tabs.admin.manageCampaigns', {
    url: "/campaigns",
    templateUrl: '/Template/?name=_ManageCampaigns',
    controller: 'manageCampaignsCtrl as vm'
})
.state('tabs.admin.manageQuestions', {
    url: "/questions",
    templateUrl: '/Template/?name=_ManageQuestions',
    controller: 'manageQuestionsCtrl as vm'
})
.state('tabs.admin.manageQueues', {
    url: "/queues",
    templateUrl: '/Template/?name=_ManageQueues',
    controller: 'manageQueuesCtrl as vm'
})
.state('tabs.admin.manageUsers', {
    url: "/users",
    templateUrl: '/Template/?name=_ManageUsers',
    controller: 'manageUsersCtrl as vm'
})

Looking at the first child, manageCampaigns, the url would look like this: http://localhost/#/admin/campaigns

ui-router knows how to find the requested state based on the url and the deep-link defined, state hierarchy dot-nation.

Our Admin HTML template looks like this:

<div>
    <div class="row">
        <div id="left-col" class="no-scroll">
            <ul class="sidenav">
                <li ng-repeat="tab in vm.tabs" ng-class="{'active-tab': tab.active}" ng-style="vm.tabHeight">
                    <a data-ui-sref="{{ tab.route }}">
                        <span ng-bind="tab.heading"></span>
                    </a>
                </li>
            </ul>
        </div>
        <div ui-view></div>
    </div>
</div>

This view is rendered as our side-navigation “admin state.” And notice, the little div with the ui-view directive. ui-router knows that this is where any child states for Admin are rendered. This effectively makes our “Admin state” composed of a top-level state and, potentially, child states.

How is this useful? Well, at this point, Admin state and its children are, effectively, completely independent. From this point forward, I could define any child state for Admin and it just works because of the way we have defined state routes, relationships, and template URLs. Further, those child states could define even more child states/regions, etc. All of this is deep-linked and we don’t have to concern ourselves any more with how states are rendered.

As an aside, I generally define an Angular service which lets me control whether state/location changes are allowed and lets me easily see what state I am currently in. I also use it for callbacks that any controller can hook into to let the state service know if the application/view/controller is in a currently valid state, what methods to execute before/after state change, etc. This is easily achieved by hooking into the Angular events such as $stateChangeStart and $stateChangeSuccess.

Here are a few links:

ui-router design document:

https://docs.google.com/document/d/1I3UC0RrgCh9CKrLxeE4sxwmNSBl3oSXQGt9g3KZnTJI/edit#heading=h.fxpk50cps4zs

A site that talks about the pros/cons:

http://www.funnyant.com/angularjs-ui-router/

Leave a Reply