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:
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.
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:
The phone list view will be shown when the URL hash fragment is /phones
. To construct this
view, angular will use the phone-list.html
template and the PhoneListCtrl
controller.
The phone details view will be shown when the URL hash fragment matches '/phone/:phoneId', where
:phoneId
is a variable part of the URL. To construct the phone details view, angular will use the
phone-detail.html
template and the PhoneDetailCtrl
controller.
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
callback
allows us to use the $route.onChange
phoneId
property of this map in the phone-details.html
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" name="query"/> </li> <li> Sort by: <select name="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.
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.
Try to add an {{orderProp}}
binding to index.html
, and you'll see that nothing happens even
when you are in the phone list view. This is because the orderProp
model is visible only in the
scope managed by PhoneListCtrl
, which is associated with the <ng:view>
element. If you add the
same binding into the phone-list.html
template, the binding will work as expected.
In PhoneCatCtrl
, create a new model called "hero
" with this.hero = 'Zoro'
. In
PhoneListCtrl
let's shadow it with this.hero = 'Batman'
, and in PhoneDetailCtrl
we'll use
this.hero = "Captain Proton"
. Then add the <p>hero = {{hero}}</p>
to all three of our templates
(index.html
, phone-list.html
, and phone-detail.html
). Open the app and you'll see scope
inheritance and model property shadowing do some wonders.
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.