Cacomania: Using nested views in AngularJS with UI-Router

Cacomania

Using nested views in AngularJS with UI-Router

Guido Krömer - 18. January 2014 - Tags: ,

The default AngularJS router works well, but has a very limited functionality. You can have only one layout file (the index.html for example) which display a template. This might be enough in many use cases, but if you are planning to build a larger app or just something like a email reader with a Thunderbird like layout you are lost.

One workaround which would prevent many duplicate layout code would be writing many custom directives displaying navigation and sub navigation elements…, but I consider the UI-Router is best solution for managing complex layouts.

The main difference between the default AngularJS Router, which uses the template matching a particular route, and the UI-Router is that the UI-Router works like a Finite-state machine. This means a route can lead to a state which can have sub state…, each state can have a template and/or a controller. By nesting those states the templates assigned to the various states get nested, the optionally controller delivers data to the state template. This sounds more complicated that it actually is.

Classic AngularJS routing

Let's take a look at the route configuration from my "Build a feed reader with the Google feed API and AngularJS" tutorial which uses the AngularJS default router. There are only five routes, three for showing the feeds and two for managing the feeds. The list of the feeds from the left column gets displayed on every screen, even if it does not makes much sense because the user currently manages his feeds. This is a concession to the limitation of the default router. The only solution would have been separating the left column into a custom directive.

angular.module('caco.MiniRSS', ['caco.MiniRSS.FeedList', 'caco.MiniRSS.googleapis.feed'])
  .config(function ($routeProvider) {
     $routeProvider
       .when('/',                      {templateUrl: 'views/list.html',        controller: 'ItemsCtrl'})
       .when('/feed/:id',              {templateUrl: 'views/list.html',        controller: 'ItemsCtrl'})
       .when('/feed/:id/item/:hashKey',{templateUrl: 'views/item.html',        controller: 'ItemCrtl'})
       .when('/manage',                {templateUrl: 'views/manage/list.html', controller: 'FeedManage'})
       .when('/manage/add',            {templateUrl: 'views/manage/add.html',  controller: 'FeedManage'})
  });

UI Router state based routing with nested views

In my latest project I did not want such limitations, due to the increased complexity in comparison to a simple feed reader. CacoCloud is a combined password and bookmark manager, mail and feed reader. Basically it combines several applications in a single UI, this leads to a master layout containing a navigation bar for accessing all single applications. The master layout is the index.html instead of the ng-view directive it uses the UI Routers ui-view directive for defining the part where the view should get displayed.

Here are two screen shots of the application, the first shows the items of a specific feed and the other one a single item. Both views have the same parent state displaying a list of all feeds in the left column. The feed reader it self is embedded into the "master view" containing the top menu bar.

AngularJS feed reader using Ui Router, feed overview.
AngularJS feed reader using Ui Router, read a single item view.

index.html

This is a stripped down version of the CacoCloud index.html file, the interesting part is the ui-view directive, the rest is quite similar to an AngularJS router based "master view".

<!DOCTYPE html>
<html lang="en" ng-app="caco">
<head>
    <!-- head code -->
</head>

<body id="body">

<nav class="navbar navbar-inverse navbar-fixed-top navbar-main"
        role="navigation">
  <div class="navbar-header">
      <!-- some navbar code -->
  </div>
</nav>

<div class="container">
  <div class="row">
      <alerts></alerts>
  </div>
</div>

<div ui-view>
</div>

<!-- footer -->
</body>
</html>

Configuring the states

Let's start defining the certain states and routes of the application, a state can have many child state the states can get nested by using a dot. Each state needs a unique name, the parent state is called "feed" and has three sub states. As you can see the URL of a state is optional, the parent state, in this example, itself does not have a route, but it has a controller loading the list of all feeds from the RESTful API which gets displayed in the states template. The sub state "feed.item" for example has got a route, by calling the route the parent state of this state gets called, too. This means by navigating to a single feed item the "main state" loading the data for the left column gets involved, too. Beginning at the root state, all child templates get included if the current template has an ui-view directive.

angular.module('caco.feed', ['ui.router', 'caco.feed.crtl', 'caco.feed.filter'])
  .config(function($stateProvider) {
    $stateProvider
      .state('feed',            {                                     templateUrl: 'views/feed/main.html',        controller: 'FeedCrtl'      })
      .state('feed.overview',   {url: '/feed/show',                   templateUrl: 'views/feed/main/list.html',   controller: 'ItemsCrtl'     })
      .state('feed.list',       {url: '/feed/show/:id',               templateUrl: 'views/feed/main/list.html',   controller: 'ItemsCrtl'     })
      .state('feed.item',       {url: '/feed/show/:id/item/:id_item', templateUrl: 'views/feed/main/item.html',   controller: 'ItemCrtl'      })
      .state('feed-manage',     {                                     templateUrl: 'views/feed/manage.html'                                   })
      .state('feed-manage.list',{url: '/feed/manage',                 templateUrl: 'views/feed/manage/list.html', controller: 'FeedManageCrtl'})
      /* much more feed-manage states… */
    );
  });

Access params from state routes

Accessing params from a state route is as easy as using the classic router with its $routeParams but they are now called $stateParams. Please note that the parameter names from the route are the same used for performing the REST request which lets you pass the whole state params object which saves some code like this Items.one([id: $stateParams.id],…);.

angular.module('caco.feed.crtl')
  .controller('ItemCrtl', function ($rootScope, $scope, $stateParams, Items…) {
    Items.one($stateParams, function (item) {
      $scope.item = item;
    });
  });

main.html

The main.html template is included by each state showing a feed or a single item, it displays the left column list of all feeds, which is provided by the FeedCrtl controller. Like in the master layout the ui-view directive defines the places where sub states get rendered.

<div class="container">
  <div class="row">
    <div class="col-lg-3 col-md-4">
      <ul class="nav nav-pills nav-stacked">
        <!-- All feeds list element -->
        <li ng-repeat="feed in feeds"
               ng-class="{active: currentFeedId == feed.id}">
          <a href="#/feed/show/{{feed.id}}">
            <!-- Icon and badge code -->
            <span ng-bind-html="feed.title"></span>
          </a>
        </li>
      </ul>
    </div>
    <div class="col-lg-9 col-md-8"
            ui-view></div>
  </div>
</div>

main/list.html

The list.html template gets rendered by two states feed.overview and feed.list and shows a list of feed items, no further view nesting is needed here.

<div class="anchor-fix"
  id="feed-items">
  <table class="table table-striped table-condensed">
    <!-- Table header containing a search input -->
    <tbody>
      <tr class="fade-in fade-out"
             ng-repeat="item in items | filter:searchText | orderBy:'date':true | paginate">
        <td>
          <img ng-src="icons/feed/{{item.id_feed}}.ico"
              width="16px"
              height="16px" />
        </td>
        <td>
          <a href="#/feed/show/{{item.id_feed}}/item/{{item.id}}#item-{{item.id}}"
                ng-bind-html="item.title"
                ng-if="item.read"></a>
          <!-- Some more template code... -->
        </td>
        <!-- Some more template code... -->
      </tr>
    </tbody>
    <!-- Table footer with paginator -->
  </table>
</div>

Some Last Words

I hope you enjoyed my petty tutorial, the whole code is available under the MIT license at GitHub. Feel free to leave a comment if you liked or disliked my posting.