UI-Router


Apps with Depth

Nested Views and Routing for Angularjs

Overview for CincyNg Meetup

Sponsored by

Union of Rad


http://unionofrad.com/

Who I am by day?


Tim Kindberg, I work at Trivantis

Trivantis is an elearning software company. We make elearning publishing software and learning management systems.


Graphic Designer  turned  

eLearning Course Designer  turned  

Flash Animator  turned  

Flash/Flex Developer  turned

Web UI/UX Designer  turned  

Front-end Web Developer  turned ?


Who am I by night?

Lazy Loving husband

Frustrated Attentive father of two devils children

Documentation grunt founding member of UI-Router team

Consumer of tv and ice cream tutorials, changelogs, and docs for new tech.

Need Input!


Agenda

  1. UI-Router's backstory
  2. Current option, ngRoute module
  3. Compare ui.router to ngRoute
  4. Nested states
  5. State activation
  6. How to activate a state
  7. State urls
  8. Multiple named views
  9. Abstract states
  10. Popularity
  11. Challenges
  12. Resources

How I Found Angular


Researching JavaScript frameworks for project.


Angular:  Easy, Fun, Familiar

Ember: Complex, Unfamiliar *
* to me

Except for one thing


I really thought Ember’s routing was much better.

Angular: Single view mapped to routes

Ember: Unlimited nested views mapped to nested routes
vs.


Meh.
Zurg!!

Were others feeling the same?


YUP!


  • Nothing existed in angular at this point
  • Found a budding github discussion—AngularUI-backed
  • Gave my .99 cents
  • Some folks (including myself) shaped the initial API
  • Released v0.0.1 on 05.24.2013

Credit


Karsten Sperling - Original Code Base / Father of UI-Router

Nate Abele - Torch Carrier / Current Lead Developer

Myself - Backseat Driver / Docs

Other Notable Github Contributers

ngRoute module




$route / $routeProvider





Out of the box, comes with core angular
(In 1.2 its now a separate module)

$routeProvider (current, single level)




ui-router module


$state / $stateProvider 

& $urlRouterProvider



UI-Router's solution

States vs. Routes


UI-Router uses the idea of states instead of routes.

Let's look at the similarities and differences.

States vs. Routes


A place within your app
Nested hierarchy
Names are names
Navigate by name or url
Multiple views (ui-view)
Populate any view
State populates 'parts'




A url within your app
Flat hierarchy
Names are urls
Navigate by url only
Single view (ng-view)
Populate the one view
Directives  populate 'parts'

A lot is the same


$stateProvider.state('contact.detail', {
    url: '/contacts/:id',
    template: '<h1>Hello</h1>',
    templateUrl: 'contacts.html',
    controller: function($scope){ ... },
    resolve: { ... }
}) 
$routeProvider.when('/contacts/:id', {
    template: '<h1>Hello</h1>',
    templateUrl: 'contacts.html',
    controller: function($scope){ ... },
    resolve: { ... }
}) 

The only noticeable difference is the state has a name and
specifies its url within the config.

Similarities


Associate a url (optional in ui-router)


Can assign controller

Can resolve dependencies before controller instantiates

Redirect with when()

Handle invalid urls with otherwise()

Difference of When()

ngRoute

ui.router

Main method of adding routes
1.2 changes to route()

Just for creating url redirects

To use when or otherwise, $urlRouterProvider

$urlRouterProvider
    .when('/user/:id', '/contacts/:id')                              .otherwise('/');

Of course, add it



Add script to <head>:
<script src="angular-ui-router.js"></script>


Then add as module dependency:
angular.module("myApp", ["ui.router"])

Nesting is where "it" is at!


The simple act of nesting makes up for 90% of the Zurg! effect.

Apps consist
of
components
within 
components 
within 
components.

Let State Views Describe It



States naturally describe these nested 'pieces'. 


A form within 
a field within 
a panel within 
a listview within 
a page within 
a site.


Is just a state within a state within a state...

A Typical Use Case





Sample app

How is this achieved?

Each template is inserted into 
the ui-view within the parent state's template
index.html
<body>    <ui-view/></body>

app.js
.state('top', {
    template:"<ui-view/>"})
.state('top.middle', {
    template:"<ui-view/>"})
.state('top.middle.bottom', {
    template:"<ui-view/>"}) 
 

The Dot®

How baby states are made.

$stateProvider    .state("home", { ... });    .state("contacts", { ... });
.state("contacts.detail", { ... });
.state("contacts.detail.edit", { ... });


The dot in the state names auto-denotes parental hierarchy.

It knows that 'contacts.detail' is a child of 'contacts'.


Sample app nested state code

Dot Optional


Two ways to nest states:

  1. The Dot®
  2. 'parent' property in config

1. $stateProvider     .state('contacts', {})
     .state('contacts.list', {});

2. $stateProvider .state('contacts', {}) .state('list', { parent: 'contacts' });

Object-based States

Another alternative

var contacts = { 
    name: 'contacts',  //mandatory
    templateUrl: 'contacts.html'
}
var contactsList = { 
    name: 'list',      //mandatory
    parent: contacts,  //mandatory
    templateUrl: 'contacts.list.html'
}

$stateProvider
  .state(contacts)
  .state(contactsList) 

Inheritance


Scope Properties and Methods*†
* This is not a ui-router feature, its just angular being angular. 
† Furthermore, this only happens if your template nesting mirrors your state nesting 


Child states inherit from their parents:
  • Resolved Dependencies
  • Custom Data

Inherited Resolved Dependencies


   .state('parent', {
      resolve:{
         resA:  function(){
            return {'value': 'A'};
         }
      },
      controller: function($scope, resA){
          $scope.resA = resA.value;
      }
   })
   .state('parent.child', {
      resolve:{
         resB: function(resA){
            return {'value': resA.value + 'B'};
         }
      },
      controller: function($scope, resA, resB){
          $scope.resA2 = resA.value;
          $scope.resB = resB.value;
      } 
Learn more about how resolves work.

Inherited Custom Data

$stateProvider   .state('parent', {
      data:{
         customData1:  "Hello",
         customData2:  "World!"
      }
   })
   .state('parent.child', {
      data:{
         // customData1 inherited from 'parent' 
         // but we'll overwrite customData2
         customData2:  "UI-Router!"
      }
   });

$rootScope.$on('$stateChangeStart', function(event, toState){ 
    var greeting = toState.data.customData1 + " " + toState.data.customData2;
    console.log(greeting);
}) 

When 'parent' is activated it prints 'Hello World!' 

When 'parent.child' is activated it prints 'Hello UI-Router!' 

Activating a State

What happens when a state is activated?

  1. Merge options with defaults.
  2. Check if state is defined, if not, broadcast $stateNotFound.
  3. Merge params with ancestor params (unless options.inherit = false).
  4. Figure out which states are changing and which aren't. 
    No changes, no transition, unless options.reload = true.
  5. Broadcast $stateChangeStart.
    If evt.defaultPrevented, returns rejected promise "transition prevented".
  6. Begin resolving locals for newly activated states.


Activating a State (cont)

Once locals are resolved the real transition can begin:
  1. Check if superseded by newer transition
  2. Run OnExit for all unkept states
  3. Run OnEnter for all newly active states
  4. Update $state service
  5. Update $stateParams service
  6. Update $location
  7. Broadcast $stateChangeSuccess

Activating a Child State

Activates any unactivated states from top to bottom.

Scenario #1: Jump right into app via url of "/contacts/1/edit"
  1. Activate implicit root state
  2. Activate 'contacts' state
  3. Activate 'contacts.detail' state
  4. Activate 'contacts.detail.edit' state

Scenario #2: From there we click contact 2
  1. Exit 'contacts.detail.edit' state
  2. Exit 'contacts.detail' state (contact 1)
  3. Activate 'contact.detail' state (contact 2)

The other states were left alone. 
No redundant work is done.

Callbacks

OnEnter and onExit
$stateProvider.state("contacts", {
  template: '<h1>{{title}}</h1>',
  resolve: { title: 'My Contacts' },
  controller: function($scope, title){
    $scope.title = 'My Contacts';
  },
  onEnter: function(title){
    if(title){ ... do something ... }
  },
  onExit: function(title){
    if(title){ ... do something ... }
  }
})

Events


State Change Events

$stateChangeStart (event, toState, toParams, fromState, fromParams)
can e.preventDefault()
$stateNotFound (event, unfoundState, fromState, fromParams)
good for lazy state definitions
$stateChangeSuccess (event, toState, toParams, fromState, fromParams)
$stateChangeError (event, toState, toParams, fromState, fromParams, error)

View Load Events

$viewContentLoading (event, viewConfig)
$viewContentLoaded (event)

How to Activate a State

Three Ways


1. $state.go()

2. Click a link with ui-sref directive
(so sexy)

3. Navigate to the state's url
(if provided)

$state.go()


Programmatic state navigation

myApp.controller('contactCtrl', ['$scope', '$state', 
  function($scope, $state){
     $scope.goToDetails = function(){
        $state.go('contact.details', {id: selectedId});
     }
  } 
Params...
1. state name
2. state params (optional)
3. options (optional)
{ location: true, inherit: true, relative: $state.$current, notify: true, reload: false }

Relative Navigation

'Cause we are in a multi-tiered view tree, ya turkey!

Move relative to current state
by using special characters.

^   is up
.   is down


Examples

Start          Middle          End   

Go to parent - $state.go('^')


Go to child - $state.go('.3')


Go to sibling - $state.go('^.1')


Absolute Path - $state.go('3.3')


Hey Cous! - $state.go('^.^.2.1')

ui-sref

State references = Smart anchors
(Used in place of href)

<a ui-sref="home">Home</a>

Generates href attr during compile
(if url exists)
<a ui-sref="home" href="#/home">Home</a>

ui-sref


Can also pass in params
<li ng-repeat="contact in contacts">
   <a ui-sref="contacts.detail({ id: contact.id })"</a>
</li>

And use relative paths
 <a ui-sref='^'>Home</a>
The path is relative to the state that the link lives in.
In other words the state that loaded the template containing the link.

URLs


In theory...

one could build an app using ui-router that associated not a single url with any state. They could navigate via go() and ui-sref.

...In theory.

Nested Urls

Go handsomely with nested states

By default, urls are appended to parent state urls.
 $stateProvider
  .state('contacts', {
     url: '/contacts',
  })
  .state('contacts.list', {
     url: '/list',
  });
contacts.list's url becomes
/contacts/list

Absolute Urls

Don't want appendage? 

Prepend with ^
 $stateProvider
  .state('contacts', {
     url: '/contacts',
  })
  .state('contacts.list', {
     url: '^/list',
  });
contacts.list's url becomes
/list

URL Parameters

Basic
url: '/contacts/:contactId'url: '/contacts/{contactId}'
Regex
url: '/contacts/{contactId:[0-9a-fA-F]{1,8}}' //Hexadecimals
Query
url: '/contacts?contactId&contactRegion' //Separate with '&'

$stateParams

//State URL:url: '/users/:id/details/{type}/{repeat:[0-9]+}?from&to'

//Navigate to:'/users/123/details//0'

//$stateParams will be{ id:'123', type:'', repeat:'0' }

//Navigated to:'/users/123/details/default/0?from=there&to=here'
//$stateParams will be{ id:'123', type:'default', repeat:'0',   from:'there', to:'here' }

Important $stateParams 'Gotcha'


In state controllers, $stateParams is scoped to contain only params declared in that state. So params from parent states will not be present in the $stateParams object.

    $stateProvider.state('contacts.detail', {   url: '/contacts/:contactId',      resolve: { depA: function(){       return $state.current.params.contactId + "!" };    },
      controller: function($stateParams){
          $stateParams.contactId  // Exists!
      }}).state('contacts.detail.subitem', {
       url: '/item/:itemId', 
       controller: function($stateParams){
          $stateParams.contactId // Doesn't exist
          $stateParams.itemId // Exists!      }
    })

    Multiple Named Views


    Pros
    • Provides flexibity
    • You get to have multiple views in any template
    • Great for singleton top-level components (side panel, modal)
    • Many devs put them to great use!

    Cons
    • Often unnecessary, try to nest first!
    • Leading cause of confusion
    • Can lead to antipatterns
      (but I'll give you some good tips on how to avoid them)

    Quick Concept

    By default there is an implicit root state that is always active.
    {
        name: '',
        url: '^',
        'abstract': true} 
    No Name
    Url is absolute root
    Can't activate it directly
    Most Important: Its "template" is your index.html

    Good Use Case Example






    Naming Views


    Mandatory if two or more views in the same template, 
    or optional if you simply want to target a view by a name.

    <div ui-view='main'>
    <div ui-view='sidenav'>

    Configure Multiple Views


    Use the views config object

    Each key is a view name, each value is a view config object
    $stateProvider  .state('deep.down.state.mainbits', {    url: "url/still/goes/up/here"    views: {      'main@': { ... },      'sidenav@': { ... }    }  })

    View Name

    Can use relative or absolute naming

    Relative (always parent)
    'main' - 'main' view in parent template
    ' ' - unnamed view in parent template

    Absolute (uses @ symbol)
    'list@contacts' - 'list' view in 'contacts' state's template
    'list@' - 'list' view in index.html.
    '@contacts' - unnamed view in 'contacts' state's template

    Pop Quiz!

    '@' - What do you think this targets?
    Answer: unnamed view in index.html

    View Config Object

    Each view can have its own:
    • template, templateUrl, templateProvider
    • controller, controllerProvider
     $stateProvider
      .state('report', {
        url: "url/still/goes/up/here"
        views: {
          'main': { ... },
          'sidenav': {         templateUrl: "sidenav.html",         controller: "SideNavCtrl"      }
        }
      })

    Another Use Case

    LectoraExpress reports. Filters, table data, and graphs.


    Potential Anti-pattern

    Splitting every possible quadrant of your app into a view

    I'm not saying this is bad for sure.

    Consider

    Anti-patterns aren't clear cut.

    What quadrants are dependent on other quadrants?
    Nested Views for those.

    Are multiple quadrants dependent on same parent? Yes.
    And, do the quadrants really need to be separated? Hmm.
    Can they share scope? Yes.
    Then share single view and nest it.

    Or maybe need encapsulation (unshared scope)? Yes.
    Multiple Named Views.

    Who's dependent on who?

    Do any quandrants depend on a single parent?
    Yes, Table and Graph depend on Filters. Nest them.

    Really need separated?

    Do Table and Graph really need to be isolated from each other?
    Maybe not. Possibly merge them.

    Abstract States

    • A state that cannot be explicitly activated.
      (attempts to activate it directly will throw)

    • It can have child states.

    • It is activated implicity when one of its descendants are activated

    Abstract State Usages


    • To prepend a url to all child state urls.
    • To set a template its child states will populate.
      • Optionally assign a controller to the template. 
      • Provide $scope properties/methods for children to inherit
    • To resolves dependencies for child states to use.
    • To set custom data for child states or events to use.
    • To run an onEnter or onExit function.
    • Any combination of the above.

    Tip: Abstract states will need their own <ui-view/>
    if child states intend to populate with templates.

    Learn more about abstract states

    UI-Router Popularity


    1,375 Star gazers on Github

    142 Watchers

    260 Forks

    39 Sporks*





    *wife added this one, thought it was sooooooo funny.

    Making Waves in Core


    Angular 1.2, $route moves to its own module

    Allows choice of routing module.

    They specifically mention ui-router.





    Eventual inclusion into angular core?? Maybe?
    Doesn't really matter, because you have access to these features either way!

    More Love for UI-Router


    The ng-boilerplate seed project uses ui-router out of the box. 

    Next Yeoman angular generator will be based off of ng-boilerplate.

    Challenges


    Only v0.2.0
    (v0.3.0 coming very soon)

    Issues: 461 Closed, 91 Open

    Big visions / small team
    Need more contributors (interested?)

    Is it ready to use?


    I would use it.
    Lots of devs are using it, loving it!

    Might run into edge case bugs. If so, the code is well commented. 
    Fix the bug and submit a pull request!

    Roadmap


     Features on their way:
      • $view service - decoupling view functions into own service
      • Typed parameters; custom param encoding/decoding
      • Two-way binding between url params and $stateParams.

    Other big ideas, future goals:
      • Orthogonal views with own state trees. 
      • Components, aka reusable states.
      • More hooks for lazy-loading states

      The Factos


      Your app is not flat, neither should your router be.

      Your app is not rigid, neither should your router be.

      States are better than urls.
      oh snap!

      I feel strongly that this will be the de facto router.
      And if not this, something very similar.

      Thanks!!



      Resources

      UI-Router

      By Tim Kindberg

      UI-Router

      UI-Router is an angularjs replacement module for ngRoute. It builds upon ngRoute's features by adding nested views and states capabiliites as well as the ability to have multiple named views at any level in the state tree.

      • 32,799

      More from Tim Kindberg