Tutorial: 7 - Routing & Multiple Views

In this step, you will learn how to create a layout template and how to build an app that has multiple views by adding routing.

Note that you are redirected to app/index.html#/phones and the same phone list appears in the browser. When you click on a phone link the stub of a phone detail page is displayed.

The most important changes are listed below. You can see the full diff on GitHub:

Multiple Views, Routing and Layout Template

Our app is slowly growing and becoming more complex. Before step 7, the app provided our users with a single view (the list of all phones), and all of the template code was located in the index.html file. The next step in building the app is to add a view that will show detailed information about each of the devices in our list.

To add the detailed view, we could expand the index.html file to contain template code for both views, but that would get messy very quickly. Instead, we are going to turn the index.html template into what we call a "layout template". This is a template that is common for all views in our application. Other "partial templates" are then included into this layout template depending on the current "route" — the view that is currently displayed to the user.

Application routes in angular are declared via the $route service. This service makes it easy to wire together controllers, view templates, and the current URL location in the browser. Using this feature we can implement deep linking, which lets us utilize the browser's history (back and forward navigation) and bookmarks.

Controllers

app/js/controller.js:

function PhoneCatCtrl($route) {
  var self = this;

  $route.when('/phones',
      {template: 'partials/phone-list.html',   controller: PhoneListCtrl});
  $route.when('/phones/:phoneId',
      {template: 'partials/phone-detail.html', controller: PhoneDetailCtrl});
  $route.otherwise({redirectTo: '/phones'});

  $route.onChange(function() {
    self.params = $route.current.params;
  });

  $route.parent(this);
}

//PhoneCatCtrl.$inject = ['$route'];
...

We created a new controller called PhoneCatCtrl. We declared its dependency on the $route service and used this service to declare that our application consists of two different views:

We reused the PhoneListCtrl controller that we constructed in previous steps and we added a new, empty PhoneDetailCtrl controller to the app/js/controllers.js file for the phone details view.

The statement $route.otherwise({redirectTo: '/phones'}) triggers a redirection to /phones when the browser address doesn't match either of our routes.

Thanks to the $route.parent(this); statement and ng:controller="PhoneCatCtrl" declaration in the index.html template, the PhoneCatCtrl controller has a special role in our app. It is the "root" controller and the parent controller for the other two sub-controllers (PhoneListCtrl and PhoneDetailCtrl). The sub-controllers inherit the model properties and behavior from the root controller.

Note the use of the :phoneId parameter in the second route declaration. The $route service uses the route declaration — '/phones/:phoneId' — as a template that is matched against the current URL. All variables defined with the : notation are extracted into the $route.current.params map.

The params alias created in the $route.onChange callback allows us to use the phoneId property of this map in the phone-details.html template.

Template

The $route service is usually used in conjunction with the ng:view widget. The role of the ng:view widget is to include the view template for the current route into the layout template, which makes it a perfect fit for our index.html template.

app/index.html:

...
<body ng:controller="PhoneCatCtrl">

  <ng:view></ng:view>

  <script src="lib/angular/angular.js" ng:autobind></script>
  <script src="js/controllers.js"></script>
</body>
</html>

Note that we removed most of the code in the index.html template and replaced it with a single line containing the ng:view tag. The code that we removed was placed into the phone-list.html template:

app/partials/phone-list.html:

<ul class="predicates">
  <li>
    Search: <input type="text" ng:model="query"/>
  </li>
  <li>
    Sort by:
    <select ng:model="orderProp">
      <option value="name">Alphabetical</option>
      <option value="age">Newest</option>
    </select>
  </li>
</ul>

<ul class="phones">
  <li ng:repeat="phone in phones.$filter(query).$orderBy(orderProp)">
    <a href="#/phones/{{phone.id}}">{{phone.name}}</a>
    <a href="#/phones/{{phone.id}}" class="thumb"><img ng:src="{{phone.imageUrl}}"></a>
    <p>{{phone.snippet}}</p>
  </li>
</ul>

We also added a placeholder template for the phone details view:

app/partials/phone-detail.html:

TBD: detail view for {{params.phoneId}}

Note how we are using params model defined in the PhoneCatCtrl controller.

Test

To automatically verify that everything is wired properly, we wrote end-to-end tests that navigate to various URLs and verify that the correct view was rendered.

...
  it('should redirect index.html to index.html#/phones', function() {
   browser().navigateTo('../../app/index.html');
   expect(browser().location().hash()).toBe('/phones');
  });
...

 describe('Phone detail view', function() {

   beforeEach(function() {
      browser().navigateTo('../../app/index.html#/phones/nexus-s');
   });


   it('should display placeholder page with phoneId', function() {
      expect(binding('params.phoneId')).toBe('nexus-s');
   });
 });

You can now refresh the browser tab with the end-to-end test runner to see the tests run, or you can see them running on angular's server.

Experiments

Summary

With the routing set up and the phone list view implemented, we're ready to go to step 8 to implement the phone details view.