209 lines
12 KiB
HTML
Executable file
209 lines
12 KiB
HTML
Executable file
<a href="http://github.com/angular/angular.js/edit/master/docs/content/tutorial/step_07.ngdoc" class="improve-docs btn btn-primary"><i class="icon-edit"> </i> Improve this doc</a><h1><code ng:non-bindable=""></code>
|
|
<div><span class="hint"></span>
|
|
</div>
|
|
</h1>
|
|
<div><div class="tutorial-page tutorial-7-routing-multiple-views-page"><ul doc-tutorial-nav="7"></ul>
|
|
|
|
|
|
<p>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.</p>
|
|
<div doc-tutorial-reset="7">
|
|
</div>
|
|
|
|
|
|
<p>Note that when you now navigate to <code>app/index.html</code>, you are redirected to <code>app/index.html#/phones</code>
|
|
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.</p>
|
|
<p>The most important changes are listed below. You can see the full diff on <a href="https://github.com/angular/angular-phonecat/compare/step-6...step-7">GitHub</a>.</p>
|
|
<h3>Multiple Views, Routing and Layout Template</h2>
|
|
<p>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
|
|
<code>index.html</code> 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.</p>
|
|
<p>To add the detailed view, we could expand the <code>index.html</code> file to contain template code for both
|
|
views, but that would get messy very quickly. Instead, we are going to turn the <code>index.html</code>
|
|
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.</p>
|
|
<p>Application routes in Angular are declared via the
|
|
<a href="api/ngRoute.$routeProvider">$routeProvider</a>, which is the provider of the
|
|
<a href="api/ngRoute.$route">$route service</a>. 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 <a href="http://en.wikipedia.org/wiki/Deep_linking">deep linking</a>, which lets us utilize the browser's
|
|
history (back and forward navigation) and bookmarks.</p>
|
|
<h4>A Note About DI, Injector and Providers</h4>
|
|
<p>As you <a href="tutorial/step_05">noticed</a>, <a href="guide/di">dependency injection</a> (DI) is the core feature of
|
|
AngularJS, so it's important for you to understand a thing or two about how it works.</p>
|
|
<p>When the application bootstraps, Angular creates an injector that will be used for all DI stuff in
|
|
this app. The injector itself doesn't know anything about what <code>$http</code> or <code>$route</code> services do, in
|
|
fact it doesn't even know about the existence of these services unless it is configured with proper
|
|
module definitions. The sole responsibilities of the injector are to load specified module
|
|
definition(s), register all service providers defined in these modules, and when asked, inject
|
|
a specified function with dependencies (services) that it lazily instantiates via their providers.</p>
|
|
<p>Providers are objects that provide (create) instances of services and expose configuration APIs
|
|
that can be used to control the creation and runtime behavior of a service. In case of the <code>$route</code>
|
|
service, the <code>$routeProvider</code> exposes APIs that allow you to define routes for your application.</p>
|
|
<p>Angular modules solve the problem of removing global state from the application and provide a way
|
|
of configuring the injector. As opposed to AMD or require.js modules, Angular modules don't try to
|
|
solve the problem of script load ordering or lazy script fetching. These goals are totally independent and
|
|
both module systems can live side by side and fulfil their goals.</p>
|
|
<h2>The App Module</h2>
|
|
<p><strong><code>app/js/app.js</code>:</strong>
|
|
<pre class="prettyprint linenums">
|
|
angular.module('phonecat', []).
|
|
config(['$routeProvider', function($routeProvider) {
|
|
$routeProvider.
|
|
when('/phones', {templateUrl: 'partials/phone-list.html', controller: PhoneListCtrl}).
|
|
when('/phones/:phoneId', {templateUrl: 'partials/phone-detail.html', controller: PhoneDetailCtrl}).
|
|
otherwise({redirectTo: '/phones'});
|
|
}]);
|
|
</pre>
|
|
<p>In order to configure our application with routes, we need to create a module for our application.
|
|
We call this module <code>phonecat</code> and using the <code>config</code> API we request the <code>$routeProvider</code> to be
|
|
injected into our config function and use <code>$routeProvider.when</code> API to define our routes.</p>
|
|
<p>Note that during the injector configuration phase, the providers can be injected as well, but they
|
|
will not be available for injection once the injector is created and starts creating service
|
|
instances.</p>
|
|
<p>Our application routes were defined as follows:</p>
|
|
<ul>
|
|
<li><p>The phone list view will be shown when the URL hash fragment is <code>/phones</code>. To construct this
|
|
view, Angular will use the <code>phone-list.html</code> template and the <code>PhoneListCtrl</code> controller.</p>
|
|
</li>
|
|
<li><p>The phone details view will be shown when the URL hash fragment matches '/phone/:phoneId', where
|
|
<code>:phoneId</code> is a variable part of the URL. To construct the phone details view, angular will use the
|
|
<code>phone-detail.html</code> template and the <code>PhoneDetailCtrl</code> controller.</p>
|
|
</li>
|
|
</ul>
|
|
<p>We reused the <code>PhoneListCtrl</code> controller that we constructed in previous steps and we added a new,
|
|
empty <code>PhoneDetailCtrl</code> controller to the <code>app/js/controllers.js</code> file for the phone details view.</p>
|
|
<p>The statement <code>$route.otherwise({redirectTo: '/phones'})</code> triggers a redirection to <code>/phones</code> when
|
|
the browser address doesn't match either of our routes.</p>
|
|
<p>Note the use of the <code>:phoneId</code> parameter in the second route declaration. The <code>$route</code> service uses
|
|
the route declaration — <code>'/phones/:phoneId'</code> — as a template that is matched against the current
|
|
URL. All variables defined with the <code>:</code> notation are extracted into the
|
|
<a href="api/ngRoute.$routeParams">$routeParams</a> object.</p>
|
|
<p>In order for our application to bootstrap with our newly created module we'll also need to specify
|
|
the module name as the value of the <a href="api/ng.directive:ngApp"><code>ngApp</code></a>
|
|
directive:</p>
|
|
<p><strong><code>app/index.html</code>:</strong>
|
|
<pre class="prettyprint linenums">
|
|
<!doctype html>
|
|
<html lang="en" ng-app="phonecat">
|
|
...
|
|
</pre>
|
|
<h2>Controllers</h2>
|
|
<p><strong><code>app/js/controllers.js</code>:</strong>
|
|
<pre class="prettyprint linenums">
|
|
...
|
|
function PhoneDetailCtrl($scope, $routeParams) {
|
|
$scope.phoneId = $routeParams.phoneId;
|
|
}
|
|
|
|
//PhoneDetailCtrl.$inject = ['$scope', '$routeParams'];
|
|
</pre>
|
|
<h2>Template</h2>
|
|
<p>The <code>$route</code> service is usually used in conjunction with the <a href="api/ngRoute.directive:ngView">ngView</a> directive. The role of the <code>ngView</code> directive is to include the view template for the current
|
|
route into the layout template, which makes it a perfect fit for our <code>index.html</code> template.</p>
|
|
<p><strong><code>app/index.html</code>:</strong>
|
|
<pre class="prettyprint linenums">
|
|
<html lang="en" ng-app="phonecat">
|
|
<head>
|
|
...
|
|
<script src="lib/angular/angular.js"></script>
|
|
<script src="js/app.js"></script>
|
|
<script src="js/controllers.js"></script>
|
|
</head>
|
|
<body>
|
|
|
|
<div ng-view></div>
|
|
|
|
</body>
|
|
</html>
|
|
</pre>
|
|
<p>Note that we removed most of the code in the <code>index.html</code> template and replaced it with a single
|
|
line containing a div with the <code>ng-view</code> attribute. The code that we removed was placed into the
|
|
<code>phone-list.html</code> template:</p>
|
|
<p><strong><code>app/partials/phone-list.html</code>:</strong>
|
|
<pre class="prettyprint linenums">
|
|
<div class="container-fluid">
|
|
<div class="row-fluid">
|
|
<div class="span2">
|
|
<!--Sidebar content-->
|
|
|
|
Search: <input ng-model="query">
|
|
Sort by:
|
|
<select ng-model="orderProp">
|
|
<option value="name">Alphabetical</option>
|
|
<option value="age">Newest</option>
|
|
</select>
|
|
|
|
</div>
|
|
<div class="span10">
|
|
<!--Body content-->
|
|
|
|
<ul class="phones">
|
|
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp" class="thumbnail">
|
|
<a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
|
|
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
|
<p>{{phone.snippet}}</p>
|
|
</li>
|
|
</ul>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</pre>
|
|
<div style="display:none">
|
|
TODO!
|
|
<img class="diagram" src="img/tutorial/tutorial_07_final.png">
|
|
</div>
|
|
|
|
<p>We also added a placeholder template for the phone details view:</p>
|
|
<p><strong><code>app/partials/phone-detail.html</code>:</strong>
|
|
<pre class="prettyprint linenums">
|
|
TBD: detail view for {{phoneId}}
|
|
</pre>
|
|
<p>Note how we are using <code>phoneId</code> model defined in the <code>PhoneDetailCtrl</code> controller.</p>
|
|
<h2>Test</h3>
|
|
<p>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.</p>
|
|
<pre class="prettyprint linenums">
|
|
...
|
|
it('should redirect index.html to index.html#/phones', function() {
|
|
browser().navigateTo('../../app/index.html');
|
|
expect(browser().location().url()).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('phoneId')).toBe('nexus-s');
|
|
});
|
|
});
|
|
</pre>
|
|
<p>You can now rerun <code>./scripts/e2e-test.sh</code> or refresh the browser tab with the end-to-end test
|
|
runner to see the tests run, or you can see them running on <a href="http://angular.github.com/angular-phonecat/step-7/test/e2e/runner.html">Angular's server</a>.</p>
|
|
<h2>Experiments</h1>
|
|
<ul>
|
|
<li>Try to add an <code>{{orderProp}}</code> binding to <code>index.html</code>, and you'll see that nothing happens even
|
|
when you are in the phone list view. This is because the <code>orderProp</code> model is visible only in the
|
|
scope managed by <code>PhoneListCtrl</code>, which is associated with the <code><div ng-view></code> element. If you add
|
|
the same binding into the <code>phone-list.html</code> template, the binding will work as expected.</li>
|
|
</ul>
|
|
<div style="display: none">
|
|
* In <code>PhoneCatCtrl</code>, create a new model called "<code>hero</code>" with <code>this.hero = 'Zoro'</code>. In
|
|
<code>PhoneListCtrl</code> let's shadow it with <code>this.hero = 'Batman'</code>, and in <code>PhoneDetailCtrl</code> we'll use
|
|
<code>this.hero = "Captain Proton"</code>. Then add the <code><p>hero = {{hero}}</p></code> to all three of our templates
|
|
(<code>index.html</code>, <code>phone-list.html</code>, and <code>phone-detail.html</code>). Open the app and you'll see scope
|
|
inheritance and model property shadowing do some wonders.
|
|
</div>
|
|
|
|
<h1>Summary</h2>
|
|
<p>With the routing set up and the phone list view implemented, we're ready to go to <a href="tutorial/step_08">step 8</a> to implement the phone details view.</p>
|
|
<ul doc-tutorial-nav="7"></ul></div></div>
|