mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-03-30 23:18:53 +08:00
369 lines
13 KiB
Plaintext
369 lines
13 KiB
Plaintext
@ngdoc tutorial
|
|
@name 7 - Routing & Multiple Views
|
|
@step 7
|
|
@description
|
|
|
|
<ul doc-tutorial-nav="7"></ul>
|
|
|
|
|
|
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, using an Angular module called 'ngRoute'.
|
|
|
|
* When you now navigate to `app/index.html`, you are redirected to `app/index.html/#/phones`
|
|
and the phone list appears in the browser.
|
|
* When you click on a phone link the url changes to one specific to that phone and the stub of a
|
|
phone detail page is displayed.
|
|
|
|
<div doc-tutorial-reset="7"></div>
|
|
|
|
## Dependencies
|
|
|
|
The routing functionality added by this step is provided by angular in the `ngRoute` module, which
|
|
is distributed separately from the core Angular framework.
|
|
|
|
We are using [Bower][bower] to install client side dependencies. This step updates the
|
|
`bower.json` configuration file to include the new dependency:
|
|
|
|
```json
|
|
{
|
|
"name": "angular-phonecat",
|
|
"description": "A starter project for AngularJS",
|
|
"version": "0.0.0",
|
|
"homepage": "https://github.com/angular/angular-phonecat",
|
|
"license": "MIT",
|
|
"private": true,
|
|
"dependencies": {
|
|
"angular": "1.2.x",
|
|
"angular-mocks": "~1.2.x",
|
|
"jquery": "1.10.2",
|
|
"bootstrap": "~3.1.1",
|
|
"angular-route": "~1.2.x"
|
|
}
|
|
}
|
|
```
|
|
|
|
The new dependency `"angular-route": "~1.2.x"` tells bower to install a version of the
|
|
angular-route component that is compatible with version 1.2.x. We must tell bower to download
|
|
and install this dependency.
|
|
|
|
If you have bower installed globally then you can run `bower install` but for this project we have
|
|
preconfigured npm to run bower install for us:
|
|
|
|
```
|
|
npm install
|
|
```
|
|
|
|
|
|
## 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 {@link ngRoute.$routeProvider $routeProvider},
|
|
which is the provider of the {@link ngRoute.$route $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](http://en.wikipedia.org/wiki/Deep_linking), which lets us
|
|
utilize the browser's history (back and forward navigation) and bookmarks.
|
|
|
|
|
|
### A Note About DI, Injector and Providers
|
|
|
|
As you {@link tutorial/step_05 noticed}, {@link guide/di dependency injection} (DI) is at the core of
|
|
AngularJS, so it's important for you to understand a thing or two about how it works.
|
|
|
|
When the application bootstraps, Angular creates an injector that will be used to find and inject all
|
|
of the services that are required by your app. The injector itself doesn't know anything about what
|
|
`$http` or `$route` services do, in fact it doesn't even know about the existence of these services
|
|
unless it is configured with proper module definitions.
|
|
|
|
The injector only carries out the following steps :
|
|
|
|
* load the module definition(s) that you specify in your app
|
|
* register all Providers defined in these module definitions
|
|
* when asked to do so, inject a specified function and any necessary dependencies (services) that
|
|
it lazily instantiates via their Providers.
|
|
|
|
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 `$route`
|
|
service, the `$routeProvider` exposes APIs that allow you to define routes for your application.
|
|
|
|
<div class="alert alert-warning">
|
|
**Note:** Providers can only be injected into `config` functions. Thus you could not inject
|
|
`$routeProvider` into `PhoneListCtrl`.
|
|
</div>
|
|
|
|
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 fulfill their goals.
|
|
|
|
To deepen your understanding of DI on Angular, see
|
|
[Understanding Dependency Injection](https://github.com/angular/angular.js/wiki/Understanding-Dependency-Injection).
|
|
|
|
## Template
|
|
|
|
The `$route` service is usually used in conjunction with the {@link ngRoute.directive:ngView
|
|
ngView} directive. The role of the `ngView` directive is to include the view template for the current
|
|
route into the layout template. This makes it a perfect fit for our `index.html` template.
|
|
|
|
<div class="alert alert-info">
|
|
**Note:** Starting with AngularJS version 1.2, `ngRoute` is in its own module and must be loaded by
|
|
loading the additional `angular-route.js` file, which we download via Bower above.
|
|
</div>
|
|
|
|
__`app/index.html`:__
|
|
|
|
```html
|
|
<!doctype html>
|
|
<html lang="en" ng-app="phonecatApp">
|
|
<head>
|
|
...
|
|
<script src="bower_components/angular/angular.js"></script>
|
|
<script src="bower_components/angular-route/angular-route.js"></script>
|
|
<script src="js/app.js"></script>
|
|
<script src="js/controllers.js"></script>
|
|
</head>
|
|
<body>
|
|
|
|
<div ng-view></div>
|
|
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
We have added two new `<script>` tags in our index file to load up extra JavaScript files into our
|
|
application:
|
|
|
|
- `angular-route.js` : defines the Angular `ngRoute` module, which provides us with routing.
|
|
- `app.js` : this file now holds the root module of our application.
|
|
|
|
Note that we removed most of the code in the `index.html` template and replaced it with a single
|
|
line containing a div with the `ng-view` attribute. The code that we removed was placed into the
|
|
`phone-list.html` template:
|
|
|
|
__`app/partials/phone-list.html`:__
|
|
|
|
```html
|
|
<div class="container-fluid">
|
|
<div class="row">
|
|
<div class="col-md-2">
|
|
<!--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="col-md-10">
|
|
<!--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>
|
|
```
|
|
|
|
<div style="display:none">
|
|
TODO!
|
|
<img class="diagram" src="img/tutorial/tutorial_07_final.png">
|
|
</div>
|
|
|
|
We also added a placeholder template for the phone details view:
|
|
|
|
__`app/partials/phone-detail.html`:__
|
|
|
|
```html
|
|
TBD: detail view for <span>{{phoneId}}</span>
|
|
```
|
|
|
|
Note how we are using the `phoneId` expression which will be defined in the `PhoneDetailCtrl` controller.
|
|
|
|
## The App Module
|
|
|
|
To improve the organization of the app, we are making use of Angular's `ngRoute` module and we've
|
|
moved the controllers into their own module `phonecatControllers` (as shown below).
|
|
|
|
We added `angular-route.js` to `index.html` and created a new `phonecatControllers` module in
|
|
`controllers.js`. That's not all we need to do to be able to use their code, however. We also have
|
|
to add the modules as dependencies of our app. By listing these two modules as dependencies of
|
|
`phonecatApp`, we can use the directives and services they provide.
|
|
|
|
|
|
__`app/js/app.js`:__
|
|
|
|
```js
|
|
var phonecatApp = angular.module('phonecatApp', [
|
|
'ngRoute',
|
|
'phonecatControllers'
|
|
]);
|
|
|
|
...
|
|
```
|
|
|
|
Notice the second argument passed to `angular.module`, `['ngRoute', 'phonecatControllers']`. This
|
|
array lists the modules that `phonecatApp` depends on.
|
|
|
|
|
|
```js
|
|
...
|
|
|
|
phonecatApp.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'
|
|
});
|
|
}]);
|
|
```
|
|
|
|
Using the `phonecatApp.config()` method, we request the `$routeProvider` to be injected into our
|
|
config function and use the {@link ngRoute.$routeProvider#when `$routeProvider.when()`} method to
|
|
define our routes.
|
|
|
|
Our application routes are defined as follows:
|
|
|
|
* `when('/phones')`: 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.
|
|
|
|
* `when('/phones/:phoneId')`: 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.
|
|
|
|
* `otherwise({redirectTo: '/phones'})`: triggers a redirection to `/phones` when the browser
|
|
address doesn't match either of our routes.
|
|
|
|
|
|
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.
|
|
|
|
|
|
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
|
|
{@link ngRoute.$routeParams `$routeParams`} object.
|
|
|
|
|
|
## Controllers
|
|
|
|
__`app/js/controllers.js`:__
|
|
|
|
```js
|
|
var phonecatControllers = angular.module('phonecatControllers', []);
|
|
|
|
phonecatControllers.controller('PhoneListCtrl', ['$scope', '$http',
|
|
function ($scope, $http) {
|
|
$http.get('phones/phones.json').success(function(data) {
|
|
$scope.phones = data;
|
|
});
|
|
|
|
$scope.orderProp = 'age';
|
|
}]);
|
|
|
|
phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams',
|
|
function($scope, $routeParams) {
|
|
$scope.phoneId = $routeParams.phoneId;
|
|
}]);
|
|
```
|
|
|
|
Again, note that we created a new module called `phonecatControllers`. For small AngularJS
|
|
applications, it's common to create just one module for all of your controllers if there are just a
|
|
few. As your application grows it is quite common to refactor your code into additional modules.
|
|
For larger apps, you will probably want to create separate modules for each major feature of
|
|
your app.
|
|
|
|
Because our example app is relatively small, we'll just add all of our controllers to the
|
|
`phonecatControllers` module.
|
|
|
|
|
|
## 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.
|
|
|
|
```js
|
|
...
|
|
it('should redirect index.html to index.html#/phones', function() {
|
|
browser.get('app/index.html');
|
|
browser.getLocationAbsUrl().then(function(url) {
|
|
expect(url.split('#')[1]).toBe('/phones');
|
|
});
|
|
});
|
|
|
|
describe('Phone list view', function() {
|
|
beforeEach(function() {
|
|
browser.get('app/index.html#/phones');
|
|
});
|
|
...
|
|
|
|
describe('Phone detail view', function() {
|
|
|
|
beforeEach(function() {
|
|
browser.get('app/index.html#/phones/nexus-s');
|
|
});
|
|
|
|
|
|
it('should display placeholder page with phoneId', function() {
|
|
expect(element(by.binding('phoneId')).getText()).toBe('nexus-s');
|
|
});
|
|
});
|
|
```
|
|
|
|
|
|
You can now rerun `npm run protractor` to see the tests run.
|
|
|
|
|
|
# Experiments
|
|
|
|
* 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 `<div ng-view>` element. If you add
|
|
the same binding into the `phone-list.html` template, the binding will work as expected.
|
|
|
|
<div style="display: none">
|
|
* 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.
|
|
</div>
|
|
|
|
|
|
# Summary
|
|
|
|
With the routing set up and the phone list view implemented, we're ready to go to {@link step_08
|
|
step 8} to implement the phone details view.
|
|
|
|
|
|
<ul doc-tutorial-nav="7"></ul>
|
|
|
|
|
|
[bower]: http://bower.io
|