Using ui-router for handling routes, defining states and sharing data between views

In a previous post, I wrote about using generator-ionic to create the scaffolding for a mobile-optimised Administration UI for the Vardyger publishing platform. In this post, we'll look at how the Admin UI uses the Angular UI router for handling routes, defining states and sharing data between views.

Note: I've updated Vardyger's project structure to include directories for controllers, directives and services:

├── /app
    └── /scripts
        └── /controllers
            ├── editor-controller.js
            ├── main-controller.js
            ├── preview-controller.js
            ├── side-menu-controller.js
        └── /directives
        └── /services
            ├── posts-service.js
        ├── app.js
    └── /styles
        ├── main.scss
    └── /templates
        ├── editor-template.html
        ├── main-template.html
        ├── preview-template.html
        ├── side-menu-template.html
    ├── index.html

...

The Angular UI router

The Ionic framework uses the angular-ui-router module for handling routes and defining states (including nested states and nested views).

Let's start by taking a look at the the Admin UI's app.js where the ui-router's $stateProvider and $urlRouterProvider are used to set up the application’s states and routing logic:

First, we define the root state:

.config(function($stateProvider, $urlRouterProvider) {
  $stateProvider
    .state('app', {
      url: '/app',
      templateUrl: 'templates/side-menu-template.html',
      controller: 'SideMenuController'
      abstract: true,
    })

By providing a name ('app') and populating a configuration object with keys & values for the state's url, templateUrl and controller name.

We have also defined this state as abstract (abstract: true), which means the side-menu can only be displayed when one of its nested states (e.g., app.main, app.preview or app.editor) is active:

Next, we define a nested state:

    .state('app.main', {
      url: '/main',
      views: {
        'menuContent': {
          templateUrl: 'templates/main-template.html',
          controller: 'MainController',
          resolve: {
            posts: function(PostsService) {
              return PostsService.findPosts();
            }
          }
        }
      }
    })

The app.main state defines a named view ('menuContent') that is referenced in the root state's template (templates/side-menu-template.html):

<ion-side-menus enable-menu-with-back-views="false">
  <ion-side-menu-content>

    <ion-nav-bar class="bar-dark">
      <ion-nav-back-button>
      </ion-nav-back-button>

      <!-- ion-nav-buttons must be immediate descendants of the ion-view
        or ion-nav-bar element (basically, don't wrap it in a div). -->
      <ion-nav-buttons side="primary">
        <button class="button button-icon button-clear ion-navicon" 
          menu-toggle="primary"></button>
      </ion-nav-buttons>
    </ion-nav-bar>

    <ion-nav-view name="menuContent"></ion-nav-view>

  </ion-side-menu-content>
  <ion-side-menu side="left">

    ...

  </ion-side-menu>
</ion-side-menus>

The Angular UI router will load the named view's template (templates/main-template.html) into the <ion-nav-view> directive with the name menuContent.

The app.main state also uses resolve and the PostsService to pass data to the MainController (scripts/controllers/main-controller.js):

angular.module('vardyger')
  .controller('MainController',
    function(
      $scope,  // inject the $scope service
      posts    // inject the resolved posts data
    ) {
      $scope.listItems = posts;
    });

The MainController inject's the $scope service and the resolved posts data, the posts data is assigned to the $scope.listItems object.

Note: Values on $scope are called models and are available in views.

The app.main state's template (templates/main-template.html):

<ion-view title="Content">

  ...

  <ion-content class="has-header has-subheader">
    <ion-list>
      <ion-item
        ng-repeat="listItem in listItems"
           ui-sref='app.preview({postId: listItem.post.id})'>
           {{listItem.post.title}}

        ...

      </ion-item>
    </ion-list>
  </ion-content>

</ion-view>

Uses the ng-repeat directive to iterate over the listItems model in order to display a list of posts:

The ui-sref directive specifies the state (app.preview) we want to transition to:

    .state('app.preview', {
      url: /preview/{postId}',
      views: {
        'menuContent': {
          templateUrl: 'templates/preview-template.html',
          controller: 'PreviewController',
          resolve: {
            post: function($stateParams, PostsService) {
              return PostsService.findPostById($stateParams.postId);
            }
          }
        }
      }
    })

Most states in your application will have a url associated with them and URLs often have dynamic parts called parameters. You can send parameters to a state, using either:

<a ui-sref="app.editor({postId: item.post.id})">Go!</a>

Or:

$state.go('app.editor', {postId: item.post.id});

You can learn more about URL parameters in the Angular UI router's wiki and in the $stateProvider documentation.

The app.preview state, like the app.main state defines a named view ('menuContent') that is referenced in the root state's template (templates/side-menu-template.html). And, like the app.main state the Angular UI router will load the app.preview state's view template (templates/preview-template.html) into the <ion-nav-view name="menuContent"> directive.

The app.preview state also uses resolve, the $stateParams service and the PostsService to pass data to the PreviewController (scripts/controllers/preview-controller.js):

angular.module('vardyger')
  .controller('PreviewController',
    function(
      $scope,  // inject the $scope service
      post     // inject the resolved post data
    ) {
      $scope.item = posts;
    });

The PreviewController inject's the $scope service and the resolved post data, the post data is assigned to the item model.

Now, let's take a look at the PostsService's findPostById():

this.findPostById = function(id) {

  var deferred = $q.defer();

  model.forEach(function(item) {
    if (item.post.id === id) {
      deferred.resolve(item);
    }
  });

  return deferred.promise;
};

findPostById() returns a promise that will be resolved and converted into a value before the PreviewController is instantiated and the $stateChangeSuccess event is fired.

The app.preview state's template (templates/preview-template.html):

<ion-view title="Preview">

  <ion-nav-buttons side="right">
   <button class="button button-outline" 
         ui-sref="app.editor({postId: item.post.id})">EDIT</button>
  </ion-nav-buttons>

  <ion-content class="has-header">
    <div class="wrapper">
      <h1 class="padding-top">{{item.post.title}}</h1>
      <div ng-bind-html="item.post.html"></div>
    </div>
  </ion-content>

</ion-view>

Uses the ng-bind-html directive to display the post's content:

And, the ui-sref directive to transition to:

The Admin UI's Markdown editor.

References: