docs(tutorial): update to use v1.5.x and best practices

This is a major re-structuring of the tutorial app's codebase, aiming at applying established best
practices (in terms of file naming/layout and code organization) and utilizing several new features
and enhancements (most notably components) introduced in recent versions of Angular (especially
v1.5).

Apart from the overall changes, two new chapters were introduced: one on components and one on code
organization.

--
In the process, several other things were (incidentally) taken care of, including:

* Dependencies were upgraded to latest versions.
* Animations were polished.
* Outdated links were updated.
* The app's base URL was changed to `/` (instead of `/app/`).

BTW, this has been tested with the following versions of Node (on Windows 10) and everything worked
fine:

* 0.11.16
* 4.2.6
* 4.4.2
* 5.10.0
* 6.2.0

--
This was inspired by (and loosely based on) #13834.
Again, mad props to @teropa for leading the way :)

--
**Note:**
The old version of the tutorial, that is compatible with Angular version 1.4 or older, has been
saved on the `pre-v1.5.0-snapshot` branch of
[angular-phonecat](https://github.com/angular/angular-phonecat). The `v1.4.x` version of the
tutorial should be pointed to that branch instead of `master`.

--
Related to angular/angular-phonecat#326.
Related to angular/angular-seed#341.

Closes #14416
This commit is contained in:
Georgios Kalpakas
2016-03-27 21:32:36 +03:00
parent 4ae4cc9d46
commit c2033d7ff0
77 changed files with 14434 additions and 2333 deletions
+5
View File
@@ -647,6 +647,11 @@ ul.events > li {
padding-top: 50px;
}
.diagram {
margin-bottom: 10px;
margin-top: 30px;
}
@media only screen and (min-width: 769px) and (max-width: 991px) {
.main-body-grid {
margin-top: 160px;
+4 -3
View File
@@ -5,7 +5,8 @@ angular.module('tutorials', [])
'',
'step_00', 'step_01', 'step_02', 'step_03', 'step_04',
'step_05', 'step_06', 'step_07', 'step_08', 'step_09',
'step_10', 'step_11', 'step_12', 'the_end'
'step_10', 'step_11', 'step_12', 'step_13', 'step_14',
'the_end'
];
return {
scope: {},
@@ -43,7 +44,7 @@ angular.module('tutorials', [])
'<a href="http://angular.github.io/angular-phonecat/step-{{step}}/app">Step {{step}} Live Demo</a>.</p>\n' +
'</div>\n' +
'<p>The most important changes are listed below. You can see the full diff on ' +
'<a ng-href="https://github.com/angular/angular-phonecat/compare/step-{{step ? (step - 1): \'0~1\'}}...step-{{step}}" title="See diff on Github">GitHub</a>\n' +
'<a ng-href="https://github.com/angular/angular-phonecat/compare/step-{{step ? (step - 1): \'0~1\'}}...step-{{step}}" title="See diff on Github">GitHub</a>.\n' +
'</p>'
};
});
});
@@ -228,10 +228,10 @@
)
</p>
<p>
Code licensed under the
<a href="https://github.com/angular/angular.js/blob/master/LICENSE" target="_blank">The
MIT License</a>. Documentation licensed under <a
href="http://creativecommons.org/licenses/by/3.0/">CC BY 3.0</a>.
Code licensed under
<a href="https://github.com/angular/angular.js/blob/master/LICENSE" target="_blank">The MIT License</a>.
Documentation licensed under
<a href="http://creativecommons.org/licenses/by/3.0/" target="_blank">CC BY 3.0</a>.
</p>
</div>
</footer>
+141 -96
View File
@@ -6,12 +6,12 @@
# PhoneCat Tutorial App
A great way to get introduced to AngularJS is to work through this tutorial, which walks you through
the construction of an AngularJS web app. The app you will build is a catalog that displays a list
the construction of an Angular web app. The app you will build is a catalog that displays a list
of Android devices, lets you filter the list to see only devices that interest you, and then view
details for any device.
<img class="diagram" src="img/tutorial/catalog_screen.png" width="488" height="413" alt="demo
application running in the browser">
<img class="diagram" src="img/tutorial/catalog_screen.png" width="488" height="413"
alt="demo application running in the browser">
Follow the tutorial to see how Angular makes browsers smarter — without the use of native
extensions or plug-ins:
@@ -28,10 +28,11 @@ When you finish the tutorial you will be able to:
* Create a dynamic application that works in all modern browsers.
* Use data binding to wire up your data model to your views.
* Create and run unit tests, with Karma.
* Create and run end to end tests, with Protractor.
* Move application logic out of the template and into Controllers.
* Create and run end-to-end tests, with Protractor.
* Move application logic out of the template and into components and controllers.
* Get data from a server using Angular services.
* Apply animations to your application, using ngAnimate.
* Apply animations to your application, using the `ngAnimate` module.
* Structure your Angular applications in a modular way that scales well for larger projects.
* Identify resources for learning more about AngularJS.
The tutorial guides you through the entire process of building a simple application, including
@@ -42,16 +43,18 @@ You can go through the whole tutorial in a couple of hours or you may want to sp
really digging into it. If you're looking for a shorter introduction to AngularJS, check out the
{@link misc/started Getting Started} document.
# Get Started
# Environment Setup
The rest of this page explains how you can set up your local machine for development.
If you just want to read the tutorial then you can just go straight to the first step:
If you just want to _read_ the tutorial, you can go straight to the first step:
[Step 0 - Bootstrapping](tutorial/step_00).
# Working with the code
## Working with the Code
You can follow along with this tutorial and hack on the code in the comfort of your own computer.
In this way you can get hands-on practice of really writing AngularJS code and also on using the
This way, you can get hands-on practice of really writing Angular code and also on using the
recommended testing tools.
The tutorial relies on the use of the [Git][git] versioning system for source code management.
@@ -62,10 +65,11 @@ a few git commands.
### Install Git
You can download and install Git from http://git-scm.com/download. Once installed, you should have
access to the `git` command line tool. The main commands that you will need to use are:
access to the `git` command line tool. The main commands that you will need to use are:
* `git clone ...`: Clone a remote repository onto your local machine.
* `git checkout ...`: Check out a particular branch or a tagged version of the code to hack on.
- `git clone ...` : clone a remote repository onto your local machine
- `git checkout ...` : check out a particular branch or a tagged version of the code to hack on
### Download angular-phonecat
@@ -73,13 +77,14 @@ Clone the [angular-phonecat repository][angular-phonecat] located at GitHub by r
command:
```
git clone --depth=14 https://github.com/angular/angular-phonecat.git
git clone --depth=16 https://github.com/angular/angular-phonecat.git
```
This command creates the `angular-phonecat` directory in your current directory.
This command creates an `angular-phonecat` sub-directory in your current directory.
<div class="alert alert-info">The `--depth=14` option just tells Git to pull down only the last 14 commits. This makes the
download much smaller and faster.
<div class="alert alert-info">
The `--depth=16` option tells Git to pull down only the last 16 commits.
This makes the download much smaller and faster.
</div>
Change your current directory to `angular-phonecat`.
@@ -88,16 +93,16 @@ Change your current directory to `angular-phonecat`.
cd angular-phonecat
```
The tutorial instructions, from now on, assume you are running all commands from the
The tutorial instructions, from now on, assume you are running all commands from within the
`angular-phonecat` directory.
### Install Node.js
If you want to run the preconfigured local web-server and the test tools then you will also need
[Node.js v0.10.27+][node].
If you want to run the preconfigured local web server and the test tools then you will also need
[Node.js v4+][node].
You can download a Node.js installer for your operating system from http://nodejs.org/download/.
You can download a Node.js installer for your operating system from https://nodejs.org/en/download/.
Check the version of Node.js that you have installed by running the following command:
@@ -105,7 +110,7 @@ Check the version of Node.js that you have installed by running the following co
node --version
```
In Debian based distributions, there is a name clash with another utility called `node`. The
In Debian based distributions, there might be a name clash with another utility called `node`. The
suggested solution is to also install the `nodejs-legacy` apt package, which renames `node` to
`nodejs`.
@@ -115,12 +120,9 @@ nodejs --version
npm --version
```
<div class="alert alert-info">If you need to run different versions of node.js
in your local environment, consider installing
<a href="https://github.com/creationix/nvm" title="Node Version Manager Github Repo link">
Node Version Manager (nvm)
</a>.
<div class="alert alert-info">
If you need to run different versions of Node.js in your local environment, consider installing
[Node Version Manager (nvm)][nvm] or [Node Version Manager (nvm) for Windows][nvm-windows].
</div>
Once you have Node.js installed on your machine, you can download the tool dependencies by running:
@@ -129,30 +131,32 @@ Once you have Node.js installed on your machine, you can download the tool depen
npm install
```
This command reads angular-phonecat's `package.json` file and downloads the following tools
into the `node_modules` directory:
This command reads angular-phonecat's `package.json` file and downloads the following tools into the
`node_modules` directory:
- [Bower][bower] - client-side code package manager
- [Http-Server][http-server] - simple local static web server
- [Karma][karma] - unit test runner
- [Protractor][protractor] - end to end (E2E) test runner
* [Bower][bower] - client-side code package manager
* [Http-Server][http-server] - simple local static web server
* [Karma][karma] - unit test runner
* [Protractor][protractor] - end-to-end (E2E) test runner
Running `npm install` will also automatically use bower to download the Angular framework into the
Running `npm install` will also automatically use bower to download the AngularJS framework into the
`app/bower_components` directory.
<div class="alert alert-info">
Note the angular-phonecat project is setup to install and run these utilities via npm scripts.
This means that you do not have to have any of these utilities installed globally on your system
to follow the tutorial. See **Installing Helper Tools** below for more information.
to follow the tutorial. See [Installing Helper Tools](tutorial/#install-helper-tools-optional-)
below for more information.
</div>
The project is preconfigured with a number of npm helper scripts to make it easy to run the common
tasks that you will need while developing:
- `npm start` : start a local development web-server
- `npm test` : start the Karma unit test runner
- `npm run protractor` : run the Protractor end to end (E2E) tests
- `npm run update-webdriver` : install the drivers needed by Protractor
* `npm start`: Start a local development web server.
* `npm test`: Start the Karma unit test runner.
* `npm run protractor`: Run the Protractor end-to-end (E2E) tests.
* `npm run update-webdriver`: Install the drivers needed by Protractor.
### Install Helper Tools (optional)
@@ -167,7 +171,7 @@ For instance, to install the Bower command line executable you would do:
sudo npm install -g bower
```
*(Omit the sudo if running on Windows)*
_(Omit the sudo if running on Windows)_
Then you can run the bower tool directly, such as:
@@ -176,10 +180,10 @@ bower install
```
### Running Development Web Server
### Running the Development Web Server
While Angular applications are purely client-side code, and it is possible to open them in a web
browser directly from the file system, it is better to serve them from a HTTP web server. In
browser directly from the file system, it is better to serve them from an HTTP web server. In
particular, for security reasons, most modern browsers will not allow JavaScript to make server
requests if the page is loaded directly from the file system.
@@ -190,70 +194,64 @@ application during development. Start the web server by running:
npm start
```
This will create a local webserver that is listening to port 8000 on your local machine.
You can now browse to the application at:
```
http://localhost:8000/app/index.html
```
This will create a local web server that is listening to port 8000 on your local machine.
You can now browse to the application at http://localhost:8000/index.html.
<div class="alert alert-info">
To serve the web app on a different IP address or port, edit the "start" script within package.json.
You can use `-a` to set the address and `-p` to set the port.
To serve the web app on a different IP address or port, edit the "start" script within
`package.json`. You can use `-a` to set the address and `-p` to set the port. You also need to
update the `baseUrl` configuration property in `e2e-test/protractor.conf.js`.
</div>
### Running Unit Tests
We use unit tests to ensure that the JavaScript code in our application is operating correctly.
Unit tests focus on testing small isolated parts of the application. The unit tests are kept in the
`test/unit` directory.
Unit tests focus on testing small isolated parts of the application. The unit tests are kept in test
files (specs) side-by-side with the application code. This way it's easier to find them and keep
them up-to-date with the code under test. It also makes refactoring our app structure easier, since
tests are moved together with the source code.
The angular-phonecat project is configured to use [Karma][karma] to run the unit tests for the
application. Start Karma by running:
application. Start Karma by running:
```
npm test
```
This will start the Karma unit test runner. Karma will read the configuration file at
`test/karma.conf.js`. This configuration file tells Karma to:
This will start the Karma unit test runner. Karma will read the configuration file `karma.conf.js`,
located at the root of the project directory. This configuration file tells Karma to:
- open up a Chrome browser and connect it to Karma
- execute all the unit tests in this browser
- report the results of these tests in the terminal/command line window
- watch all the project's JavaScript files and re-run the tests whenever any of these change
* Open up instances of the Chrome and Firefox browsers and connect them to Karma.
* Execute all the unit tests in these browsers.
* Report the results of these tests in the terminal/command line window.
* Watch all the project's JavaScript files and re-run the tests whenever any of these change.
It is good to leave this running all the time, in the background, as it will give you immediate
feedback about whether your changes pass the unit tests while you are working on the code.
### Running End to End Tests
### Running E2E Tests
We use End to End tests to ensure that the application as a whole operates as expected.
End to End tests are designed to test the whole client side application, in particular that the
views are displaying and behaving correctly. It does this by simulating real user interaction with
the real application running in the browser.
We use E2E (end-to-end) tests to ensure that the application as a whole operates as expected.
E2E tests are designed to test the whole client-side application, in particular that the views are
displaying and behaving correctly. It does this by simulating real user interaction with the real
application running in the browser.
The End to End tests are kept in the `test/e2e` directory.
The E2E tests are kept in the `e2e-tests` directory.
The angular-phonecat project is configured to use [Protractor][protractor] to run the End to End
tests for the application. Protractor relies upon a set of drivers to allow it to interact with
the browser. You can install these drivers by running:
The angular-phonecat project is configured to use [Protractor][protractor] to run the E2E tests for
the application. Protractor relies upon a set of drivers to allow it to interact with the browser.
You can install these drivers by running:
```
npm run update-webdriver
```
*(You should only need to do this once.)*
You will need to have Java present on your dev machine to allow the Selenium standalone to be started.
Check if you already have java installed by opening a terminal/command line window and typing
'''
java -version
'''
If java is already installed and exists in the PATH then you will be shown the version installed,
if, however you receive a message that "java is not recognized as an internal command or external
command" you will need to install [java].
<div class="alert alert-info">
You don't have to manually run this command. Our npm scripts are configured so that it will be
automatically executed as part of the command that runs the E2E tests.
</div>
Since Protractor works by interacting with a running application, we need to start our web server:
@@ -261,32 +259,79 @@ Since Protractor works by interacting with a running application, we need to sta
npm start
```
Then in a separate terminal/command line window, we can run the Protractor test scripts against the
application by running:
Then, in a _separate_ terminal/command line window, we can run the Protractor test scripts against
the application by running:
```
npm run protractor
```
Protractor will read the configuration file at `test/protractor-conf.js`. This configuration tells
Protractor to:
Protractor will read the configuration file at `e2e-tests/protractor.conf.js`. This configuration
file tells Protractor to:
- open up a Chrome browser and connect it to the application
- execute all the End to End tests in this browser
- report the results of these tests in the terminal/command line window
- close down the browser and exit
* Open up a Chrome browser and connect it to the application.
* Execute all the E2E tests in this browser.
* Report the results of these tests in the terminal/command line window.
* Close the browser and exit.
It is good to run the end to end tests whenever you make changes to the HTML views or want to check
that the application as a whole is executing correctly. It is very common to run End to End tests
before pushing a new commit of changes to a remote repository.
It is good to run the E2E tests whenever you make changes to the HTML views or want to check that
the application as a whole is executing correctly. It is very common to run E2E tests before pushing
a new commit of changes to a remote repository.
### Common Issues
<br />
**Firewall / Proxy issues**
Git and other tools, often use the `git:` protocol for accessing files in remote repositories.
Some firewall configurations are blocking `git://` URLs, which leads to errors when trying to clone
repositories or download dependencies. (For example corporate firewalls are "notorious" for blocking
`git:`.)
If you run into this issue, you can force the use of `https:` instead, by running the following
command: `git config --global url."https://".insteadOf git://`
<br />
**Updating WebDriver takes too long**
Running `update-webdriver` for the first time may take from several seconds up to a few minutes
(depending on your hardware and network connection). If you cancel the operation (e.g. using
`Ctrl+C`), you might get errors, when trying to run Protractor later.
In that case, you can delete the `node_modules/` directory and run `npm install` again.
<br />
**Protractor dependencies**
Under the hood, Protractor uses the [Selenium Stadalone Server][selenium], which in turn requires
the [Java Development Kit (JDK)][jdk] to be installed on your local machine. Check this by running
`java -version` from the command line.
If JDK is not already installed, you can download it [here][jdk-download].
<br />
**Error running the web server**
The web server is configured to use port 8000. If the port is already in use (for example by another
instance of a running web server) you will get an `EADDRINUSE` error. Make sure the port is
available, before running `npm start`.
<hr />
Now that you have set up your local machine, let's get started with the tutorial:
{@link step_00 Step 0 - Bootstrapping}
Now that you have set up your local machine, let's get started with the tutorial: {@link step_00 Step 0 - Bootstrapping}
[git]: http://git-scm.com/
[node]: http://nodejs.org/
[angular-phonecat]: https://github.com/angular/angular-phonecat
[protractor]: https://github.com/angular/protractor
[bower]: http://bower.io/
[git]: http://git-scm.com/
[http-server]: https://github.com/nodeapps/http-server
[karma]: https://github.com/karma-runner/karma
[java]: https://www.java.com/en/download/help/download_options.xml
[jdk]: https://en.wikipedia.org/wiki/Java_Development_Kit
[jdk-download]: http://www.oracle.com/technetwork/java/javase/downloads/index.html
[karma]: https://karma-runner.github.io/
[node]: http://nodejs.org/
[nvm]: https://github.com/creationix/nvm
[nvm-windows]: https://github.com/coreybutler/nvm-windows
[protractor]: https://github.com/angular/protractor
[selenium]: http://docs.seleniumhq.org/
+87 -72
View File
@@ -7,11 +7,12 @@
In this step of the tutorial, you will become familiar with the most important source code files of
the AngularJS phonecat app. You will also learn how to start the development servers bundled with
angular-seed, and run the application in the browser.
the AngularJS Phonecat App. You will also learn how to start the development servers bundled with
[angular-seed][angular-seed], and run the application in the browser.
Before you continue, make sure you have set up your development environment and installed all necessary
dependencies, as described in {@link index#get-started Get Started}.
Before you continue, make sure you have set up your development environment and installed all
necessary dependencies, as described in the {@link tutorial/#environment-setup Environment Setup}
section.
In the `angular-phonecat` directory, run this command:
@@ -19,118 +20,130 @@ In the `angular-phonecat` directory, run this command:
git checkout -f step-0
```
This resets your workspace to step 0 of the tutorial app.
You must repeat this for every future step in the tutorial and change the number to the number of
the step you are on. This will cause any changes you made within your working directory to be lost.
If you haven't already done so you need to install the dependencies by running:
If you haven't already done so, you need to install the dependencies by running:
```
npm install
```
To see the app running in a browser, open a *separate* terminal/command line tab or window, then
run `npm start` to start the web server. Now, open a browser window for the app and navigate to
<a href="http://localhost:8000/app/" target="_blank" title="Open app on localhost">`http://localhost:8000/app/`</a>
To see the app running in a browser, open a _separate_ terminal/command line tab or window, then run
`npm start` to start the web server. Now, open a browser window for the app and navigate to
http://localhost:8000/index.html.
Note that if you already ran the master branch app prior to checking out step-0, you may see the cached
master version of the app in your browser window at this point. Just hit refresh to re-load the page.
Note that if you already ran the master branch app prior to checking out step-0, you may see the
cached master version of the app in your browser window at this point. Just hit refresh to re-load
the page.
You can now see the page in your browser. It's not very exciting, but that's OK.
The HTML page that displays "Nothing here yet!" was constructed with the HTML code shown below.
The code contains some key Angular elements that we will need as we progress.
__`app/index.html`:__
**`app/index.html`:**
```html
<!doctype html>
<html lang="en" ng-app>
<head>
<meta charset="utf-8">
<title>My HTML File</title>
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">
<link rel="stylesheet" href="css/app.css">
<script src="bower_components/angular/angular.js"></script>
</head>
<body>
<head>
<meta charset="utf-8">
<title>My HTML File</title>
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css" />
<script src="bower_components/angular/angular.js"></script>
</head>
<body>
<p>Nothing here {{'yet' + '!'}}</p>
<p>Nothing here {{'yet' + '!'}}</p>
</body>
</body>
</html>
```
## What is the code doing?
**`ng-app` directive:**
<br />
**`ng-app` attribute:**
<html ng-app>
```html
<html ng-app>
```
The `ng-app` attribute represents an Angular directive named `ngApp` (Angular uses
`spinal-case` for its custom attributes and `camelCase` for the corresponding directives
which implement them).
This directive is used to flag the html element that Angular should consider to be the root element
of our application.
This gives application developers the freedom to tell Angular if the entire html page or only a
portion of it should be treated as the Angular application.
The `ng-app` attribute represents an Angular directive, named `ngApp` (Angular uses `kebab-case` for
its custom attributes and `camelCase` for the corresponding directives which implement them). This
directive is used to flag the HTML element that Angular should consider to be the root element of
our application. This gives application developers the freedom to tell Angular if the entire HTML
page or only a portion of it should be treated as the AngularJS application.
**AngularJS script tag:**
For more info on `ngApp`, check out the {@link ngApp API Reference}.
<script src="bower_components/angular/angular.js">
<br />
**`angular.js` script tag:**
This code downloads the `angular.js` script which registers a callback that will be executed by the
```html
<script src="bower_components/angular/angular.js"></script>
```
This code downloads the `angular.js` script which registers a callback that will be executed by the
browser when the containing HTML page is fully downloaded. When the callback is executed, Angular
looks for the {@link ng.directive:ngApp ngApp} directive. If
Angular finds the directive, it will bootstrap the application with the root of the application DOM
being the element on which the `ngApp` directive was defined.
looks for the {@link ngApp ngApp} directive. If Angular finds the directive, it will bootstrap the
application with the root of the application DOM being the element on which the `ngApp` directive
was defined.
For more info on bootstrapping your app, checkout the [Bootstrap](guide/bootstrap) section of the
Developer Guide.
<br />
**Double-curly binding with an expression:**
Nothing here {{'yet' + '!'}}
```html
Nothing here {{'yet' + '!'}}
```
This line demonstrates two core features of Angular's templating capabilities:
* a binding, denoted by double-curlies `{{ }}`
* a simple expression `'yet' + '!'` used in this binding.
* A binding, denoted by double-curlies: `{{ }}`
* A simple expression used in this binding: `'yet' + '!'`
The binding tells Angular that it should evaluate an expression and insert the result into the
DOM in place of the binding. Rather than a one-time insert, as we'll see in the next steps, a
binding will result in efficient continuous updates whenever the result of the expression
evaluation changes.
The binding tells Angular that it should evaluate an expression and insert the result into the DOM
in place of the binding. As we will see in the next steps, rather than a one-time insert, a binding
will result in efficient continuous updates whenever the result of the expression evaluation
changes.
{@link guide/expression Angular expression} is a JavaScript-like code snippet that is
evaluated by Angular in the context of the current model scope, rather than within the scope of
the global context (`window`).
{@link guide/expression Angular expressions} are JavaScript-like code snippets that are evaluated by
Angular in the context of the current model scope, rather than within the scope of the global
context (`window`).
As expected, once this template is processed by Angular, the html page contains the text:
"Nothing here yet!".
As expected, once this template is processed by Angular, the HTML page contains the text:
## Bootstrapping AngularJS apps
```
Nothing here yet!
```
Bootstrapping AngularJS apps automatically using the `ngApp` directive is very easy and suitable
for most cases. In advanced cases, such as when using script loaders, you can use the
{@link guide/bootstrap imperative / manual way} to bootstrap the app.
## Bootstrapping Angular Applications
There are 3 important things that happen during the app bootstrap:
Bootstrapping Angular applications automatically using the `ngApp` directive is very easy and
suitable for most cases. In advanced cases, such as when using script loaders, you can use the
{@link guide/bootstrap#manual-initialization imperative/manual way} to bootstrap the application.
There are 3 important things that happen during the bootstrap phase:
1. The {@link auto.$injector injector} that will be used for dependency injection is created.
2. The injector will then create the {@link ng.$rootScope root scope} that will
become the context for the model of our application.
2. The injector will then create the {@link ng.$rootScope root scope} that will become the context
for the model of our application.
3. Angular will then "compile" the DOM starting at the `ngApp` root element, processing any
directives and bindings found along the way.
Once an application is bootstrapped, it will then wait for incoming browser events (such as mouse
click, key press or incoming HTTP response) that might change the model. Once such an event occurs,
Angular detects if it caused any model changes and if changes are found, Angular will reflect them
in the view by updating all of the affected bindings.
clicks, key presses or incoming HTTP responses) that might change the model. Once such an event
occurs, Angular detects if it caused any model changes and if changes are found, Angular will
reflect them in the view by updating all of the affected bindings.
The structure of our application is currently very simple. The template contains just one directive
and one static binding, and our model is empty. That will soon change!
@@ -140,27 +153,29 @@ and one static binding, and our model is empty. That will soon change!
## What are all these files in my working directory?
Most of the files in your working directory come from the [angular-seed project][angular-seed] which
is typically used to bootstrap new Angular projects. The seed project is pre-configured to install
the angular framework (via `bower` into the `app/bower_components/` folder) and tools for developing
a typical web app (via `npm`).
Most of the files in your working directory come from the [angular-seed project][angular-seed],
which is typically used to bootstrap new AngularJS projects. The seed project is pre-configured to
install the AngularJS framework (via `bower` into the `app/bower_components/` directory) and tools
for developing and testing a typical web application (via `npm`).
For the purposes of this tutorial, we modified the angular-seed with the following changes:
* Removed the example app
* Added phone images to `app/img/phones/`
* Added phone data files (JSON) to `app/phones/`
* Removed the example app.
* Removed unused dependencies.
* Added phone images to `app/img/phones/`.
* Added phone data files (JSON) to `app/phones/`.
* Added a dependency on [Bootstrap](http://getbootstrap.com) in the `bower.json` file.
# Experiments
* Try adding a new expression to the `index.html` that will do some math:
<div></div>
<p>1 + 2 = {{ 1 + 2 }}</p>
* Try adding a new expression to `index.html` that will do some math:
```html
<p>1 + 2 = {{1 + 2}}</p>
```
# Summary
+9 -5
View File
@@ -12,11 +12,11 @@ dynamically display the same result with any set of data.
In this step you will add some basic information about two cell phones to an HTML page.
- The page now contains a list with information about two phones.
* The page now contains a list with information about two phones.
<div doc-tutorial-reset="1"></div>
<br />
**`app/index.html`:**
```html
@@ -39,15 +39,19 @@ In this step you will add some basic information about two cell phones to an HTM
# Experiments
<div></div>
* Try adding more static HTML to `index.html`. For example:
<p>Total number of phones: 2</p>
```html
<p>Total number of phones: 2</p>
```
# Summary
This addition to your app uses static HTML to display the list. Now, let's go to {@link step_02
step 2} to learn how to use AngularJS to dynamically generate the same list.
This addition to your app uses static HTML to display the list. Now, let's go to
{@link step_02 step 2} to learn how to use Angular to dynamically generate the same list.
<ul doc-tutorial-nav="1"></ul>
+171 -114
View File
@@ -6,37 +6,38 @@
<ul doc-tutorial-nav="2"></ul>
Now it's time to make the web page dynamic — with AngularJS. We'll also add a test that verifies the
code for the controller we are going to add.
Now, it's time to make the web page dynamic — with AngularJS. We will also add a test that verifies
the code for the controller we are going to add.
There are many ways to structure the code for an application. For Angular apps, we encourage the use of
[the Model-View-Controller (MVC) design pattern](http://en.wikipedia.org/wiki/ModelViewController)
to decouple the code and to separate concerns. With that in mind, let's use a little Angular and
JavaScript to add model, view, and controller components to our app.
There are many ways to structure the code for an application. For Angular applications, we encourage
the use of the [Model-View-Controller (MVC) design pattern][mvc-pattern] to decouple the code and
separate concerns. With that in mind, let's use a little Angular and JavaScript to add models,
views, and controllers to our app.
- The list of three phones is now generated dynamically from data
* The list of three phones is now generated dynamically from data
<div doc-tutorial-reset="2"></div>
## View and Template
In Angular, the __view__ is a projection of the model through the HTML __template__. This means that
In Angular, the **view** is a projection of the model through the HTML **template**. This means that
whenever the model changes, Angular refreshes the appropriate binding points, which updates the
view.
The view component is constructed by Angular from this template:
The view is constructed by Angular from this template.
__`app/index.html`:__
<br />
**`app/index.html`:**
```html
<html ng-app="phonecatApp">
<head>
...
<script src="bower_components/angular/angular.js"></script>
<script src="js/controllers.js"></script>
<script src="app.js"></script>
</head>
<body ng-controller="PhoneListCtrl">
<body ng-controller="PhoneListController">
<ul>
<li ng-repeat="phone in phones">
@@ -49,97 +50,117 @@ __`app/index.html`:__
</html>
```
We replaced the hard-coded phone list with the {@link ng.directive:ngRepeat ngRepeat directive}
and two {@link guide/expression Angular expressions}:
We replaced the hard-coded phone list with the {@link ngRepeat ngRepeat} directive and two
{@link guide/expression Angular expressions}:
* The `ng-repeat="phone in phones"` attribute in the `<li>` tag is an Angular repeater directive.
The repeater tells Angular to create a `<li>` element for each phone in the list using the `<li>`
tag as the template.
* The expressions wrapped in curly braces (`{{phone.name}}` and `{{phone.snippet}}`) will be replaced
by the value of the expressions.
* The `ng-repeat="phone in phones"` attribute on the `<li>` tag is an Angular repeater directive.
The repeater tells Angular to create a `<li>` element for each phone in the list, using the `<li>`
tag as the template.
* The expressions wrapped in curly braces (`{{phone.name}}` and `{{phone.snippet}}`) will be
replaced by the values of the expressions.
We have added a new directive, called `ng-controller`, which attaches a `PhoneListCtrl`
__controller__ to the &lt;body&gt; tag. At this point:
We have also added a new directive, called {@link ngController ngController}, which attaches a
`PhoneListController` **controller** to the `<body>` tag. At this point:
* The expressions in curly braces (`{{phone.name}}` and `{{phone.snippet}}`) denote
bindings, which are referring to our application model, which is set up in our `PhoneListCtrl`
controller.
* `PhoneListController` is in charge of the DOM sub-tree under (and including) the `<body>` element.
* The expressions in curly braces (`{{phone.name}}` and `{{phone.snippet}}`) denote bindings, which
are referring to our application model, which is set up in our `PhoneListController` controller.
<div class="alert alert-info">
Note: We have specified an {@link angular.Module Angular Module} to load using `ng-app="phonecatApp"`,
where `phonecatApp` is the name of our module. This module will contain the `PhoneListCtrl`.
Note: We have specified an {@link angular.Module Angular Module} to load using
`ng-app="phonecatApp"`, where `phonecatApp` is the name of our module. This module will contain
the `PhoneListController`.
</div>
<img class="diagram" src="img/tutorial/tutorial_02.png">
## Model and Controller
The data __model__ (a simple array of phones in object literal notation) is now instantiated within
the `PhoneListCtrl` __controller__. The __controller__ is simply a constructor function that takes a
`$scope` parameter:
The data **model** (a simple array of phones in object literal notation) is now instantiated within
the `PhoneListController` **controller**. The **controller** is simply a constructor function that
takes a `$scope` parameter:
__`app/js/controllers.js`:__
<br />
**`app/app.js`:**
```js
// Define the `phonecatApp` module
var phonecatApp = angular.module('phonecatApp', []);
phonecatApp.controller('PhoneListCtrl', function ($scope) {
// Define the `PhoneListController` controller on the `phonecatApp` module
phonecatApp.controller('PhoneListController', function PhoneListController($scope) {
$scope.phones = [
{'name': 'Nexus S',
'snippet': 'Fast just got faster with Nexus S.'},
{'name': 'Motorola XOOM™ with Wi-Fi',
'snippet': 'The Next, Next Generation tablet.'},
{'name': 'MOTOROLA XOOM™',
'snippet': 'The Next, Next Generation tablet.'}
{
name: 'Nexus S',
snippet: 'Fast just got faster with Nexus S.'
}, {
name: 'Motorola XOOM™ with Wi-Fi',
snippet: 'The Next, Next Generation tablet.'
}, {
name: 'MOTOROLA XOOM™',
snippet: 'The Next, Next Generation tablet.'
}
];
});
```
Here we declared a controller called `PhoneListCtrl` and registered it in an AngularJS
module, `phonecatApp`. Notice that our `ng-app` directive (on the `<html>` tag) now specifies the `phonecatApp`
module name as the module to load when bootstrapping the Angular application.
Here we declared a controller called `PhoneListController` and registered it in an Angular module,
`phonecatApp`. Notice that our `ngApp` directive (on the `<html>` tag) now specifies the
`phonecatApp` module name as the module to load when bootstrapping the application.
Although the controller is not yet doing very much, it plays a crucial role. By providing context
for our data model, the controller allows us to establish data-binding between
the model and the view. We connected the dots between the presentation, data, and logic components
as follows:
for our data model, the controller allows us to establish data-binding between the model and the
view. We connected the dots between the presentation, data, and logic components as follows:
* The {@link ng.directive:ngController ngController} directive, located on the `<body>` tag,
references the name of our controller, `PhoneListCtrl` (located in the JavaScript file
`controllers.js`).
* The {@link ngController ngController} directive, located on the `<body>` tag, references the name
of our controller, `PhoneListController` (located in the JavaScript file `app.js`).
* The `PhoneListController` controller attaches the phone data to the `$scope` that was injected
into our controller function. This _scope_ is a prototypal descendant of the _root scope_ that was
created when the application was defined. This controller scope is available to all bindings
located within the `<body ng-controller="PhoneListController">` tag.
* The `PhoneListCtrl` controller attaches the phone data to the `$scope` that was injected into our
controller function. This *scope* is a prototypical descendant of the *root scope* that was created
when the application was defined. This controller scope is available to all bindings located within
the `<body ng-controller="PhoneListCtrl">` tag.
### Scope
The concept of a scope in Angular is crucial. A scope can be seen as the glue which allows the
template, model and controller to work together. Angular uses scopes, along with the information
template, model, and controller to work together. Angular uses scopes, along with the information
contained in the template, data model, and controller, to keep models and views separate, but in
sync. Any changes made to the model are reflected in the view; any changes that occur in the view
are reflected in the model.
To learn more about Angular scopes, see the {@link ng.$rootScope.Scope angular scope documentation}.
<img class="diagram" src="img/tutorial/tutorial_02.png">
## Tests
<div class="alert alert-warning">
<p>
Angular scopes prototypally inherit from their parent scope, all the way up to the *root scope*
of the application. As a result, assigning values directly on the scope makes it easy to share
data across different parts of the page and create interactive applications.
While this approach works for prototypes and smaller applications, it quickly leads to tight
coupling and difficulty to reason about changes in our data model.
</p>
<p>
In the next step, we will learn how to better organize our code, by "packaging" related pieces
of application and presentation logic into isolated, reusable entities, called components.
</p>
</div>
# Testing
The "Angular way" of separating controller from the view, makes it easy to test code as it is being
developed. If our controller is available on the global namespace then we could simply instantiate it
with a mock `scope` object:
__`test/e2e/scenarios.js`:__
developed. If our controller were available on the global namespace, we could simply instantiate it
with a mock scope object:
<br />
```js
describe('PhoneListCtrl', function(){
describe('PhoneListController', function() {
it('should create "phones" model with 3 phones', function() {
var scope = {},
ctrl = new PhoneListCtrl(scope);
it('should create a `phones` model with 3 phones', function() {
var scope = {};
var ctrl = new PhoneListController(scope);
expect(scope.phones.length).toBe(3);
});
@@ -147,30 +168,31 @@ describe('PhoneListCtrl', function(){
});
```
The test instantiates `PhoneListCtrl` and verifies that the phones array property on the scope
contains three records. This example demonstrates how easy it is to create a unit test for code in
Angular. Since testing is such a critical part of software development, we make it easy to create
tests in Angular so that developers are encouraged to write them.
The test instantiates `PhoneListController` and verifies that the phones array property on the
scope contains three records. This example demonstrates how easy it is to create a unit test for
code in Angular. Since testing is such a critical part of software development, we make it easy to
create tests in Angular so that developers are encouraged to write them.
### Testing non-Global Controllers
In practice, you will not want to have your controller functions in the global namespace. Instead,
you can see that we have registered it via an anonymous constructor function on the `phonecatApp`
module.
## Testing non-global Controllers
In practice, you will not want to have your controller functions in the global namespace. Instead,
you can see that we have registered it via a constructor function on the `phonecatApp` module.
In this case Angular provides a service, `$controller`, which will retrieve your controller by name.
Here is the same test using `$controller`:
__`test/unit/controllersSpec.js`:__
<br />
**`app/app.spec.js`:**
```js
describe('PhoneListCtrl', function(){
describe('PhoneListController', function() {
beforeEach(module('phonecatApp'));
it('should create "phones" model with 3 phones', inject(function($controller) {
var scope = {},
ctrl = $controller('PhoneListCtrl', {$scope:scope});
it('should create a `phones` model with 3 phones', inject(function($controller) {
var scope = {};
var ctrl = $controller('PhoneListController', {$scope: scope});
expect(scope.phones.length).toBe(3);
}));
@@ -179,29 +201,46 @@ describe('PhoneListCtrl', function(){
```
* Before each test we tell Angular to load the `phonecatApp` module.
* We ask Angular to `inject` the `$controller` service into our test function
* We use `$controller` to create an instance of the `PhoneListCtrl`
* We ask Angular to `inject` the `$controller` service into our test function.
* We use `$controller` to create an instance of the `PhoneListController`.
* With this instance, we verify that the phones array property on the scope contains three records.
<div class="alert alert-info">
<p>**A note on file naming:**</p>
<p>
As already mentioned in the [introduction](tutorial/#running-unit-tests), the unit test files
(specs) are kept side-by-side with the application code. We name our specs after the file
containing the code to be tested plus a specific suffix to distinguish them from files
containing application code. Note that test files are still plain JavaScript files, so they have
a `.js` file extension.
</p>
<p>
In this tutorial, we are using the `.spec` suffix. So the test file corresponding to
`something.js` would be called `something.spec.js`.
(Another common convention is to use a `_spec` or `_test` suffix.)
</p>
</div>
### Writing and Running Tests
Angular developers prefer the syntax of Jasmine's Behavior-driven Development (BDD) framework when
writing tests. Although Angular does not require you to use Jasmine, we wrote all of the tests in
this tutorial in Jasmine v1.3. You can learn about Jasmine on the [Jasmine home page][jasmine] and
at the [Jasmine docs][jasmine-docs].
## Writing and Running Tests
The angular-seed project is pre-configured to run unit tests using [Karma][karma] but you will need
Many Angular developers prefer the syntax of
[Jasmine's Behavior-Driven Development (BDD) framework][jasmine-home], when writing tests. Although
Angular does not require you to use Jasmine, we wrote all of the tests in this tutorial in Jasmine
v2.4. You can learn about Jasmine on the [Jasmine home page][jasmine-home] and at the
[Jasmine docs][jasmine-docs].
The angular-seed project is pre-configured to run unit tests using [Karma][karma], but you will need
to ensure that Karma and its necessary plugins are installed. You can do this by running
`npm install`.
To run the tests, and then watch the files for changes: `npm test`.
To run the tests, and then watch the files for changes execute: `npm test`
* Karma will start new instances of Chrome and Firefox browsers automatically. Just ignore them and
let them run in the background. Karma will use these browsers for test execution.
* If you only have one of the browsers installed on your machine (either Chrome or Firefox), make
sure to update the karma configuration file before running the test. Locate the configuration file
in `test/karma.conf.js`, then update the `browsers` property.
sure to update the karma configuration file (`karma.conf.js`), before running the test. Locate the
configuration file in the root directory and update the `browsers` property.
E.g. if you only have Chrome installed:
<pre>
@@ -213,23 +252,27 @@ To run the tests, and then watch the files for changes: `npm test`.
* You should see the following or similar output in the terminal:
<pre>
info: Karma server started at http://localhost:9876/
info (launcher): Starting browser "Chrome"
info (Chrome 22.0): Connected on socket id tPUm9DXcLHtZTKbAEO-n
Chrome 22.0: Executed 1 of 1 SUCCESS (0.093 secs / 0.004 secs)
INFO [karma]: Karma server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 49.0]: Connected on socket ... with id ...
Chrome 49.0: Executed 1 of 1 SUCCESS (0.05 secs / 0.04 secs)
</pre>
Yay! The test passed! Or not...
* To rerun the tests, just change any of the source or test .js files. Karma will notice the change
* To rerun the tests, just change any of the source or test `.js` files. Karma will notice the change
and will rerun the tests for you. Now isn't that sweet?
<div class="alert alert-info">
Make sure you don't minimize the browser that Karma opened. On some OS, memory assigned to a minimized
browser is limited, which results in your karma tests running extremely slow.
Make sure you don't minimize the browser that Karma opened. On some OS, memory assigned to a
minimized browser is limited, which results in your karma tests running extremely slow.
</div>
# Experiments
<div></div>
* Add another binding to `index.html`. For example:
```html
@@ -238,46 +281,60 @@ browser is limited, which results in your karma tests running extremely slow.
* Create a new model property in the controller and bind to it from the template. For example:
$scope.name = "World";
```js
$scope.name = 'world';
```
Then add a new binding to `index.html`:
<p>Hello, {{name}}!</p>
```html
<p>Hello, {{name}}!</p>
```
Refresh your browser and verify that it says "Hello, World!".
Refresh your browser and verify that it says 'Hello, world!'.
* Update the unit test for the controller in `./test/unit/controllersSpec.js` to reflect the previous change. For example by adding:
* Update the unit test for the controller in `app/app.spec.js` to reflect the previous change.
For example by adding:
expect(scope.name).toBe('World');
```js
expect(scope.name).toBe('world');
```
* Create a repeater in `index.html` that constructs a simple table:
<table>
<tr><th>row number</th></tr>
<tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i}}</td></tr>
</table>
```html
<table>
<tr><th>Row number</th></tr>
<tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i}}</td></tr>
</table>
```
Now, make the list 1-based by incrementing `i` by one in the binding:
<table>
<tr><th>row number</th></tr>
<tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i+1}}</td></tr>
</table>
```html
<table>
<tr><th>Row number</th></tr>
<tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i+1}}</td></tr>
</table>
```
Extra points: try and make an 8x8 table using an additional `ng-repeat`.
Extra points: Try and make an 8x8 table using an additional `ng-repeat`.
* Make the unit test fail by changing `expect(scope.phones.length).toBe(3)` to instead use `toBe(4)`.
* Make the unit test fail by changing `expect(scope.phones.length).toBe(3)` to instead use
`toBe(4)`.
# Summary
You now have a dynamic app that features separate model, view, and controller components, and you
are testing as you go. Now, let's go to {@link step_03 step 3} to learn how to add full text search
to the app.
We now have a dynamic application which separates models, views, and controllers, and we are testing
as we go. Let's go to {@link step_03 step 3} to learn how to improve our application's architecture,
by utilizing components.
<ul doc-tutorial-nav="2"></ul>
[jasmine]: http://jasmine.github.io/
[jasmine-docs]: http://jasmine.github.io/1.3/introduction.html
[karma]: http://karma-runner.github.io/
[jasmine-docs]: http://jasmine.github.io/2.4/introduction.html
[jasmine-home]: http://jasmine.github.io/
[karma]: https://karma-runner.github.io/
[mvc-pattern]: http://en.wikipedia.org/wiki/ModelViewController
+230 -171
View File
@@ -1,223 +1,282 @@
@ngdoc tutorial
@name 3 - Filtering Repeaters
@name 3 - Components
@step 3
@description
<ul doc-tutorial-nav="3"></ul>
We did a lot of work in laying a foundation for the app in the last step, so now we'll do something
simple; we will add full text search (yes, it will be simple!). We will also write an end-to-end
test, because a good end-to-end test is a good friend. It stays with your app, keeps an eye on it,
and quickly detects regressions.
In the previous step, we saw how a controller and a template worked together to convert a static
HTML page into a dynamic view. This is a very common pattern in Single-Page Applications in general
(and Angular applications in particular):
* Instead of creating a static HTML page on the server, the client-side code "takes over" and
interacts dynamically with the view, updating it instantly to reflect changes in model data or
state, usually as a result of user interaction (we'll see an example shortly in
{@link step_05 step 5}).
The **template** (the part of the view containing the bindings and presentation logic) acts as a
a blueprint for how our data should be organized and presented to the user.
The **controller** provides the context in which the bindings are evaluated and applies behavior
and logic to our template.
There are still a couple of areas we can do better:
1. What if we want to reuse the same functionality in a different part of our application ?<br />
We would need to duplicate the whole template (including the controller). This is error-prone and
hurts maintainability.
2. The scope, that glues our controller and template together into a dynamic view, is not isolated
from other parts of the page. What this means is that a random, unrelated change in a different
part of the page (e.g. a property-name conflict) could have unexpected and hard-to-debug side
effects on our view.
(OK, this might not be a real concern in our minimal example, but it **is** a valid concern for
bigger, real-world applications.)
* The app now has a search box. Notice that the phone list on the page changes depending on what a
user types into the search box.
<div doc-tutorial-reset="3"></div>
## Controller
## Components to the rescue!
We made no changes to the controller.
Since this combination (template + controller) is such a common and recurring pattern, Angular
provides an easy and concise way to combine them together into reusable and isolated entities,
known as _components_.
Additionally, Angular will create a so called _isolate scope_ for each instance of our component,
which means no prototypal inheritance and no risk of our component affecting other parts of the
application or vice versa.
<div class="alert alert-info">
<p>
Since this is an introductory tutorial, we are not going to dive deep into all features provided
by Angular **components**. You can read more about components and their usage patterns in the
[Components](guide/component) section of the Developer Guide.
</p>
<p>
In fact, one could think of components as an opinionated and stripped-down version of their more
complex and verbose (but powerful) siblings, **directives**, which are Angular's way of teaching
HTML new tricks. You can read all about them in the [Directives](guide/directive) section of the
Developer Guide.
</p>
<p>
(**Note:** Directives are an advanced topic, so you might want to postpone studying them, until
you have mastered the basics.)
</p>
</div>
## Template
To create a component, we use the {@link angular.Module#component .component()} method of an
{@link module Angular module}. We must provide the name of the component and the Component
Definition Object (CDO for short).
__`app/index.html`:__
Remember that (since components are also directives) the name of the component is in `camelCase`,
but we will use `kebab-case`, when referring to it in our HTML.
```html
<div class="container-fluid">
<div class="row">
<div class="col-md-2">
<!--Sidebar content-->
In its simplest form, the CDO will just contain a template and a controller. (We can actually omit
the controller and Angular will create a dummy controller for us. This is useful for simple
"presentational" components, that don't attach any behavior to the template.)
Search: <input ng-model="query">
</div>
<div class="col-md-10">
<!--Body content-->
<ul class="phones">
<li ng-repeat="phone in phones | filter:query">
{{phone.name}}
<p>{{phone.snippet}}</p>
</li>
</ul>
</div>
</div>
</div>
```
We added a standard HTML `<input>` tag and used Angular's
{@link ng.filter:filter filter} function to process the input for the
{@link ng.directive:ngRepeat ngRepeat} directive.
This lets a user enter search criteria and immediately see the effects of their search on the phone
list. This new code demonstrates the following:
* Data-binding: This is one of the core features in Angular. When the page loads, Angular binds the
name of the input box to a variable of the same name in the data model and keeps the two in sync.
In this code, the data that a user types into the input box (named __`query`__) is immediately
available as a filter input in the list repeater (`phone in phones | filter:`__`query`__). When
changes to the data model cause the repeater's input to change, the repeater efficiently updates
the DOM to reflect the current state of the model.
<img class="diagram" src="img/tutorial/tutorial_03.png">
* Use of the `filter` filter: The {@link ng.filter:filter filter} function uses the
`query` value to create a new array that contains only those records that match the `query`.
`ngRepeat` automatically updates the view in response to the changing number of phones returned
by the `filter` filter. The process is completely transparent to the developer.
## Test
In Step 2, we learned how to write and run unit tests. Unit tests are perfect for testing
controllers and other components of our application written in JavaScript, but they can't easily
test DOM manipulation or the wiring of our application. For these, an end-to-end test is a much
better choice.
The search feature was fully implemented via templates and data-binding, so we'll write our first
end-to-end test, to verify that the feature works.
__`test/e2e/scenarios.js`:__
Let's see an example:
```js
describe('PhoneCat App', function() {
describe('Phone list view', function() {
beforeEach(function() {
browser.get('app/index.html');
angular.
module('myApp').
component('greetUser', {
template: 'Hello, {{$ctrl.user}}!',
controller: function GreetUserController() {
this.user = 'world';
}
});
```
Now, every time we include `<greet-user></greet-user>` in our view, Angular will expand it into a
DOM sub-tree constructed using the provided `template` and managed by an instance of the specified
controller.
But wait, where did that `$ctrl` come from and what does it refer to ?
For reasons already mentioned (and for other reasons that are out of the scope of this tutorial), it
is considered a good practice to avoid using the scope directly. We can (and should) use our
controller instance; i.e. assign our data and methods on properties of our controller (the "`this`"
inside the controller constructor), instead of directly to the scope.
From the template, we can refer to our controller instance using an alias. This way, the context of
evaluation for our expressions is even more clear. By default, components use `$ctrl` as the
controller alias, but we can override it, should the need arise.
There are more options available, so make sure you check out the
{@link ng.$compileProvider#component API Reference}, before using `.component()` in your own
applications.
it('should filter the phone list as a user types into the search box', function() {
## Using Components
var phoneList = element.all(by.repeater('phone in phones'));
var query = element(by.model('query'));
Now that we know how to create components, let's refactor the HTML page to make use of our newly
acquired skill.
expect(phoneList.count()).toBe(3);
<br />
**`app/index.html`:**
query.sendKeys('nexus');
expect(phoneList.count()).toBe(1);
```html
<html ng-app="phonecatApp">
<head>
...
<script src="bower_components/angular/angular.js"></script>
<script src="app.js"></script>
<script src="phone-list.component.js"></script>
</head>
<body>
query.clear();
query.sendKeys('motorola');
expect(phoneList.count()).toBe(2);
});
<!-- Use a custom component to render a list of phones -->
<phone-list></phone-list>
</body>
</html>
```
<br />
**`app/app.js`:**
```js
// Define the `phonecatApp` module
angular.module('phonecatApp', []);
```
<br />
**`app/phone-list.component.js`:**
```js
// Register `phoneList` component, along with its associated controller and template
angular.
module('phonecatApp').
component('phoneList', {
template:
'<ul>' +
'<li ng-repeat="phone in $ctrl.phones">' +
'<span>{{phone.name}}</span>' +
'<p>{{phone.snippet}}</p>' +
'</li>' +
'</ul>',
controller: function PhoneListController() {
this.phones = [
{
name: 'Nexus S',
snippet: 'Fast just got faster with Nexus S.'
}, {
name: 'Motorola XOOM™ with Wi-Fi',
snippet: 'The Next, Next Generation tablet.'
}, {
name: 'MOTOROLA XOOM™',
snippet: 'The Next, Next Generation tablet.'
}
];
}
});
```
Voilà! The resulting output should look the same, but let's see what we have gained:
* Our phone list is reusable. Just drop `<phone-list></phone-list>` anywhere in the page to get a
list of phones.
* Our main view (`index.html`) is cleaner and more declarative. Just by looking at it, we know there
is a list of phones. We are not bothered with implementation details.
* Our component is isolated and safe from "external influences". Likewise, we don't have to worry,
that we might accidentally break something in some other part of the application. What happens
inside our component, stays inside our component.
* It's easier to test our component in isolation.
<img class="diagram" src="img/tutorial/tutorial_03.png">
<div class="alert alert-info">
<p>**A note on file naming:**</p>
<p>
It is a good practice to distinguish different types of entities by suffix. In this tutorial, we
are using the `.component` suffix for components, so the definition of a `someComponent`
component would be in a file named `some-component.component.js`.
</p>
</div>
# Testing
Although we have combined our controller with a template into a component, we still can (and should)
unit test the controller separately, since this is where are application logic and data reside.
In order to retrieve and instantiate a component's controller, Angular provides the
{@link ngMock.$componentController $componentController} service.
<div class="alert alert-info">
The `$controller` service that we used in the previous step, can only instantiate controllers that
where registered by name, using the `.controller()` method. We could have registered our component
controller this way too, if we wanted to. Instead, we chose to define it inline &mdash; inside the
CDO &mdash; to keep things localized, but either way works equally well.
</div>
<br />
**`app/phone-list.component.spec.js`:**
```js
describe('phoneList', function() {
// Load the module that contains the `phoneList` component before each test
beforeEach(module('phonecatApp'));
// Test the controller
describe('PhoneListController', function() {
it('should create a `phones` model with 3 phones', inject(function($componentController) {
var ctrl = $componentController('phoneList');
expect(ctrl.phones.length).toBe(3);
}));
});
});
```
This test verifies that the search box and the repeater are correctly wired together. Notice how
easy it is to write end-to-end tests in Angular. Although this example is for a simple test, it
really is that easy to set up any functional, readable, end-to-end test.
The test retrieves the controller associated with the `phoneList` component, instantiates it and
verifies that the phones array property on it contains three records. Note that the data is now on
the controller instance itself, not on a `scope`.
### Running End to End Tests with Protractor
Even though the syntax of this test looks very much like our controller unit test written with
Jasmine, the end-to-end test uses APIs of [Protractor](https://github.com/angular/protractor). Read
about the Protractor APIs at http://angular.github.io/protractor/#/api.
Much like Karma is the test runner for unit tests, we use Protractor to run end-to-end tests.
Try it with `npm run protractor`. End-to-end tests are slow, so unlike with unit tests, Protractor
will exit after the test run and will not automatically rerun the test suite on every file change.
To rerun the test suite, execute `npm run protractor` again.
## Running Tests
<div class="alert alert-info">
Note: You must ensure your application is being served via a web-server to test with protractor.
You can do this using `npm start`.
You also need to ensure you've installed the protractor and updated webdriver prior to running the
`npm run protractor`. You can do this by issuing `npm install` and `npm run update-webdriver` into
your terminal.
</div>
Same as before, execute `npm test` to run the tests and then watch the files for changes.
# Experiments
### Display Current Query
Display the current value of the `query` model by adding a `{{query}}` binding into the
`index.html` template, and see how it changes when you type in the input box.
<div></div>
### Display Query in Title
Let's see how we can get the current value of the `query` model to appear in the HTML page title.
* Try the experiments from the previous step, this time on the `phoneList` component.
* Add an end-to-end test into the `describe` block, `test/e2e/scenarios.js` should look like this:
* Add a couple more phone lists on the page, by just adding more `<phone-list></phone-list>`
elements in `index.html`. Now add another binding to the `phoneList` component's template:
```js
describe('PhoneCat App', function() {
describe('Phone list view', function() {
beforeEach(function() {
browser.get('app/index.html');
});
var phoneList = element.all(by.repeater('phone in phones'));
var query = element(by.model('query'));
it('should filter the phone list as a user types into the search box', function() {
expect(phoneList.count()).toBe(3);
query.sendKeys('nexus');
expect(phoneList.count()).toBe(1);
query.clear();
query.sendKeys('motorola');
expect(phoneList.count()).toBe(2);
});
it('should display the current filter value in the title bar', function() {
query.clear();
expect(browser.getTitle()).toMatch(/Google Phone Gallery:\s*$/);
query.sendKeys('nexus');
expect(browser.getTitle()).toMatch(/Google Phone Gallery: nexus$/);
});
});
});
template:
'<p>Total number of phones: {{$ctrl.phones.length}}</p>' +
'<ul>' +
...
```
Run protractor (`npm run protractor`) to see this test fail.
* You might think you could just add the `{{query}}` to the title tag element as follows:
<title>Google Phone Gallery: {{query}}</title>
However, when you reload the page, you won't see the expected result. This is because the "query"
model lives in the scope, defined by the `ng-controller="PhoneListCtrl"` directive, on the body
element:
<body ng-controller="PhoneListCtrl">
If you want to bind to the query model from the `<title>` element, you must __move__ the
`ngController` declaration to the HTML element because it is the common parent of both the body
and title elements:
<html ng-app="phonecatApp" ng-controller="PhoneListCtrl">
Be sure to __remove__ the `ng-controller` declaration from the body element.
* Re-run `npm run protractor` to see the test now pass.
* While using double curlies works fine within the title element, you might have noticed that
for a split second they are actually displayed to the user while the page is loading. A better
solution would be to use the {@link ng.directive:ngBind ngBind} or
{@link ng.directive:ngBindTemplate ngBindTemplate} directives, which are invisible to the user
while the page is loading:
<title ng-bind-template="Google Phone Gallery: {{query}}">Google Phone Gallery</title>
Reload the page and watch the new "feature" propagate to all phone lists. In real-world
applications, where the phone lists could appear on several different pages, being able to change
or add something in one place (e.g. a component's template) and have that change propagate
throughout the application, is a big win.
# Summary
We have now added full text search and included a test to verify that search works! Now let's go on
to {@link step_04 step 4} to learn how to add sorting capability to the phone app.
You have learned how to organize your application and presentation logic into isolated reusable
components. Let's go to {@link step_04 step 4} to learn how to organize our code in directories and
files, so it remains easy to locate as our application grows.
<ul doc-tutorial-nav="3"></ul>
[jasmine-docs]: http://jasmine.github.io/2.4/introduction.html
[jasmine-home]: http://jasmine.github.io/
[karma]: https://karma-runner.github.io/
[mvc-pattern]: http://en.wikipedia.org/wiki/ModelViewController
+261 -145
View File
@@ -1,199 +1,315 @@
@ngdoc tutorial
@name 4 - Two-way Data Binding
@name 4 - Directory and File Organization
@step 4
@description
<ul doc-tutorial-nav="4"></ul>
In this step, you will add a feature to let your users control the order of the items in the phone
list. The dynamic ordering is implemented by creating a new model property, wiring it together with
the repeater, and letting the data binding magic do the rest of the work.
In this step, we will not be adding any new functionality to our application. Instead, we are going
to take a step back, refactor our codebase and move files and code around, in order to make our
application more easily expandable and maintainable.
* In addition to the search box, the app displays a drop down menu that allows users to control the
order in which the phones are listed.
In the previous step, we saw how to architect our application to be modular and testable. What's
equally important though, is organizing our codebase in a way that makes it easy (both for us and
other developers on our team) to navigate through the code and quickly locate the pieces that are
relevant to a specific feature or section of the application.
To that end, we will explain why and how we:
* Put each entity in its **own file**.
* Organize our code by **feature area**, instead of by function.
* Split our code into **modules** that other modules can depend on.
<div class="alert alert-info">
We will keep it short, not going into great detail on every good practice and convention. These
principles are explained in great detail in the [Angular Style Guide][styleguide], which also
contains many more techniques for effectively organizing Angular codebases.
</div>
<div doc-tutorial-reset="4"></div>
## Template
## One Feature per File
__`app/index.html`:__
It might be tempting, for the sake of simplicity, to put everything in one file, or have one file
per type; e.g. all controllers is one file, all components in another file, all services in a third
file, and so on.
This might seem to work well in the beginning, but as our application grows it becomes a burden to
maintain. As we add more and more features, our files will get bigger and bigger and it will be
difficult to navigate and find the code we are looking for.
```html
Search: <input ng-model="query">
Sort by:
<select ng-model="orderProp">
<option value="name">Alphabetical</option>
<option value="age">Newest</option>
</select>
Instead we should put each feature/entity in its own file. Each stand-alone controller will be
defined in its own file, each component will be defined in each own file, etc.
Luckily, we don't need to change anything with respect to that guideline in our code, since we have
already defined our `phoneList` component in its own `phone-list.component.js` file. Good job!
We will keep this in mind though, as we add more features.
<ul class="phones">
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp">
<span>{{phone.name}}</span>
<p>{{phone.snippet}}</p>
</li>
</ul>
## Organizing by Feature
So, now that we learned we should put everything in its own file, our `app/` directory will soon be
full with dozens of files and specs (remember we keep our unit test files next to the corresponding
source code files). What's more important, logically related files will not be grouped together; it
will be really difficult of locate all files related to a specific section of the application and
make a change or fix a bug.
So, what shall we do?
Well, we are going to group our files into directories _by feature_. For example, since we have a
section in our application that lists phones, we will put all related files into a `phone-list/`
directory under `app/`. We are soon to find out that certain features are used across different
parts of the application. We will put those inside `app/core/`.
<div class="alert alert-info">
<p>
Other typical names for our `core` directory are `shared`, `common` and `components`. The last
one is kind of misleading though, as it will contain other things than components as well.
</p>
<p>
(This is mostly a relic of the past, when "components" just meant the generic building blocks of
an application.)
</p>
</div>
Based on what we have discussed so far, here is our directory/file layout for the `phoneList`
"feature":
```
app/
phone-list/
phone-list.component.js
phone-list.component.spec.js
app.js
```
We made the following changes to the `index.html` template:
* First, we added a `<select>` html element named `orderProp`, so that our users can pick from the
two provided sorting options.
## Using Modules
<img class="diagram" src="img/tutorial/tutorial_04.png">
As previously mentioned, one of the benefits of having a modular architecture is code reuse &mdash;
not only inside the same application, but across applications too. There is one final step in making
this code reuse frictionless:
* We then chained the `filter` filter with {@link ng.filter:orderBy `orderBy`}
filter to further process the input into the repeater. `orderBy` is a filter that takes an input
array, copies it and reorders the copy which is then returned.
* Each feature/section should declare its own module and all related entities should register
themselves on that module.
Angular creates a two way data-binding between the select element and the `orderProp` model.
`orderProp` is then used as the input for the `orderBy` filter.
As we discussed in the section about data-binding and the repeater in step 3, whenever the model
changes (for example because a user changes the order with the select drop down menu), Angular's
data-binding will cause the view to automatically update. No bloated DOM manipulation code is
necessary!
## Controller
__`app/js/controllers.js`:__
Let's take the `phoneList` feature as an example. Previously, the `phoneList` component would
register itself on the `phonecatApp` module:
```js
var phonecatApp = angular.module('phonecatApp', []);
phonecatApp.controller('PhoneListCtrl', function ($scope) {
$scope.phones = [
{'name': 'Nexus S',
'snippet': 'Fast just got faster with Nexus S.',
'age': 1},
{'name': 'Motorola XOOM™ with Wi-Fi',
'snippet': 'The Next, Next Generation tablet.',
'age': 2},
{'name': 'MOTOROLA XOOM™',
'snippet': 'The Next, Next Generation tablet.',
'age': 3}
];
$scope.orderProp = 'age';
});
angular.
module('phonecatApp').
component('phoneList', ...);
```
* We modified the `phones` model - the array of phones - and added an `age` property to each phone
record. This property is used to order phones by age.
Similarly, the accompanying spec file loads the `phonecatApp` module before each test (because
that's where our component is registered). Now, imagine that we need a list of phones on another
project that we are working on. Thanks to our modular architecture, we don't have to reinvent the
wheel; we simply copy the `phone-list/` directory on our other project and add the necessary script
tags in our `index.html` file and we are done, right ?
* We added a line to the controller that sets the default value of `orderProp` to `age`. If we had
not set a default value here, the `orderBy` filter would remain uninitialized until our
user picked an option from the drop down menu.
Well, not so fast. The new project doesn't know anything about a `phonecatApp` module. So, we would
have to replace all references to `phonecatApp` with the name of this project's main module. As you
can imagine this is both laborious and error-prone.
This is a good time to talk about two-way data-binding. Notice that when the app is loaded in the
browser, "Newest" is selected in the drop down menu. This is because we set `orderProp` to `'age'`
in the controller. So the binding works in the direction from our model to the UI. Now if you
select "Alphabetically" in the drop down menu, the model will be updated as well and the phones
will be reordered. That is the data-binding doing its job in the opposite direction — from the UI
to the model.
Yeah, you guessed it: There is a better way !
Each feature/section, will declare its own module and have all related entities registered there.
The main module (`phonecatApp`) will declare a dependency on each feature/section module. Now,
all it takes to reuse the same code on new project is copying the feature directory over and adding
the feature module as a dependency in the new project's main module.
Here is what our `phoneList` feature will look like after this change:
## Test
<br />
**`/`:**
The changes we made should be verified with both a unit test and an end-to-end test. Let's look at
the unit test first.
```
app/
phone-list/
phone-list.module.js
phone-list.component.js
phone-list.component.spec.js
app.module.js
```
__`test/unit/controllersSpec.js`:__
<br />
**`app/phone-list/phone-list.module.js`:**
```js
describe('PhoneCat controllers', function() {
// Define the `phoneList` module
angular.module('phoneList', []);
```
describe('PhoneListCtrl', function(){
var scope, ctrl;
<br />
**`app/phone-list/phone-list.component.js`:**
beforeEach(module('phonecatApp'));
```js
// Register the `phoneList` component on the `phoneList` module,
angular.
module('phoneList').
component('phoneList', {...});
```
beforeEach(inject(function($controller) {
scope = {};
ctrl = $controller('PhoneListCtrl', {$scope:scope});
}));
<br />
**`app/app.module.js`:**<br />
_(since `app/app.js` now only contains the main module declaration, we gave it a `.module` suffix)_
it('should create "phones" model with 3 phones', function() {
expect(scope.phones.length).toBe(3);
});
```js
// Define the `phonecatApp` module
angular.module('phonecatApp', [
// ...which depends on the `phoneList` module
'phoneList'
]);
```
By passing `phoneList` inside the dependencies array when defining the `phonecatApp` module, Angular
will make all entities registered on `phoneList` available on `phonecatApp` as well.
<div class="alert alert-info">
<p>
Don't forget to also update your `index.html` adding a `<script>` tag for each JacaScript file
we have created. This might seem tedious, but is totally worth it.
</p>
<p>
In a production-ready application, you would concatenate and minify all your JavaScript files
anyway (for performance reasons), so this won't be an issue any more.
</p>
</div>
<div class="alert alert-warning">
Note that files defining a module (i.e. `.module.js`) need to be included before other files that
add features (e.g. components, controllers, services, filters) to that module.
</div>
it('should set the default value of orderProp model', function() {
expect(scope.orderProp).toBe('age');
});
## External Templates
Since we are at refactoring, let's do one more thing. As we learned, components have templates,
which are basically fragments of HTML code that dictate how our data is laid out and presented to
the user. In {@link step_03 step 3}, we saw how we can specify the template for a component as a
string using the `template` property of the CDO (Component Definition Object).
Having HTML code in a string isn't ideal, especially for bigger templates. It would be much better,
if we could have our HTML code in `.html` files. This way, we would get all the support our
IDE/editor has to offer (e.g. HTML-specific color-highlighting and auto-completion) and also keep
our component definitions cleaner.
So, while it's perfectly fine to keep our component templates inline (using the `template` property
of the CDO), we are going to use an external template for our `phoneList` component. In order to
denote that we are using an external template, we use the `templateUrl` property and specify the URL
that our template will be loaded from. Since we want to keep our template close to where the
component is defined, we place it inside `app/phone-list/`.
We copied the contents of the `template` property (the HTML code) into
`app/phone-list/phone-list.template.html` and modified our CDO like this:
<br />
**`app/phone-list/phone-list.component.js`:**
```js
angular.
module('phoneList').
component('phoneList', {
// Note: The URL is relative to our `index.html` file
templateUrl: 'phone-list/phone-list.template.html',
controller: ...
});
```
At runtime, when Angular needs to create an instance of the `phoneList` component, it will make an
HTTP request to get the template from `app/phone-list/phone-list.template.html`.
<div class="alert alert-info">
Keeping inline with our convention, we will be using the `.template` suffix for external
templates. Another common convention is to just have the `.html` extension
(e.g. `phone-list.html`).
</div>
<div class="alert alert-warning">
<p>
Using an external template like this, will result in more HTTP requests to the server (one for
each external template). Although Angular takes care not to make extraneous requests (e.g.
fetching the templates lazily, caching the results, etc), additional requests do have a cost
(especially on mobile devices and data-plan connections).
</p>
<p>
Luckily, there are ways to avoid the extra costs (while still keeping your templates external).
A detailed discussion of the subject is outside the scope of this tutorial, but you can take a
look at the {@link ng.$templateRequest $templateRequest} and
{@link ng.$templateCache $templateCache} services for more info on how Angular manages external
templates.
</p>
</div>
## Final Directory/File Layout
After all the refactorings that took place, this is how our application looks from the outside:
<br />
**`/`:**
```
app/
phone-list/
phone-list.component.js
phone-list.component.spec.js
phone-list.module.js
phone-list.template.html
app.css
app.module.js
index.html
```
# Testing
Since this was just a refactoring step (no actual code addition/deletions), we shouldn't need to
change much (if anything) as far as our specs are concerned.
One thing that we can (and should) change is the name of the module to be loaded before each test in
`app/phone-list/phone-list.component.spec.js`. We don't need to pull in the whole `phonecatApp`
module (which will soon grow to depend on more stuff). All we want to test is already included in
the much smaller `phoneList` module, so it suffices to just load that.
This is one extra benefit that we get out of our modular architecture for free.
<br />
**`app/phone-list/phone-list.component.spec.js`:**
```js
describe('phoneList', function() {
// Load the module that contains the `phoneList` component before each test
beforeEach(module('phoneList'));
...
});
```
If not already done so, run the tests (using the `npm test` command) and verify that they still
pass.
The unit test now verifies that the default ordering property is set.
<div class="alert alert-success">
One of the great things about tests is the confidence they provide, when refactoring your
application. It's easy to break something as you start moving files around and re-arranging
modules. Having good test coverage is the quickest, easiest and most reliable way of knowing that
your application will continue to work as expected.
</div>
We used Jasmine's API to extract the controller construction into a `beforeEach` block, which is
shared by all tests in the parent `describe` block.
You should now see the following output in the Karma tab:
<pre>Chrome 22.0: Executed 2 of 2 SUCCESS (0.021 secs / 0.001 secs)</pre>
Let's turn our attention to the end-to-end test.
__`test/e2e/scenarios.js`:__
```js
...
it('should be possible to control phone order via the drop down select box', function() {
var phoneNameColumn = element.all(by.repeater('phone in phones').column('phone.name'));
var query = element(by.model('query'));
function getNames() {
return phoneNameColumn.map(function(elm) {
return elm.getText();
});
}
query.sendKeys('tablet'); //let's narrow the dataset to make the test assertions shorter
expect(getNames()).toEqual([
"Motorola XOOM\u2122 with Wi-Fi",
"MOTOROLA XOOM\u2122"
]);
element(by.model('orderProp')).element(by.css('option[value="name"]')).click();
expect(getNames()).toEqual([
"MOTOROLA XOOM\u2122",
"Motorola XOOM\u2122 with Wi-Fi"
]);
});...
```
The end-to-end test verifies that the ordering mechanism of the select box is working correctly.
You can now rerun `npm run protractor` to see the tests run.
# Experiments
* In the `PhoneListCtrl` controller, remove the statement that sets the `orderProp` value and
you'll see that Angular will temporarily add a new blank ("unknown") option to the drop-down list and the
ordering will default to unordered/natural order.
* Add an `{{orderProp}}` binding into the `index.html` template to display its current value as
text.
* Reverse the sort order by adding a `-` symbol before the sorting value: `<option value="-age">Oldest</option>`
# Summary
Now that you have added list sorting and tested the app, go to {@link step_05 step 5} to learn
about Angular services and how Angular uses dependency injection.
Even if we didn't add any new and exciting functionality to our application, we have made a great
step towards a well-architected and maintainable application. Time to spice things up. Let's go to
{@link step_05 step 5} to learn how to add full-text search to the application.
<ul doc-tutorial-nav="4"></ul>
[angular-seed]: https://github.com/angular/angular-seed
[styleguide]: https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md
+122 -231
View File
@@ -1,277 +1,168 @@
@ngdoc tutorial
@name 5 - XHRs & Dependency Injection
@name 5 - Filtering Repeaters
@step 5
@description
<ul doc-tutorial-nav="5"></ul>
Enough of building an app with three phones in a hard-coded dataset! Let's fetch a larger dataset
from our server using one of Angular's built-in {@link guide/services services} called {@link
ng.$http $http}. We will use Angular's {@link guide/di dependency
injection (DI)} to provide the service to the `PhoneListCtrl` controller.
We did a lot of work in laying a foundation for the app in the previous steps, so now we'll do
something simple; we will add full-text search (yes, it will be simple!). We will also write an
end-to-end (E2E) test, because a good E2E test is a good friend. It stays with your app, keeps an
eye on it, and quickly detects regressions.
* The app now has a search box. Notice that the phone list on the page changes depending on what a
user types into the search box.
* There is now a list of 20 phones, loaded from the server.
<div doc-tutorial-reset="5"></div>
## Data
The `app/phones/phones.json` file in your project is a dataset that contains a larger list of phones
stored in the JSON format.
## Component Controller
Following is a sample of the file:
We made no changes to the component's controller.
```js
[
{
"age": 13,
"id": "motorola-defy-with-motoblur",
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
"snippet": "Are you ready for everything life throws your way?"
...
},
...
]
## Component Template
<br />
**`app/phone-list/phone-list.template.html`:**
```html
<div class="container-fluid">
<div class="row">
<div class="col-md-2">
<!--Sidebar content-->
Search: <input ng-model="$ctrl.query" />
</div>
<div class="col-md-10">
<!--Body content-->
<ul class="phones">
<li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query">
<span>{{phone.name}}</span>
<p>{{phone.snippet}}</p>
</li>
</ul>
</div>
</div>
</div>
```
We added a standard HTML `<input>` tag and used Angular's {@link ng.filter:filter filter} function
to process the input for the {@link ngRepeat ngRepeat} directive.
## Controller
By virtue of the {@link ngModel ngModel} directive, this lets a user enter search criteria and
immediately see the effects of their search on the phone list. This new code demonstrates the
following:
We'll use Angular's {@link ng.$http $http} service in our controller to make an HTTP
request to your web server to fetch the data in the `app/phones/phones.json` file. `$http` is just
one of several built-in {@link guide/services Angular services} that handle common operations
in web apps. Angular injects these services for you where you need them.
* Data-binding: This is one of the core features in Angular. When the page loads, Angular binds the
value of the input box to the data model variable specified with `ngModel` and keeps the two in
sync.
Services are managed by Angular's {@link guide/di DI subsystem}. Dependency injection
helps to make your web apps both well-structured (e.g., separate components for presentation, data,
and control) and loosely coupled (dependencies between components are not resolved by the
components themselves, but by the DI subsystem).
In this code, the data that a user types into the input box (bound to **`$ctrl.query`**) is
immediately available as a filter input in the list repeater
(`phone in $ctrl.phones | filter:`**`$ctrl.query`**). When changes to the data model cause the
repeater's input to change, the repeater efficiently updates the DOM to reflect the current state
of the model.
__`app/js/controllers.js:`__
<img class="diagram" src="img/tutorial/tutorial_05.png">
* Use of the `filter` filter: The {@link ng.filter:filter filter} function uses the `$ctrl.query`
value to create a new array that contains only those records that match the query.
`ngRepeat` automatically updates the view in response to the changing number of phones returned
by the `filter` filter. The process is completely transparent to the developer.
# Testing
In previous steps, we learned how to write and run unit tests. Unit tests are perfect for testing
controllers and other parts of our application written in JavaScript, but they can't easily
test templates, DOM manipulation or interoperability of components and services. For these, an
end-to-end (E2E) test is a much better choice.
The search feature was fully implemented via templates and data-binding, so we'll write our first
E2E test, to verify that the feature works.
<br />
**`e2e-tests/scenarios.js`:**
```js
var phonecatApp = angular.module('phonecatApp', []);
describe('PhoneCat Application', function() {
describe('phoneList', function() {
beforeEach(function() {
browser.get('index.html');
});
it('should filter the phone list as a user types into the search box', function() {
var phoneList = element.all(by.repeater('phone in $ctrl.phones'));
var query = element(by.model('$ctrl.query'));
expect(phoneList.count()).toBe(3);
query.sendKeys('nexus');
expect(phoneList.count()).toBe(1);
query.clear();
query.sendKeys('motorola');
expect(phoneList.count()).toBe(2);
});
phonecatApp.controller('PhoneListCtrl', function ($scope, $http) {
$http.get('phones/phones.json').success(function(data) {
$scope.phones = data;
});
$scope.orderProp = 'age';
});
```
`$http` makes an HTTP GET request to our web server, asking for `phones/phones.json` (the url is
relative to our `index.html` file). The server responds by providing the data in the json file.
(The response might just as well have been dynamically generated by a backend server. To the
browser and our app, they both look the same. For the sake of simplicity, we used a json file in this
tutorial.)
The `$http` service returns a {@link ng.$q promise object} with a `success`
method. We call this method to handle the asynchronous response and assign the phone data to the
scope controlled by this controller, as a model called `phones`. Notice that Angular detected the
json response and parsed it for us!
To use a service in Angular, you simply declare the names of the dependencies you need as arguments
to the controller's constructor function, as follows:
phonecatApp.controller('PhoneListCtrl', function ($scope, $http) {...}
Angular's dependency injector provides services to your controller when the controller is being
constructed. The dependency injector also takes care of creating any transitive dependencies the
service may have (services often depend upon other services).
Note that the names of arguments are significant, because the injector uses these to look up the
dependencies.
This test verifies that the search box and the repeater are correctly wired together. Notice how
easy it is to write E2E tests in Angular. Although this example is for a simple test, it really is
that easy to set up any functional, readable, E2E test.
<img class="diagram" src="img/tutorial/tutorial_05.png">
## Running E2E Tests with Protractor
Even though the syntax of this test looks very much like our controller unit test written with
Jasmine, the E2E test uses APIs of [Protractor][protractor]. Read about the Protractor APIs in the
[Protractor API Docs][protractor-docs].
### `$` Prefix Naming Convention
You can create your own services, and in fact we will do exactly that in step 11. As a naming
convention, Angular's built-in services, Scope methods and a few other Angular APIs have a `$`
prefix in front of the name.
The `$` prefix is there to namespace Angular-provided services.
To prevent collisions it's best to avoid naming your services and models anything that begins with a `$`.
If you inspect a Scope, you may also notice some properties that begin with `$$`. These
properties are considered private, and should not be accessed or modified.
### A Note on Minification
Since Angular infers the controller's dependencies from the names of arguments to the controller's
constructor function, if you were to [minify](http://goo.gl/SAnnsm) the JavaScript code for
`PhoneListCtrl` controller, all of its function arguments would be minified as well, and the
dependency injector would not be able to identify services correctly.
We can overcome this problem by annotating the function with the names of the dependencies, provided
as strings, which will not get minified. There are two ways to provide these injection annotations:
* Create a `$inject` property on the controller function which holds an array of strings.
Each string in the array is the name of the service to inject for the corresponding parameter.
In our example, we would write:
```js
function PhoneListCtrl($scope, $http) {...}
PhoneListCtrl.$inject = ['$scope', '$http'];
phonecatApp.controller('PhoneListCtrl', PhoneListCtrl);
```
* Use an inline annotation where, instead of just providing the function, you provide an array.
This array contains a list of the service names, followed by the function itself.
```js
function PhoneListCtrl($scope, $http) {...}
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', PhoneListCtrl]);
```
Both of these methods work with any function that can be injected by Angular, so it's up to your
project's style guide to decide which one you use.
When using the second method, it is common to provide the constructor function inline as an
anonymous function when registering the controller:
```js
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', function($scope, $http) {...}]);
```
From this point onward, we're going to use the inline method in the tutorial. With that in mind,
let's add the annotations to our `PhoneListCtrl`:
__`app/js/controllers.js:`__
```js
var phonecatApp = angular.module('phonecatApp', []);
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http',
function ($scope, $http) {
$http.get('phones/phones.json').success(function(data) {
$scope.phones = data;
});
$scope.orderProp = 'age';
}]);
```
## Test
__`test/unit/controllersSpec.js`:__
Because we started using dependency injection and our controller has dependencies, constructing the
controller in our tests is a bit more complicated. We could use the `new` operator and provide the
constructor with some kind of fake `$http` implementation. However, Angular provides a mock `$http`
service that we can use in unit tests. We configure "fake" responses to server requests by calling
methods on a service called `$httpBackend`:
```js
describe('PhoneCat controllers', function() {
describe('PhoneListCtrl', function(){
var scope, ctrl, $httpBackend;
// Load our app module definition before each test.
beforeEach(module('phonecatApp'));
// The injector ignores leading and trailing underscores here (i.e. _$httpBackend_).
// This allows us to inject a service but then attach it to a variable
// with the same name as the service in order to avoid a name conflict.
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
$httpBackend.expectGET('phones/phones.json').
respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
scope = $rootScope.$new();
ctrl = $controller('PhoneListCtrl', {$scope: scope});
}));
```
Note: Because we loaded Jasmine and `angular-mocks.js` in our test environment, we got two helper
methods {@link angular.mock.module module} and {@link angular.mock.inject inject} that we'll
use to access and configure the injector.
We created the controller in the test environment, as follows:
* We used the `inject` helper method to inject instances of
{@link ng.$rootScope $rootScope},
{@link ng.$controller $controller} and
{@link ng.$httpBackend $httpBackend} services into the Jasmine's `beforeEach`
function. These instances come from an injector which is recreated from scratch for every single
test. This guarantees that each test starts from a well known starting point and each test is
isolated from the work done in other tests.
* We created a new scope for our controller by calling `$rootScope.$new()`
* We called the injected `$controller` function passing the name of the `PhoneListCtrl` controller
and the created scope as parameters.
Because our code now uses the `$http` service to fetch the phone list data in our controller, before
we create the `PhoneListCtrl` child scope, we need to tell the testing harness to expect an
incoming request from the controller. To do this we:
* Request `$httpBackend` service to be injected into our `beforeEach` function. This is a mock
version of the service that in a production environment facilitates all XHR and JSONP requests.
The mock version of this service allows you to write tests without having to deal with
native APIs and the global state associated with them — both of which make testing a nightmare.
* Use the `$httpBackend.expectGET` method to train the `$httpBackend` service to expect an incoming
HTTP request and tell it what to respond with. Note that the responses are not returned until we call
the `$httpBackend.flush` method.
Now we will make assertions to verify that the `phones` model doesn't exist on `scope` before
the response is received:
```js
it('should create "phones" model with 2 phones fetched from xhr', function() {
expect(scope.phones).toBeUndefined();
$httpBackend.flush();
expect(scope.phones).toEqual([{name: 'Nexus S'},
{name: 'Motorola DROID'}]);
});
```
* We flush the request queue in the browser by calling `$httpBackend.flush()`. This causes the
promise returned by the `$http` service to be resolved with the trained response. See
'Flushing HTTP requests' in the {@link ngMock.$httpBackend mock $httpBackend} documentation for
a full explanation of why this is necessary.
* We make the assertions, verifying that the phone model now exists on the scope.
Finally, we verify that the default value of `orderProp` is set correctly:
```js
it('should set the default value of orderProp model', function() {
expect(scope.orderProp).toBe('age');
});
```
You should now see the following output in the Karma tab:
<pre>Chrome 22.0: Executed 2 of 2 SUCCESS (0.028 secs / 0.007 secs)</pre>
Much like Karma is the test runner for unit tests, we use Protractor to run E2E tests. Try it with
`npm run protractor`. E2E tests take time, so unlike with unit tests, Protractor will exit after the
tests run and will not automatically rerun the test suite on every file change.
To rerun the test suite, execute `npm run protractor` again.
<div class="alert alert-info">
**Note:** In order for protractor to access and run tests against your application, it must be
served via a web server. In a different terminal/command line window, run `npm start` to fire up
the web server.
</div>
# Experiments
* At the bottom of `index.html`, add a `<pre>{{phones | filter:query | orderBy:orderProp | json}}</pre>`
binding to see the list of phones displayed in json format.
<div></div>
* In the `PhoneListCtrl` controller, pre-process the http response by limiting the number of phones
to the first 5 in the list. Use the following code in the `$http` callback:
* Display the current value of the `query` model by adding a `{{$ctrl.query}}` binding into the
`phone-list.template.html` template and see how it changes, when you type in the input box.
$scope.phones = data.splice(0, 5);
You might also try to add the `{{$ctrl.query}}` binding to `index.html`. However, when you reload
the page, you won't see the expected result. This is because the `query` model lives in the scope
defined by the `<phone-list>` component.<br />
Component isolation at work!
# Summary
Now that you have learned how easy it is to use Angular services (thanks to Angular's dependency
injection), go to {@link step_06 step 6}, where you will add some
thumbnail images of phones and some links.
We have now added full-text search and included a test to verify that it works! Now let's go on to
{@link step_06 step 6} to learn how to add sorting capabilities to the PhoneCat application.
<ul doc-tutorial-nav="5"></ul>
[protractor]: https://github.com/angular/protractor
[protractor-docs]: https://angular.github.io/protractor/#/api
+197 -64
View File
@@ -1,108 +1,241 @@
@ngdoc tutorial
@name 6 - Templating Links & Images
@name 6 - Two-way Data Binding
@step 6
@description
<ul doc-tutorial-nav="6"></ul>
In this step, you will add thumbnail images for the phones in the phone list, and links that, for
now, will go nowhere. In subsequent steps you will use the links to display additional information
about the phones in the catalog.
In this step, we will add a feature to let our users control the order of the items in the phone
list. The dynamic ordering is implemented by creating a new model property, wiring it together with
the repeater, and letting the data binding magic do the rest of the work.
* In addition to the search box, the application displays a drop-down menu that allows users to
control the order in which the phones are listed.
* There are now links and images of the phones in the list.
<div doc-tutorial-reset="6"></div>
## Data
Note that the `phones.json` file contains unique IDs and image URLs for each of the phones. The
URLs point to the `app/img/phones/` directory.
## Component Template
__`app/phones/phones.json`__ (sample snippet):
```js
[
{
...
"id": "motorola-defy-with-motoblur",
"imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg",
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
...
},
...
]
```
## Template
__`app/index.html`:__
<br />
**`app/phone-list/phone-list.template.html`:**
```html
...
<div class="container-fluid">
<div class="row">
<div class="col-md-2">
<!--Sidebar content-->
<p>
Search:
<input ng-model="$ctrl.query">
</p>
<p>
Sort by:
<select ng-model="$ctrl.orderProp">
<option value="name">Alphabetical</option>
<option value="age">Newest</option>
</select>
</p>
</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}}" alt="{{phone.name}}"></a>
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
<li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp">
<span>{{phone.name}}</span>
<p>{{phone.snippet}}</p>
</li>
</ul>
...
</div>
</div>
</div>
```
To dynamically generate links that will in the future lead to phone detail pages, we used the
now-familiar double-curly brace binding in the `href` attribute values. In step 2, we added the
`{{phone.name}}` binding as the element content. In this step the `{{phone.id}}` binding is used in
the element attribute.
We made the following changes to the `phone-list.template.html` template:
We also added phone images next to each record using an image tag with the {@link
ng.directive:ngSrc ngSrc} directive. That directive prevents the
browser from treating the Angular `{{ expression }}` markup literally, and initiating a request to
an invalid URL `http://localhost:8000/app/{{phone.imageUrl}}`, which it would have done if we had
only specified an attribute binding in a regular `src` attribute (`<img src="{{phone.imageUrl}}">`).
Using the `ngSrc` directive prevents the browser from making an http request to an invalid location.
* First, we added a `<select>` element bound to `$ctrl.orderProp`, so that our users can pick from
the two provided sorting options.
<img class="diagram" src="img/tutorial/tutorial_06.png">
* We then chained the `filter` filter with the {@link orderBy orderBy} filter to further process the
input for the repeater. `orderBy` is a filter that takes an input array, copies it and reorders
the copy which is then returned.
Angular creates a two way data-binding between the select element and the `$ctrl.orderProp` model.
`$ctrl.orderProp` is then used as the input for the `orderBy` filter.
As we discussed in the section about data-binding and the repeater in {@link step_05 step 5},
whenever the model changes (for example because a user changes the order with the select drop-down
menu), Angular's data-binding will cause the view to automatically update. No bloated DOM
manipulation code is necessary!
## Test
## Component Controller
__`test/e2e/scenarios.js`__:
<br />
**`app/phone-list/phone-list.components.js`:**
```js
...
it('should render phone specific links', function() {
var query = element(by.model('query'));
query.sendKeys('nexus');
element.all(by.css('.phones li a')).first().click();
browser.getLocationAbsUrl().then(function(url) {
expect(url).toBe('/phones/nexus-s');
});
angular.
module('phoneList').
component('phoneList', {
templateUrl: 'phone-list/phone-list.template.html',
controller: function PhoneListController() {
this.phones = [
{
name: 'Nexus S',
snippet: 'Fast just got faster with Nexus S.',
age: 1
}, {
name: 'Motorola XOOM™ with Wi-Fi',
snippet: 'The Next, Next Generation tablet.',
age: 2
}, {
name: 'MOTOROLA XOOM™',
snippet: 'The Next, Next Generation tablet.',
age: 3
}
];
this.orderProp = 'age';
}
});
...
```
We added a new end-to-end test to verify that the app is generating correct links to the phone
views that we will implement in the upcoming steps.
* We modified the `phones` model - the array of phones - and added an `age` property to each phone
record. This property is used to order the phones by age.
* We added a line to the controller that sets the default value of `orderProp` to `age`. If we had
not set a default value here, the `orderBy` filter would remain uninitialized until the user
picked an option from the drop-down menu.
This is a good time to talk about two-way data-binding. Notice that when the application is loaded
in the browser, "Newest" is selected in the drop-down menu. This is because we set `orderProp` to
`'age'` in the controller. So the binding works in the direction from our model to the UI. Now if
you select "Alphabetically" in the drop-down menu, the model will be updated as well and the phones
will be reordered. That is the data-binding doing its job in the opposite direction — from the UI to
the model.
# Testing
The changes we made should be verified with both a unit test and an E2E test. Let's look at the unit
test first.
<br />
**`app/phone-list/phone-list.component.spec.js`:**
```js
describe('phoneList', function() {
// Load the module that contains the `phoneList` component before each test
beforeEach(module('phoneList'));
// Test the controller
describe('PhoneListController', function() {
var ctrl;
beforeEach(inject(function($componentController) {
ctrl = $componentController('phoneList');
}));
it('should create a `phones` model with 3 phones', function() {
expect(ctrl.phones.length).toBe(3);
});
it('should set a default value for the `orderProp` model', function() {
expect(ctrl.orderProp).toBe('age');
});
});
});
```
The unit test now verifies that the default ordering property is set.
We used Jasmine's API to extract the controller construction into a `beforeEach` block, which is
shared by all tests in the parent `describe` block.
You should now see the following output in the Karma tab:
```
Chrome 49.0: Executed 2 of 2 SUCCESS (0.136 secs / 0.08 secs)
```
Let's turn our attention to the E2E tests.
<br />
**`e2e-tests/scenarios.js`:**
```js
describe('PhoneCat Application', function() {
describe('phoneList', function() {
...
it('should be possible to control phone order via the drop-down menu', function() {
var queryField = element(by.model('$ctrl.query'));
var orderSelect = element(by.model('$ctrl.orderProp'));
var nameOption = orderSelect.element(by.css('option[value="name"]'));
var phoneNameColumn = element.all(by.repeater('phone in $ctrl.phones').column('phone.name'));
function getNames() {
return phoneNameColumn.map(function(elem) {
return elem.getText();
});
}
queryField.sendKeys('tablet'); // Let's narrow the dataset to make the assertions shorter
expect(getNames()).toEqual([
'Motorola XOOM\u2122 with Wi-Fi',
'MOTOROLA XOOM\u2122'
]);
nameOption.click();
expect(getNames()).toEqual([
'MOTOROLA XOOM\u2122',
'Motorola XOOM\u2122 with Wi-Fi'
]);
});
...
```
The E2E test verifies that the ordering mechanism of the select box is working correctly.
You can now rerun `npm run protractor` to see the tests run.
# Experiments
* Replace the `ng-src` directive with a plain old `src` attribute. Using tools such as Firebug,
or Chrome's Web Inspector, or inspecting the webserver access logs, confirm that the app is indeed
making an extraneous request to `/app/%7B%7Bphone.imageUrl%7D%7D` (or
`/app/{{phone.imageUrl}}`).
<div></div>
The issue here is that the browser will fire a request for that invalid image address as soon as
it hits the `img` tag, which is before Angular has a chance to evaluate the expression and inject
the valid address.
* In the `phoneList` component's controller, remove the statement that sets the `orderProp` value
and you'll see that Angular will temporarily add a new blank ("unknown") option to the drop-down
list and the ordering will default to unordered/natural order.
* Add a `{{$ctrl.orderProp}}` binding into the `phone-list.template.html` template to display its
current value as text.
* Reverse the sort order by adding a `-` symbol before the sorting value:
`<option value="-age">Oldest</option>`
# Summary
Now that you have added phone images and links, go to {@link step_07 step 7} to learn about Angular
layout templates and how Angular makes it easy to create applications that have multiple views.
Now that you have added list sorting and tested the application, go to {@link step_07 step 7} to
learn about Angular services and how Angular uses dependency injection.
<ul doc-tutorial-nav="6"></ul>
+261 -329
View File
@@ -1,380 +1,312 @@
@ngdoc tutorial
@name 7 - Routing & Multiple Views
@name 7 - XHR & Dependency Injection
@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'.
Enough of building an app with three phones in a hard-coded dataset! Let's fetch a larger dataset
from our server using one of Angular's built-in {@link guide/services services} called
{@link ng.$http $http}. We will use Angular's {@link guide/di dependency injection (DI)} to provide
the service to the `phoneList` component's controller.
* There is now a list of 20 phones, loaded from the server.
* 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 that specific 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.
## Data
We are using [Bower][bower] to install client-side dependencies. This step updates the
`bower.json` configuration file to include the new dependency:
The `app/phones/phones.json` file in our project is a dataset that contains a larger list of phones,
stored in JSON format.
Following is a sample of the file:
```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.4.x",
"angular-mocks": "1.4.x",
"jquery": "~2.2.3",
"bootstrap": "~3.1.1",
"angular-route": "1.4.x"
}
}
[
{
"age": 13,
"id": "motorola-defy-with-motoblur",
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
"snippet": "Are you ready for everything life throws your way?"
...
},
...
]
```
The new dependency `"angular-route": "1.4.x"` tells bower to install a version of the
angular-route component that is compatible with version 1.4.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:
## Component Controller
```
npm install
```
We will use Angular's {@link ng.$http $http} service in our controller for making an HTTP request to
our web server to fetch the data in the `app/phones/phones.json` file. `$http` is just one of
several built-in {@link guide/services Angular services} that handle common operations in web
applications. Angular injects these services for you, right where you need them.
<div class="alert alert-warning">
**Warning:** If a new version of Angular has been released since you last ran `npm install`, then you may have a
problem with the `bower install` due to a conflict between the versions of angular.js that need to
be installed. If you get this then simply delete your `app/bower_components` folder before running
`npm install`.
</div>
Services are managed by Angular's {@link guide/di DI subsystem}. Dependency injection helps to make
your web applications both well-structured (e.g. separate entities for presentation, data, and
control) and loosely coupled (dependencies between entities are not resolved by the entities
themselves, but by the DI subsystem). As a result, applications are easier to test as well.
<div class="alert alert-info">
**Note:** If you have bower installed globally then you can run `bower install` but for this project we have
preconfigured `npm install` to run bower for us.
</div>
## 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, the injector 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`:__
<br />
**`app/phone-list/phone-list.component.js:`**
```js
var phonecatApp = angular.module('phonecatApp', [
'ngRoute',
'phonecatControllers'
]);
angular.
module('phoneList').
component('phoneList', {
templateUrl: 'phone-list/phone-list.template.html',
controller: function PhoneListController($http) {
var self = this;
self.orderProp = 'age';
...
```
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 '/phones/: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;
$http.get('phones/phones.json').then(function(response) {
self.phones = response.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.
`$http` makes an HTTP GET request to our web server, asking for `phones.json` (the URL is relative
to our `index.html` file). The server responds by providing the data in the JSON file.
(The response might just as well have been dynamically generated by a backend server. To the
browser and our app, they both look the same. For the sake of simplicity, we will use JSON files
in this tutorial.)
Because our example app is relatively small, we'll just add all of our controllers to the
`phonecatControllers` module.
The `$http` service returns a {@link ng.$q promise object}, which has a `then()` method. We call
this method to handle the asynchronous response and assign the phone data to the controller, as a
property called `phones`. Notice that Angular detected the JSON response and parsed it for us into
the `data` property of the `response` object passed to our callback!
Since we are making the assignment of the `phones` property in a callback function, where the `this`
value is not defined, we also introduce a local variable called `self` that points back to the
controller instance.
## 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.
To use a service in Angular, you simply declare the names of the dependencies you need as arguments
to the controller's constructor function, as follows:
```js
...
it('should redirect index.html to index.html#/phones', function() {
browser.get('app/index.html');
browser.getLocationAbsUrl().then(function(url) {
expect(url).toEqual('/phones');
});
function PhoneListController($http) {...}
```
Angular's dependency injector provides services to your controller, when the controller is being
constructed. The dependency injector also takes care of creating any transitive dependencies the
service may have (services often depend upon other services).
Note that the names of arguments are significant, because the injector uses these to look up the
dependencies.
<img class="diagram" src="img/tutorial/tutorial_05.png">
### `$`-prefix Naming Convention
You can create your own services, and in fact we will do exactly that a few steps down the road. As
a naming convention, Angular's built-in services, Scope methods and a few other Angular APIs have a
`$` prefix in front of the name.
The `$` prefix is there to namespace Angular-provided services. To prevent collisions it's best to
avoid naming your services and models anything that begins with a `$`.
If you inspect a Scope, you may also notice some properties that begin with `$$`. These properties
are considered private, and should not be accessed or modified.
### A Note on Minification
Since Angular infers the controller's dependencies from the names of arguments to the controller's
constructor function, if you were to [minify][minification] the JavaScript code for the
`PhoneListController` controller, all of its function arguments would be minified as well, and the
dependency injector would not be able to identify services correctly.
We can overcome this problem by annotating the function with the names of the dependencies, provided
as strings, which will not get minified. There are two ways to provide these injection annotations:
* Create an `$inject` property on the controller function which holds an array of strings.
Each string in the array is the name of the service to inject for the corresponding parameter.
In our example, we would write:
```js
function PhoneListController($http) {...}
PhoneListController.$inject = ['$http'];
...
.component('phoneList', {..., controller: PhoneListController});
```
* Use an inline annotation where, instead of just providing the function, you provide an array.
This array contains a list of the service names, followed by the function itself as the last item
of the array.
```js
function PhoneListController($http) {...}
...
.component('phoneList', {..., controller: ['$http', PhoneListController]});
```
Both of these methods work with any function that can be injected by Angular, so it's up to your
project's style guide to decide which one you use.
When using the second method, it is common to provide the constructor function inline, when
registering the controller:
```js
.component('phoneList', {..., controller: ['$http', function PhoneListController($http) {...}]});
```
From this point onwards, we are going to use the inline method in the tutorial. With that in mind,
let's add the annotations to our `PhoneListController`:
<br />
**`app/phone-list/phone-list.component.js`**
```js
angular.
module('phoneList').
component('phoneList', {
templateUrl: 'phone-list/phone-list.template.html',
controller: ['$http',
function PhoneListController($http) {
var self = this;
self.orderProp = 'age';
$http.get('phones/phones.json').then(function(response) {
self.phones = response.data;
});
}
]
});
```
# Testing
Because we started using dependency injection and our controller has dependencies, constructing the
controller in our tests is a bit more complicated. We could use the `new` operator and provide the
constructor with some kind of fake `$http` implementation. However, Angular provides a mock `$http`
service that we can use in unit tests. We configure "fake" responses to server requests by calling
methods on a service called `$httpBackend`:
<br />
**`app/phone-list/phone-list.component.spec.js`:**
```js
describe('phoneList', function() {
beforeEach(module('phoneList'));
describe('controller', function() {
var $httpBackend, ctrl;
// The injector ignores leading and trailing underscores here (i.e. _$httpBackend_).
// This allows us to inject a service and assign it to a variable with the same name
// as the service while avoiding a name conflict.
beforeEach(inject(function($componentController, _$httpBackend_) {
$httpBackend = _$httpBackend_;
$httpBackend.expectGET('phones/phones.json')
.respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
ctrl = $componentController('phoneList');
}));
...
});
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');
});
});
});
```
<div class="alert alert-info">
**Note:** Because we loaded Jasmine and `angular-mocks.js` in our test environment, we got two
helper methods {@link angular.mock.module module} and {@link angular.mock.inject inject} that we
can use to access and configure the injector.
</div>
You can now rerun `npm run protractor` to see the tests run.
We created the controller in the test environment, as follows:
* We used the `inject()` helper method to inject instances of
{@link ngMock.$componentController $componentController} and {@link ng.$httpBackend $httpBackend}
services into Jasmine's `beforeEach()` function. These instances come from an injector which is
recreated from scratch for every single test. This guarantees that each test starts from a well
known starting point and each test is isolated from the work done in other tests.
* We called the injected `$componentController` function passing the name of the `phoneList`
component (whose controller we wanted to instantiate) as a parameter.
Because our code now uses the `$http` service to fetch the phone list data in our controller, before
we create the `PhoneListController`, we need to tell the testing harness to expect an incoming
request from the controller. To do this we:
* Inject the `$httpBackend` service into the `beforeEach()` function. This is a
{@link ngMock.$httpBackend mock version} of the service that in a production environment
facilitates all XHR and JSONP requests. The mock version of this service allows us to write tests
without having to deal with native APIs and the global state associated with them — both of which
make testing a nightmare. It also overcomes the asynchronous nature of these calls, which would
slow down unit tests.
* Use the `$httpBackend.expectGET()` method to train the `$httpBackend` service to expect an
incoming HTTP request and tell it what to respond with. Note that the responses are not returned
until we call the `$httpBackend.flush()` method.
Now we will make assertions to verify that the `phones` property doesn't exist on the controller
before the response is received:
```js
it('should create a `phones` property with 2 phones fetched with `$http`', function() {
expect(ctrl.phones).toBeUndefined();
$httpBackend.flush();
expect(ctrl.phones).toEqual([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
});
```
* We flush the request queue in the browser by calling `$httpBackend.flush()`. This causes the
promise returned by the `$http` service to be resolved with the trained response. See
{@link ngMock.$httpBackend#flushing-http-requests Flushing HTTP requests} in the mock
`$httpBackend` documentation for a full explanation of why this is necessary.
* We make the assertions, verifying that the `phones` property now exists on the controller.
Finally, we verify that the default value of `orderProp` is set correctly:
```js
it('should set a default value for the `orderProp` property', function() {
expect(ctrl.orderProp).toBe('age');
});
```
You should now see the following output in the Karma tab:
```
Chrome 49.0: Executed 2 of 2 SUCCESS (0.133 secs / 0.097 secs)
```
# 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></div>
<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'`. 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>
* At the bottom of `phone-list.template.html`, add a
`<pre>{{$ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp | json}}</pre>` binding to see
the list of phones displayed in JSON format.
* In the `PhoneListController` controller, pre-process the HTTP response by limiting the number of
phones to the first 5 in the list. Use the following code in the `$http` callback:
```js
self.phones = response.data.slice(0, 5);
```
# 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.
Now that you have learned how easy it is to use Angular services (thanks to Angular's dependency
injection), go to {@link step_08 step 8}, where you will add some thumbnail images of phones and
some links.
<ul doc-tutorial-nav="7"></ul>
[bower]: http://bower.io
[minification]: https://en.wikipedia.org/wiki/Minification_(programming)
+69 -149
View File
@@ -1,197 +1,117 @@
@ngdoc tutorial
@name 8 - More Templating
@name 8 - Templating Links & Images
@step 8
@description
<ul doc-tutorial-nav="8"></ul>
In this step, you will implement the phone details view, which is displayed when a user clicks on a
phone in the phone list.
In this step, we will add thumbnail images for the phones in the phone list, and links that, for
now, will go nowhere. In subsequent steps, we will use the links to display additional information
about the phones in the catalog.
* When you click on a phone on the list, the phone details page with phone-specific information
is displayed.
* There are now links and images of the phones in the list.
To implement the phone details view we are going to use {@link ng.$http $http} to fetch our data,
and then flesh out the `phone-detail.html` view template.
<div doc-tutorial-reset="8"></div>
## Data
In addition to `phones.json`, the `app/phones/` directory also contains one JSON file for each
phone:
Note that the `phones.json` file contains unique IDs and image URLs for each of the phones. The
URLs point to the `app/img/phones/` directory.
__`app/phones/nexus-s.json`:__ (sample snippet)
<br />
**`app/phones/phones.json`** (sample snippet):
```js
{
"additionalFeatures": "Contour Display, Near Field Communications (NFC),...",
"android": {
"os": "Android 2.3",
"ui": "Android"
```json
[
{
...
"id": "motorola-defy-with-motoblur",
"imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg",
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
...
},
...
"images": [
"img/phones/nexus-s.0.jpg",
"img/phones/nexus-s.1.jpg",
"img/phones/nexus-s.2.jpg",
"img/phones/nexus-s.3.jpg"
],
"storage": {
"flash": "16384MB",
"ram": "512MB"
}
}
]
```
Each of these files describes various properties of the phone using the same data structure. We'll
show this data in the phone detail view.
## Component Template
## Controller
We'll expand the `PhoneDetailCtrl` by using the `$http` service to fetch the JSON files. This works
the same way as the phone list controller.
__`app/js/controllers.js`:__
```js
var phonecatControllers = angular.module('phonecatControllers',[]);
phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', '$http',
function($scope, $routeParams, $http) {
$http.get('phones/' + $routeParams.phoneId + '.json').success(function(data) {
$scope.phone = data;
});
}]);
```
To construct the URL for the HTTP request, we use `$routeParams.phoneId` extracted from the current
route by the `$route` service.
## Template
The TBD placeholder line has been replaced with lists and bindings that comprise the phone details.
Note where we use the Angular `{{expression}}` markup and `ngRepeat` to project phone data from
our model into the view.
__`app/partials/phone-detail.html`:__
<br />
**`app/phone-list/phone-list.template.html`:**
```html
<img ng-src="{{phone.images[0]}}" class="phone">
<h1>{{phone.name}}</h1>
<p>{{phone.description}}</p>
<ul class="phone-thumbs">
<li ng-repeat="img in phone.images">
<img ng-src="{{img}}">
...
<ul class="phones">
<li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp" class="thumbnail">
<a href="#/phones/{{phone.id}}" class="thumb">
<img ng-src="{{phone.imageUrl}}" alt="{{phone.name}}" />
</a>
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
<p>{{phone.snippet}}</p>
</li>
</ul>
...
```
<ul class="specs">
<li>
<span>Availability and Networks</span>
<dl>
<dt>Availability</dt>
<dd ng-repeat="availability in phone.availability">{{availability}}</dd>
</dl>
</li>
To dynamically generate links that will in the future lead to phone detail pages, we used the
now-familiar double-curly brace binding in the `href` attribute values. In step 2, we added the
`{{phone.name}}` binding as the element content. In this step the `{{phone.id}}` binding is used in
the element attribute.
We also added phone images next to each record using an image tag with the {@link ngSrc ngSrc}
directive. That directive prevents the browser from treating the Angular `{{ expression }}` markup
literally, and initiating a request to an invalid URL (`http://localhost:8000/{{phone.imageUrl}}`),
which it would have done if we had only specified an attribute binding in a regular `src` attribute
(`<img src="{{phone.imageUrl}}">`). Using the `ngSrc` directive, prevents the browser from making an
HTTP request to an invalid location.
# Testing
<br />
**`e2e-tests/scenarios.js`**:
```js
...
<li>
<span>Additional Features</span>
<dd>{{phone.additionalFeatures}}</dd>
</li>
</ul>
```
<div style="display: none">
TODO!
<img class="diagram" src="img/tutorial/tutorial_08-09_final.png">
</div>
it('should render phone specific links', function() {
var query = element(by.model('$ctrl.query'));
query.sendKeys('nexus');
## Test
We wrote a new unit test that is similar to the one we wrote for the `PhoneListCtrl` controller in
step 5.
__`test/unit/controllersSpec.js`:__
```js
beforeEach(module('phonecatApp'));
...
describe('PhoneDetailCtrl', function(){
var scope, $httpBackend, ctrl;
beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) {
$httpBackend = _$httpBackend_;
$httpBackend.expectGET('phones/xyz.json').respond({name:'phone xyz'});
$routeParams.phoneId = 'xyz';
scope = $rootScope.$new();
ctrl = $controller('PhoneDetailCtrl', {$scope: scope});
}));
it('should fetch phone detail', function() {
expect(scope.phone).toBeUndefined();
$httpBackend.flush();
expect(scope.phone).toEqual({name:'phone xyz'});
});
});
...
```
You should now see the following output in the Karma tab:
<pre>Chrome 22.0: Executed 3 of 3 SUCCESS (0.039 secs / 0.012 secs)</pre>
We also added a new end-to-end test that navigates to the Nexus S detail page and verifies that the
heading on the page is "Nexus S".
__`test/e2e/scenarios.js`:__
```js
...
describe('Phone detail view', function() {
beforeEach(function() {
browser.get('app/index.html#/phones/nexus-s');
element.all(by.css('.phones li a')).first().click();
expect(browser.getLocationAbsUrl()).toBe('/phones/nexus-s');
});
it('should display nexus-s page', function() {
expect(element(by.binding('phone.name')).getText()).toBe('Nexus S');
});
});
...
...
```
We added a new E2E test to verify that the application is generating correct links to the phone
views, that we will implement in the upcoming steps.
You can now rerun `npm run protractor` to see the tests run.
# Experiments
* Using the [Protractor API](http://angular.github.io/protractor/#/api),
write a test that verifies that we display 4 thumbnail images on the Nexus S details page.
<div></div>
* Replace the `ngSrc` directive with a plain old `src` attribute. Using tools such as your browser's
developer tools or inspecting the web server access logs, confirm that the application is indeed
making an extraneous request to `%7B%7Bphone.imageUrl%7D%7D` (or `{{phone.imageUrl}}`).
The issue here is that the browser will fire a request for that invalid image address as soon as
it hits the `<img>` tag, which is before Angular has a chance to evaluate the expression and
inject the valid address.
# Summary
Now that the phone details view is in place, proceed to {@link step_09 step 9} to learn how to
write your own custom display filter.
Now that you have added phone images and links, go to {@link step_09 step 9} to learn about Angular
layout templates and how Angular makes it easy to create applications that have multiple views.
<ul doc-tutorial-nav="8"></ul>
+374 -89
View File
@@ -1,144 +1,429 @@
@ngdoc tutorial
@name 9 - Filters
@name 9 - Routing & Multiple Views
@step 9
@description
<ul doc-tutorial-nav="9"></ul>
In this step you will learn how to create your own custom display filter.
* In the previous step, the details page displayed either "true" or "false" to indicate whether
certain phone features were present or not. We have used a custom filter to convert those text
strings into glyphs: ✓ for "true", and ✘ for "false". Let's see what the filter code looks like.
In this step, you will learn how to create a layout template and how to build an application that
has multiple views by adding routing, using an Angular module called {@link ngRoute ngRoute}.
* When you now navigate to `/index.html`, you are redirected to `/index.html#!/phones` and the phone
list appears in the browser.
* When you click on a phone link, the URL changes to that specific phone and the stub of a phone
detail page is displayed.
<div doc-tutorial-reset="9"></div>
## Custom Filter
## Dependencies
In order to create a new filter, you are going to create a `phonecatFilters` module and register
your custom filter with this module:
The routing functionality added in this step is provided by Angular in the `ngRoute` module, which
is distributed separately from the core Angular framework.
__`app/js/filters.js`:__
Since we are using [Bower][bower] to install client-side dependencies, this step updates the
`bower.json` configuration file to include the new dependency:
```js
angular.module('phonecatFilters', []).filter('checkmark', function() {
return function(input) {
return input ? '\u2713' : '\u2718';
};
});
<br />
**`bower.json`:**
```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.5.x",
"angular-mocks": "1.5.x",
"angular-route": "1.5.x",
"bootstrap": "3.3.x"
}
}
```
The name of our filter is "checkmark". The `input` evaluates to either `true` or `false`, and we
return one of the two unicode characters we have chosen to represent true (`\u2713` -> ✓) or false (`\u2718` -> ✘).
The new dependency `"angular-route": "1.5.x"` tells bower to install a version of the angular-route
module that is compatible with version 1.5.x of Angular. We must tell bower to download and install
this dependency.
Now that our filter is ready, we need to register the `phonecatFilters` module as a dependency for
our main `phonecatApp` module.
__`app/js/app.js`:__
```js
...
angular.module('phonecatApp', ['ngRoute','phonecatControllers','phonecatFilters']);
...
```
npm install
```
<div class="alert alert-info">
**Note:** If you have bower installed globally, you can run `bower install`, but for this project
we have preconfigured `npm install` to run bower for us.
</div>
<div class="alert alert-warning">
**Warning:** If a new version of Angular has been released since you last ran `npm install`, then
you may have a problem with the `bower install` due to a conflict between the versions of
angular.js that need to be installed. If you run into this issue, simply delete your
`app/bower_components` directory and then run `npm install`.
</div>
## Multiple Views, Routing and Layout Templates
Our app is slowly growing and becoming more complex. Prior to this step, the app provided our users
with a single view (including the list of all phones), and all of the template code was located in
the `phone-list.template.html` file. The next step in building the application is to add a view that
will show detailed information about each of the devices in our list.
To add the detailed view, we are going to turn `index.html` 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][deep-linking], which lets us utilize the browser's history
(back and forward navigation) and bookmarks.
<div class="alert alert-success">
<p>
`ngRoute` allows as to associate a controller and a template with a specific URL (or URL
pattern). This is pretty close to what we did with `ngController` and `index.html` back in
{@link step_02 step 2}.
</p>
<p>
Since we have already learned that components allow us to combine controllers with templates in
a modular, testable way, we are going to use components for routing as well.
Each route will be associated with a component and that component will be in charge of providing
the view template and the controller.
</p>
</div>
### A Note about DI, Injector and Providers
As you {@link step_07 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 application. The injector itself doesn't know anything
about what the `$http` or `$route` services do. In fact, the injector 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 application.
* Register all Providers defined in these module definition(s).
* When asked to do so, lazily instantiate services and their dependencies, via their Providers, as
parameters to an injectable function.
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 `PhoneListController` at runtime.
</div>
Angular modules solve the problem of removing global variables 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 on Angular's DI, see [Understanding Dependency Injection][wiki-di].
## Template
Since the filter code lives in the `app/js/filters.js` file, we need to include this file in our
layout 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.
__`app/index.html`:__
<br />
**`app/index.html`:**
```html
...
<script src="js/controllers.js"></script>
<script src="js/filters.js"></script>
...
<head>
...
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-route/angular-route.js"></script>
<script src="app.module.js"></script>
<script src="app.config.js"></script>
...
<script src="phone-detail/phone-detail.module.js"></script>
<script src="phone-detail/phone-detail.component.js"></script>
</head>
<body>
<div ng-view></div>
</body>
```
The syntax for using filters in Angular templates is as follows:
We have added four new `<script>` tags in our `index.html` file to load some extra JavaScript files
into our application:
{{ expression | filter }}
* `angular-route.js`: Defines the Angular `ngRoute` module, which provides us with routing.
* `app.config.js`: Configures the providers available to our main module (see
[below](tutorial/step_09#configuring-a-module)).
* `phone-detail.module.js`: Defines a new module containing a `phoneDetail` component.
* `phone-detail.component.js`: Defines a dummy `phoneDetail` component (see
[below](tutorial/step_09#the-phonedetail-component)).
Let's employ the filter in the phone details template:
Note that we removed the `<phone-list></phone-list>` line from the `index.html` template and
replaced it with a `<div>` with the `ng-view` attribute.
<img class="diagram" src="img/tutorial/tutorial_09.png">
## Configuring a Module
__`app/partials/phone-detail.html`:__
A module's {@link ng.angular.Module#config .config()} method gives us access to the available
providers for configuration. To make the providers, services and directives defined in `ngRoute`
available to our application, we need to add `ngRoute` as a dependency of our `phonecatApp` module.
```html
...
<dl>
<dt>Infrared</dt>
<dd>{{phone.connectivity.infrared | checkmark}}</dd>
<dt>GPS</dt>
<dd>{{phone.connectivity.gps | checkmark}}</dd>
</dl>
...
```
## Test
Filters, like any other component, should be tested and these tests are very easy to write.
__`test/unit/filtersSpec.js`:__
<br />
**`app/app.module.js`:**
```js
describe('filter', function() {
beforeEach(module('phonecatFilters'));
describe('checkmark', function() {
it('should convert boolean values to unicode checkmark or cross',
inject(function(checkmarkFilter) {
expect(checkmarkFilter(true)).toBe('\u2713');
expect(checkmarkFilter(false)).toBe('\u2718');
}));
});
});
angular.module('phonecatApp', [
'ngRoute',
...
]);
```
We must call `beforeEach(module('phonecatFilters'))` before any of
our filter tests execute. This call loads our `phonecatFilters` module into the injector
for this test run.
Now, in addition to the core services and directives, we can also configure the `$route` service
(using it's provider) for our application. In order to be able to quickly locate the configuration
code, we put it into a separate file and used the `.config` suffix.
Note that we call the helper function, `inject(function(checkmarkFilter) { ... })`, to get
access to the filter that we want to test. See {@link angular.mock.inject angular.mock.inject()}.
<br />
**`app/app.config.js`:**
Notice that the suffix 'Filter' is appended to your filter name when injected.
See the {@link guide/filter#using-filters-in-controllers-services-and-directives Filter Guide}
section where this is outlined.
```js
angular.
module('phonecatApp').
config(['$locationProvider', '$routeProvider',
function config($locationProvider, $routeProvider) {
$locationProvider.hashPrefix('!');
You should now see the following output in the Karma tab:
$routeProvider.
when('/phones', {
template: '<phone-list></phone-list>'
}).
when('/phones/:phoneId', {
template: '<phone-detail></phone-detail>'
}).
otherwise('/phones');
}
]);
```
<pre>Chrome 22.0: Executed 4 of 4 SUCCESS (0.034 secs / 0.012 secs)</pre>
Using the `.config()` method, we request the necessary providers (for example the `$routeProvider`)
to be injected into our configuration function and then use their methods to specify the behavior of
the corresponding services. Here, we use the
{@link ngRoute.$routeProvider#when $routeProvider.when()} and
{@link ngRoute.$routeProvider#otherwise $routeProvider.otherwise()} methods to define our
application routes.
<div class="alert alert-success">
<p>
We also used {@ink $locationProvider#hashPrefix $locationProvider.hashPrefix()} to set the
hash-prefix to `!`. This prefix will appear in the links to our client-side routes, right after
the hash (`#`) symbol and before the actual path (e.g. `index.html#!/some/path`).
</p>
<p>
Setting a prefix is not necessary, but it is considered a good practice (for reasons that are
outside the scope of this tutorial). `!` is the most commonly used prefix.
</p>
</div>
Our routes are defined as follows:
* `when('/phones')`: Determines the view that will be shown, when the URL hash fragment is
`/phones`. According to the specified template, Angular will create an instance of the `phoneList`
component to manage the view. Note that this is the same markup that we used to have in the
`index.html` file.
* `when('/phones/:phoneId')`: Determines the view that will be shown, when the URL hash fragment
matches `/phones/<phoneId>`, where `<phoneId>` is a variable part of the URL. In charge of the
view will be the `phoneDetail` component.
* `otherwise('/phones')`: Defines a fallback route to redirect to, when no route definition matches
the current URL.(Here it will redirect to `/phones`.)
We reused the `phoneList` component that we have already built and a new "dummy" `phoneDetail`
component. For now, the `phoneDetail` component will just display the selected phone's ID.
(Not too impressive, but we will enhance it in the {@link step_10 next step}.)
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 `:` prefix are extracted into the (injectable)
{@link ngRoute.$routeParams $routeParams} object.
## The `phoneDetail` Component
We created a `phoneDetail` component to handle the phone details view. We followed the same
conventions as with `phoneList`, using a separate directory and creating a `phoneDetail` module,
which we added as a dependency of the `phonecatApp` module.
<br />
**`app/phone-detail/phone-detail.module.js`:**
```js
angular.module('phoneDetail', [
'ngRoute'
]);
```
<br />
**`app/phone-detail/phone-detail.component.js`:**
```js
angular.
module('phoneDetail').
component('phoneDetail', {
template: 'TBD: Detail view for <span>{{$ctrl.phoneId}}</span>',
controller: ['$routeParams',
function PhoneDetailController($routeParams) {
this.phoneId = $routeParams.phoneId;
}
]
});
```
<br />
**`app/app.module.js`:**
```js
angular.module('phonecatApp', [
...
'phoneDetail',
...
]);
```
### A Note on Sub-module Dependencies
The `phoneDetail` module depends on the `ngRoute` module for providing the `$routeParams` object,
which is used in the `phoneDetail` component's controller. Since `ngRoute` is also a dependency of
the main `phonecatApp` module, its services and directives are already available everywhere in the
application (including the `phoneDetail` component).
This means that our application would continue to work even if we didn't include `ngRoute` in the
list of dependencies for the `phoneDetail` component. Although it might be tempting to omit
dependencies of a sub-module that are already imported by the main module, it breaks our hard-earned
modularity.
<div class="alert alert-warning">
Imagine what would happen if we decided to copy the `phoneDetail` feature over to another project
that does not declare a dependency on `ngRoute`. The injector would not be able to provide
`$routeParams` and our application would break.
</div>
The takeaway here is:
* Always be explicit about the dependecies of a sub-module. Do not rely on dependencies inherited
from a parent module (because that parent module might not be there some day).
<div class="alert alert-success">
Declaring the same dependency in multiple modules does not incur extra "cost", because Angular
will still load each dependency once. For more info on modules and their dependencies take a look
at the [Modules](guide/module) section of the Developer Guide.
</div>
# Testing
Since some of our modules depend on {@link ngRoute ngRoute} now, it is necessary to update the Karma
configuration file with angular-route. Other than that, the unit tests should (still) pass without
any modification.
<br />
**`karma.conf.js`:**
```js
files: [
'bower_components/angular/angular.js',
'bower_components/angular-route/angular-route.js',
...
],
```
<br />
To automatically verify that everything is wired properly, we wrote E2E tests for navigating to
various URLs and verifying that the correct view was rendered.
<br />
**`e2e-tests/scenarios.js`**
```js
...
it('should redirect `index.html` to `index.html#!/phones', function() {
browser.get('index.html');
expect(browser.getLocationAbsUrl()).toBe('/phones');
});
...
describe('View: Phone list', function() {
beforeEach(function() {
browser.get('index.html#!/phones');
});
...
});
...
describe('View: Phone details', function() {
beforeEach(function() {
browser.get('index.html#!/phones/nexus-s');
});
it('should display placeholder page with `phoneId`', function() {
expect(element(by.binding('$ctrl.phoneId')).getText()).toBe('nexus-s');
});
});
...
```
You can now rerun `npm run protractor` to see the tests run (and hopefully pass).
# Experiments
* Let's experiment with some of the {@link api/ng/filter built-in Angular filters} and add the
following bindings to `index.html`:
* `{{ "lower cap string" | uppercase }}`
* `{{ {foo: "bar", baz: 23} | json }}`
* `{{ 1304375948024 | date }}`
* `{{ 1304375948024 | date:"MM/dd/yyyy @ h:mma" }}`
<div></div>
* We can also create a model with an input element, and combine it with a filtered binding. Add
the following to index.html:
* Try to add a `{{$ctrl.phoneId}` binding in the template string for the phone details view:
```html
<input ng-model="userInput"> Uppercased: {{ userInput | uppercase }}
```js
when('/phones/:phoneId', {
template: '{{$ctrl.phoneId}} <phone-detail></phone-detail>'
...
```
You will see that nothing happens, even when you are in the phone details view. This is because
the `phoneId` model is visible only in the context set by the `phoneDetail` component. Again,
component isolation at work!
# Summary
Now that you have learned how to write and test a custom filter, go to {@link step_10 step 10} to
learn how we can use Angular to enhance the phone details page further.
With the routing set up and the phone list view implemented, we are ready to go to
{@link step_10 step 10} and implement a proper phone details view.
<ul doc-tutorial-nav="9"></ul>
[bower]: http://bower.io
[deep-linking]: https://en.wikipedia.org/wiki/Deep_linking
[wiki-di]: https://github.com/angular/angular.js/wiki/Understanding-Dependency-Injection
+153 -119
View File
@@ -1,180 +1,214 @@
@ngdoc tutorial
@name 10 - Event Handlers
@name 10 - More Templating
@step 10
@description
<ul doc-tutorial-nav="10"></ul>
In this step, you will add a clickable phone image swapper to the phone details page.
In this step, we will implement the phone details view, which is displayed when a user clicks on a
phone in the phone list.
* When you click on a phone on the list, the phone details page with phone-specific information is
displayed.
To implement the phone details view we are going to use {@link ng.$http $http} to fetch our data,
and then flesh out the `phoneDetail` component's template.
* The phone details view displays one large image of the current phone and several smaller thumbnail
images. It would be great if we could replace the large image with any of the thumbnails just by
clicking on the desired thumbnail image. Let's have a look at how we can do this with Angular.
<div doc-tutorial-reset="10"></div>
## Controller
__`app/js/controllers.js`:__
## Data
```js
...
var phonecatControllers = angular.module('phonecatControllers',[]);
In addition to `phones.json`, the `app/phones/` directory also contains one JSON file for each
phone:
phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', '$http',
function($scope, $routeParams, $http) {
$http.get('phones/' + $routeParams.phoneId + '.json').success(function(data) {
$scope.phone = data;
$scope.mainImageUrl = data.images[0];
});
<br />
**`app/phones/nexus-s.json`:** (sample snippet)
$scope.setImage = function(imageUrl) {
$scope.mainImageUrl = imageUrl;
};
}]);
```json
{
"additionalFeatures": "Contour Display, Near Field Communications (NFC), ...",
"android": {
"os": "Android 2.3",
"ui": "Android"
},
...
"images": [
"img/phones/nexus-s.0.jpg",
"img/phones/nexus-s.1.jpg",
"img/phones/nexus-s.2.jpg",
"img/phones/nexus-s.3.jpg"
],
"storage": {
"flash": "16384MB",
"ram": "512MB"
}
}
```
In the `PhoneDetailCtrl` controller, we created the `mainImageUrl` model property and set its
default value to the first phone image URL.
We also created a `setImage` event handler function that will change the value of `mainImageUrl`.
Each of these files describes various properties of the phone using the same data structure. We will
show this data in the phone details view.
## Template
## Component Controller
__`app/partials/phone-detail.html`:__
We will expand the `phoneDetail` component's controller by using the `$http` service to fetch the
appropriate JSON files. This works the same way as the `phoneList` component's controller.
<br />
**`app/phone-detail/phone-detail.component.js`:**
```js
angular.
module('phoneDetail').
component('phoneDetail', {
templateUrl: 'phone-detail/phone-detail.template.html',
controller: ['$http', '$routeParams',
function PhoneDetailController($http, $routeParams) {
var self = this;
$http.get('phones/' + $routeParams.phoneId + '.json').then(function(response) {
self.phone = response.data;
});
}
]
});
```
To construct the URL for the HTTP request, we use `$routeParams.phoneId`, which is extracted from
the current route by the `$route` service.
## Component Template
The inline, TBD placeholder template has been replaced with a full blown external template,
including lists and bindings that comprise the phone details. Note how we use the Angular
`{{expression}}` markup and `ngRepeat` to project phone data from our model into the view.
<br />
**`app/phone-detail/phone-detail.template.html`:**
```html
<img ng-src="{{mainImageUrl}}" class="phone">
<img ng-src="{{$ctrl.phone.images[0]}}" class="phone" />
...
<h1>{{$ctrl.phone.name}}</h1>
<p>{{$ctrl.phone.description}}</p>
<ul class="phone-thumbs">
<li ng-repeat="img in phone.images">
<img ng-src="{{img}}" ng-click="setImage(img)">
<li ng-repeat="img in $ctrl.phone.images">
<img ng-src="{{img}}" />
</li>
</ul>
<ul class="specs">
<li>
<span>Availability and Networks</span>
<dl>
<dt>Availability</dt>
<dd ng-repeat="availability in $ctrl.phone.availability">{{availability}}</dd>
</dl>
</li>
...
<li>
<span>Additional Features</span>
<dd>{{$ctrl.phone.additionalFeatures}}</dd>
</li>
</ul>
...
```
We bound the `ngSrc` directive of the large image to the `mainImageUrl` property.
<img class="diagram" src="img/tutorial/tutorial_10.png">
We also registered an {@link ng.directive:ngClick `ngClick`}
handler with thumbnail images. When a user clicks on one of the thumbnail images, the handler will
use the `setImage` event handler function to change the value of the `mainImageUrl` property to the
URL of the thumbnail image.
<div style="display: none">
TODO!
<img class="diagram" src="img/tutorial/tutorial_10-11_final.png">
</div>
# Testing
## Test
We wrote a new unit test that is similar to the one we wrote for the `phoneList` component's
controller in {@link step_07#testing step 7}.
To verify this new feature, we added two end-to-end tests. One verifies that the main image is set
to the first phone image by default. The second test clicks on several thumbnail images and
verifies that the main image changed appropriately.
__`test/e2e/scenarios.js`:__
<br />
**`app/phone-detail/phone-detail.component.spec.js`:**
```js
...
describe('Phone detail view', function() {
describe('phoneDetail', function() {
...
// Load the module that contains the `phoneDetail` component before each test
beforeEach(module('phoneDetail'));
it('should display the first phone image as the main phone image', function() {
expect(element(by.css('img.phone')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
});
// Test the controller
describe('PhoneDetailController', function() {
var $httpBackend, ctrl;
it('should swap main image if a thumbnail image is clicked on', function() {
element(by.css('.phone-thumbs li:nth-child(3) img')).click();
expect(element(by.css('img.phone')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.2.jpg/);
element(by.css('.phone-thumbs li:nth-child(1) img')).click();
expect(element(by.css('img.phone')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
});
});
```
You can now rerun `npm run protractor` to see the tests run.
You also have to refactor one of your unit tests because of the addition of the `mainImageUrl`
model property to the `PhoneDetailCtrl` controller. Below, we create the function `xyzPhoneData`
which returns the appropriate json with the `images` attribute in order to get the test to pass.
__`test/unit/controllersSpec.js`:__
```js
...
beforeEach(module('phonecatApp'));
...
describe('PhoneDetailCtrl', function(){
var scope, $httpBackend, ctrl,
xyzPhoneData = function() {
return {
name: 'phone xyz',
images: ['image/url1.png', 'image/url2.png']
}
};
beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) {
beforeEach(inject(function($componentController, _$httpBackend_, $routeParams) {
$httpBackend = _$httpBackend_;
$httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData());
$httpBackend.expectGET('phones/xyz.json').respond({name: 'phone xyz'});
$routeParams.phoneId = 'xyz';
scope = $rootScope.$new();
ctrl = $controller('PhoneDetailCtrl', {$scope: scope});
ctrl = $componentController('phoneDetail');
}));
it('should fetch the phone details', function() {
expect(ctrl.phone).toBeUndefined();
it('should fetch phone detail', function() {
expect(scope.phone).toBeUndefined();
$httpBackend.flush();
expect(scope.phone).toEqual(xyzPhoneData());
expect(ctrl.phone).toEqual({name: 'phone xyz'});
});
});
});
```
Your unit tests should now be passing.
You should now see the following output in the Karma tab:
```
Chrome 49.0: Executed 3 of 3 SUCCESS (0.159 secs / 0.136 secs)
```
We also added a new E2E test that navigates to the 'Nexus S' details page and verifies that the
heading on the page is "Nexus S".
<br />
**`e2e-tests/scenarios.js`**
```js
...
describe('View: Phone detail', function() {
beforeEach(function() {
browser.get('index.html#!/phones/nexus-s');
});
it('should display the `nexus-s` page', function() {
expect(element(by.binding('$ctrl.phone.name')).getText()).toBe('Nexus S');
});
});
...
```
You can run the tests with `npm run protractor`.
# Experiments
* Let's add a new controller method to `PhoneDetailCtrl`:
<div></div>
$scope.hello = function(name) {
alert('Hello ' + (name || 'world') + '!');
}
and add:
<button ng-click="hello('Elmo')">Hello</button>
to the `phone-detail.html` template.
<div style="display: none">
TODO!
The controller methods are inherited between controllers/scopes, so you can use the same snippet
in the `phone-list.html` template as well.
* Move the `hello` method from `PhoneCatCtrl` to `PhoneListCtrl` and you'll see that the button
declared in `index.html` will stop working, while the one declared in the `phone-list.html`
template remains operational.
</div>
* Using [Protractor's API][protractor-docs], write a test that verifies that we display 4 thumbnail
images on the 'Nexus S' details page.
# Summary
With the phone image swapper in place, we're ready for {@link step_11 step 11} to
learn an even better way to fetch data.
Now that the phone details view is in place, proceed to {@link step_11 step 11} to learn how to
write your own custom display filter.
<ul doc-tutorial-nav="10"></ul>
[protractor-docs]: https://angular.github.io/protractor/#/api
+119 -236
View File
@@ -1,293 +1,176 @@
@ngdoc tutorial
@name 11 - REST and Custom Services
@name 11 - Custom Filters
@step 11
@description
<ul doc-tutorial-nav="11"></ul>
In this step, you will change the way our app fetches data.
In this step you will learn how to create your own custom display filter.
* We define a custom service that represents a [RESTful][restful] client. Using this client we
can make requests to the server for data in an easier way, without having to deal with the
lower-level {@link ng.$http $http} API, HTTP methods and URLs.
* In the previous step, the details page displayed either "true" or "false" to indicate whether
certain phone features were present or not. In this step, we are using a custom filter to convert
those text strings into glyphs: ✓ for "true", and ✘ for "false".
Let's see what the filter code looks like.
<div doc-tutorial-reset="11"></div>
## Dependencies
The RESTful functionality is provided by Angular in the `ngResource` module, which is distributed
separately from the core Angular framework.
## The `checkmark` Filter
We are using [Bower][bower] to install client side dependencies. This step updates the
`bower.json` configuration file to include the new dependency:
Since this filter is generic (i.e. it is not specific to any view or component), we are going to
register it in a `core` module, which contains "application-wide" features.
```
{
"name": "angular-seed",
"description": "A starter project for AngularJS",
"version": "0.0.0",
"homepage": "https://github.com/angular/angular-seed",
"license": "MIT",
"private": true,
"dependencies": {
"angular": "1.4.x",
"angular-mocks": "1.4.x",
"jquery": "~2.2.3",
"bootstrap": "~3.1.1",
"angular-route": "1.4.x",
"angular-resource": "1.4.x"
}
}
<br />
**`app/core/core.module.js`:**
```js
angular.module('core', []);
```
The new dependency `"angular-resource": "1.4.x"` tells bower to install a version of the
angular-resource component that is compatible with version 1.4.x. We must ask bower to download
and install this dependency. We can do this by running:
<br />
**`app/core/checkmark/checkmark.filter.js`:**
```js
angular.
module('core').
filter('checkmark', function() {
return function(input) {
return input ? '\u2713' : '\u2718';
};
});
```
npm install
```
<div class="alert alert-warning">
**Warning:** If a new version of Angular has been released since you last ran `npm install`, then you may have a
problem with the `bower install` due to a conflict between the versions of angular.js that need to
be installed. If you get this then simply delete your `app/bower_components` folder before running
`npm install`.
</div>
<div class="alert alert-info">
**Note:** If you have bower installed globally then you can run `bower install` but for this project we have
preconfigured `npm install` to run bower for us.
As you may have noticed, we (unsurprisingly) gave our file a `.filter` suffix.
</div>
The name of our filter is "checkmark". The `input` evaluates to either `true` or `false`, and we
return one of the two unicode characters we have chosen to represent true (`\u2713` -> ✓) and false
(`\u2718` -> ✘).
## Template
Now that our filter is ready, we need to register the `core` module as a dependency of our main
`phonecatApp` module.
Our custom resource service will be defined in `app/js/services.js` so we need to include this file
in our layout template. Additionally, we also need to load the `angular-resource.js` file, which
contains the {@link module:ngResource ngResource} module:
<br />
**`app/app.module.js`:**
__`app/index.html`.__
```js
angular.module('phonecatApp', [
...
'core',
...
]);
```
## Templates
Since we have created two new files (**core.module.js**, **checkmark.filter.js**), we need to
include them in our layout template.
<br />
**`app/index.html`:**
```html
...
<script src="bower_components/angular-resource/angular-resource.js"></script>
<script src="js/services.js"></script>
...
...
<script src="core/core.module.js"></script>
<script src="core/checkmark/checkmark.filter.js"></script>
...
```
## Service
The syntax for using filters in Angular templates is as follows:
We create our own service to provide access to the phone data on the server:
__`app/js/services.js`.__
```js
var phonecatServices = angular.module('phonecatServices', ['ngResource']);
phonecatServices.factory('Phone', ['$resource',
function($resource){
return $resource('phones/:phoneId.json', {}, {
query: {method:'GET', params:{phoneId:'phones'}, isArray:true}
});
}]);
```
{{expression | filter}}
```
We used the module API to register a custom service using a factory function. We passed in the name
of the service - 'Phone' - and the factory function. The factory function is similar to a
controller's constructor in that both can declare dependencies to be injected via function
arguments. The Phone service declared a dependency on the `$resource` service.
Let's employ the filter in the phone details template:
The {@link ngResource.$resource `$resource`} service makes it easy to create a
[RESTful][restful] client with just a few lines of code. This client can then be used in our
application, instead of the lower-level {@link ng.$http $http} service.
<br />
**`app/phone-detail/phone-detail.template.html`:**
__`app/js/app.js`.__
```js
...
angular.module('phonecatApp', ['ngRoute', 'phonecatControllers','phonecatFilters', 'phonecatServices']).
...
```html
...
<dl>
<dt>Infrared</dt>
<dd>{{$ctrl.phone.connectivity.infrared | checkmark}}</dd>
<dt>GPS</dt>
<dd>{{$ctrl.phone.connectivity.gps | checkmark}}</dd>
</dl>
...
```
We need to add the 'phonecatServices' module dependency to 'phonecatApp' module's requires array.
# Testing
## Controller
Filters, like any other code, should be tested. Luckily, these tests are very easy to write.
We simplified our sub-controllers (`PhoneListCtrl` and `PhoneDetailCtrl`) by factoring out the
lower-level {@link ng.$http $http} service, replacing it with a new service called
`Phone`. Angular's {@link ngResource.$resource `$resource`} service is easier to
use than `$http` for interacting with data sources exposed as RESTful resources. It is also easier
now to understand what the code in our controllers is doing.
__`app/js/controllers.js`.__
<br />
**`app/core/checkmark/checkmark.filter.spec.js`:**
```js
var phonecatControllers = angular.module('phonecatControllers', []);
describe('checkmark', function() {
...
beforeEach(module('core'));
phonecatControllers.controller('PhoneListCtrl', ['$scope', 'Phone', function($scope, Phone) {
$scope.phones = Phone.query();
$scope.orderProp = 'age';
}]);
it('should convert boolean values to unicode checkmark or cross',
inject(function(checkmarkFilter) {
expect(checkmarkFilter(true)).toBe('\u2713');
expect(checkmarkFilter(false)).toBe('\u2718');
})
);
phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', 'Phone', function($scope, $routeParams, Phone) {
$scope.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) {
$scope.mainImageUrl = phone.images[0];
});
$scope.setImage = function(imageUrl) {
$scope.mainImageUrl = imageUrl;
}
}]);
```
Notice how in `PhoneListCtrl` we replaced:
$http.get('phones/phones.json').success(function(data) {
$scope.phones = data;
});
with:
$scope.phones = Phone.query();
This is a simple statement that we want to query for all phones.
An important thing to notice in the code above is that we don't pass any callback functions when
invoking methods of our Phone service. Although it looks as if the result were returned
synchronously, that is not the case at all. What is returned synchronously is a "future" — an
object, which will be filled with data when the XHR response returns. Because of the data-binding
in Angular, we can use this future and bind it to our template. Then, when the data arrives, the
view will automatically update.
Sometimes, relying on the future object and data-binding alone is not sufficient to do everything
we require, so in these cases, we can add a callback to process the server response. The
`PhoneDetailCtrl` controller illustrates this by setting the `mainImageUrl` in a callback.
## Test
Because we're now using the {@link ngResource ngResource} module, it's necessary to
update the Karma config file with angular-resource so the new tests will pass.
__`test/karma.conf.js`:__
```js
files : [
'app/bower_components/angular/angular.js',
'app/bower_components/angular-route/angular-route.js',
'app/bower_components/angular-resource/angular-resource.js',
'app/bower_components/angular-mocks/angular-mocks.js',
'app/js/**/*.js',
'test/unit/**/*.js'
],
```
We have modified our unit tests to verify that our new service is issuing HTTP requests and
processing them as expected. The tests also check that our controllers are interacting with the
service correctly.
The {@link ngResource.$resource $resource} service augments the response object
with methods for updating and deleting the resource. If we were to use the standard `toEqual`
matcher, our tests would fail because the test values would not match the responses exactly. To
solve the problem, we use a newly-defined `toEqualData` [Jasmine matcher][jasmine-matchers]. When
the `toEqualData` matcher compares two objects, it takes only object properties into account and
ignores methods.
__`test/unit/controllersSpec.js`:__
```js
describe('PhoneCat controllers', function() {
beforeEach(function(){
this.addMatchers({
toEqualData: function(expected) {
return angular.equals(this.actual, expected);
}
});
});
beforeEach(module('phonecatApp'));
beforeEach(module('phonecatServices'));
describe('PhoneListCtrl', function(){
var scope, ctrl, $httpBackend;
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
$httpBackend.expectGET('phones/phones.json').
respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
scope = $rootScope.$new();
ctrl = $controller('PhoneListCtrl', {$scope: scope});
}));
it('should create "phones" model with 2 phones fetched from xhr', function() {
expect(scope.phones).toEqualData([]);
$httpBackend.flush();
expect(scope.phones).toEqualData(
[{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
});
it('should set the default value of orderProp model', function() {
expect(scope.orderProp).toBe('age');
});
});
describe('PhoneDetailCtrl', function(){
var scope, $httpBackend, ctrl,
xyzPhoneData = function() {
return {
name: 'phone xyz',
images: ['image/url1.png', 'image/url2.png']
}
};
beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) {
$httpBackend = _$httpBackend_;
$httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData());
$routeParams.phoneId = 'xyz';
scope = $rootScope.$new();
ctrl = $controller('PhoneDetailCtrl', {$scope: scope});
}));
it('should fetch phone detail', function() {
expect(scope.phone).toEqualData({});
$httpBackend.flush();
expect(scope.phone).toEqualData(xyzPhoneData());
});
});
});
```
The call to `beforeEach(module('core'))` loads the `core` module (which contains the `checkmark`
filter) into the injector, before every test.
Note that we call the helper function `inject(function(checkmarkFilter) {...})`, to get access to
the filter that we want to test. See also {@link angular.mock.inject angular.mock.inject()}.
<div class="alert alert-info">
When injecting a filter, we need to suffix the filter name with 'Filter'. For example, our
`checkmark` filter is injected as `checkmarkFilter`.
See the [Filters](guide/filter#using-filters-in-controllers-services-and-directives) section of
the Developer Guide for more info.
</div>
You should now see the following output in the Karma tab:
<pre>Chrome 22.0: Executed 5 of 5 SUCCESS (0.038 secs / 0.01 secs)</pre>
```
Chrome 49.0: Executed 4 of 4 SUCCESS (0.091 secs / 0.075 secs)
```
# Experiments
<div></div>
* Let's experiment with some of the {@link api/ng/filter built-in Angular filters}.
Add the following bindings to `index.html`:
* `{{'lower cap string' | uppercase}}`
* `{{{foo: 'bar', baz: 42} | json}}`
* `{{1459461289000 | date}}`
* `{{1459461289000 | date:'MM/dd/yyyy @ h:mma'}}`
* We can also create a model with an input element, and combine it with a filtered binding.
Add the following to `index.html`:
```html
<input ng-model="userInput" /> Uppercased: {{userInput | uppercase}}
```
# Summary
Now that we've seen how to build a custom service as a RESTful client, we're ready for {@link step_12 step 12} (the last step!) to
learn how to improve this application with animations.
Now that we have learned how to write and test a custom filter, let's go to {@link step_12 step 12}
to learn how we can use Angular to enhance the phone details page further.
<ul doc-tutorial-nav="11"></ul>
[restful]: http://en.wikipedia.org/wiki/Representational_State_Transfer
[jasmine-matchers]: http://jasmine.github.io/1.3/introduction.html#section-Matchers
[bower]: http://bower.io/
+131 -484
View File
@@ -1,538 +1,185 @@
@ngdoc tutorial
@name 12 - Applying Animations
@name 12 - Event Handlers
@step 12
@description
<ul doc-tutorial-nav="12"></ul>
In this final step, we will enhance our phonecat web application by attaching CSS and JavaScript
animations on top of the template code we created before.
In this step, you will add a clickable phone image swapper to the phone details page.
* The phone details view displays one large image of the current phone and several smaller thumbnail
images. It would be great if we could replace the large image with any of the thumbnails just by
clicking on the desired thumbnail image. Let's have a look at how we can do this with Angular.
* We now use the `ngAnimate` module to enable animations throughout the application.
* We also use common `ng` directives to automatically trigger hooks for animations to tap into.
* When an animation is found then the animation will run in between the standard DOM operation that
is being issued on the element at the given time (e.g. inserting and removing nodes on
{@link ngRepeat `ngRepeat`} or adding and removing classes on
{@link ngClass `ngClass`}).
<div doc-tutorial-reset="12"></div>
## Dependencies
The animation functionality is provided by Angular in the `ngAnimate` module, which is distributed
separately from the core Angular framework. In addition we will use `jQuery` in this project to do
extra JavaScript animations.
## Component Controller
We are using [Bower][bower] to install client side dependencies. This step updates the
`bower.json` configuration file to include the new dependency:
```
{
"name": "angular-seed",
"description": "A starter project for AngularJS",
"version": "0.0.0",
"homepage": "https://github.com/angular/angular-seed",
"license": "MIT",
"private": true,
"dependencies": {
"angular": "1.4.x",
"angular-mocks": "1.4.x",
"jquery": "~2.2.3",
"bootstrap": "~3.1.1",
"angular-route": "1.4.x",
"angular-resource": "1.4.x",
"angular-animate": "1.4.x"
}
}
```
* `"angular-animate": "1.4.x"` tells bower to install a version of the
angular-animate component that is compatible with version 1.4.x.
* `"jquery": "~2.2.3"` tells bower to install the 2.2.3 version of jQuery. Note that this is not an
Angular library, it is the standard jQuery library. We can use bower to install a wide range of 3rd
party libraries.
We must ask bower to download and install this dependency. We can do this by running:
```
npm install
```
<div class="alert alert-warning">
**Warning:** If a new version of Angular has been released since you last ran `npm install`, then you may have a
problem with the `bower install` due to a conflict between the versions of angular.js that need to
be installed. If you get this then simply delete your `app/bower_components` folder before running
`npm install`.
</div>
<div class="alert alert-info">
**Note:** If you have bower installed globally then you can run `bower install` but for this project we have
preconfigured `npm install` to run bower for us.
</div>
## How Animations work with `ngAnimate`
To get an idea of how animations work with AngularJS, please read the
{@link guide/animations AngularJS Animation Guide} first.
## Template
The changes required within the HTML template code is to link the asset files which define the animations as
well as the `angular-animate.js` file. The animation module, known as {@link module:ngAnimate `ngAnimate`}, is
defined within `angular-animate.js` and contains the code necessary to make your application become animation
aware.
Here's what needs to be changed in the index file:
__`app/index.html`.__
```html
...
<!-- for CSS Transitions and/or Keyframe Animations -->
<link rel="stylesheet" href="css/animations.css">
...
<!-- jQuery is used for JavaScript animations (include this before angular.js) -->
<script src="bower_components/jquery/dist/jquery.js"></script>
...
<!-- required module to enable animation support in AngularJS -->
<script src="bower_components/angular-animate/angular-animate.js"></script>
<!-- for JavaScript Animations -->
<script src="js/animations.js"></script>
...
```
<div class="alert alert-error">
**Important:** Be sure to use jQuery version 2.1 or newer when using Angular 1.4; jQuery 1.x is
not officially supported.
Be sure to load jQuery before all AngularJS scripts, otherwise AngularJS won't detect jQuery and
animations will not work as expected.
</div>
Animations can now be created within the CSS code (`animations.css`) as well as the JavaScript code (`animations.js`).
But before we start, let's create a new module which uses the ngAnimate module as a dependency just like we did before
with `ngResource`.
## Module & Animations
__`app/js/animations.js`.__
<br />
**`app/phone-detail/phone-detail.component.js`:**
```js
angular.module('phonecatAnimations', ['ngAnimate']);
// ...
// this module will later be used to define animations
// ...
...
controller: ['$http', '$routeParams',
function PhoneDetailController($http, $routeParams) {
var self = this;
self.setImage = function setImage(imageUrl) {
self.mainImageUrl = imageUrl;
};
$http.get('phones/' + $routeParams.phoneId + '.json').then(function(response) {
self.phone = response.data;
self.setImage(self.phone.images[0]);
});
}
]
...
```
And now let's attach this module to our application module...
In the `phoneDetail` component's controller, we created the `mainImageUrl` model property and set
its default value to the first phone image URL.
__`app/js/app.js`.__
```js
// ...
angular.module('phonecatApp', [
'ngRoute',
'phonecatAnimations',
'phonecatControllers',
'phonecatFilters',
'phonecatServices',
]);
// ...
```
Now, the phonecat module is animation aware. Let's make some animations!
We also created a `setImage()` method (to be used as event handler), that will change the value of
`mainImageUrl`.
## Animating ngRepeat with CSS Transition Animations
## Component Template
We'll start off by adding CSS transition animations to our `ngRepeat` directive present on the `phone-list.html` page.
First let's add an extra CSS class to our repeated element so that we can hook into it with our CSS animation code.
__`app/partials/phone-list.html`.__
<br />
**`app/phone-detail/phone-detail.template.html`:**
```html
<!--
Let's change the repeater HTML to include a new CSS class
which we will later use for animations:
-->
<ul class="phones">
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp"
class="thumbnail phone-listing">
<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>
```
Notice how we added the `phone-listing` CSS class? This is all we need in our HTML code to get animations working.
Now for the actual CSS transition animation code:
__`app/css/animations.css`__
```css
.phone-listing.ng-enter,
.phone-listing.ng-leave,
.phone-listing.ng-move {
-webkit-transition: 0.5s linear all;
-moz-transition: 0.5s linear all;
-o-transition: 0.5s linear all;
transition: 0.5s linear all;
}
.phone-listing.ng-enter,
.phone-listing.ng-move {
opacity: 0;
height: 0;
overflow: hidden;
}
.phone-listing.ng-move.ng-move-active,
.phone-listing.ng-enter.ng-enter-active {
opacity: 1;
height: 120px;
}
.phone-listing.ng-leave {
opacity: 1;
overflow: hidden;
}
.phone-listing.ng-leave.ng-leave-active {
opacity: 0;
height: 0;
padding-top: 0;
padding-bottom: 0;
}
```
As you can see our `phone-listing` CSS class is combined together with the animation hooks that occur when items are
inserted into and removed from the list:
* The `ng-enter` class is applied to the element when a new phone is added to the list and rendered on the page.
* The `ng-move` class is applied when items are moved around in the list.
* The `ng-leave` class is applied when they're removed from the list.
The phone listing items are added and removed depending on the data passed to the `ng-repeat` attribute.
For example, if the filter data changes, the items will be animated in and out of the repeat list.
Something important to note is that when an animation occurs, two sets of CSS classes
are added to the element:
1. a "starting" class that represents the style at the beginning of the animation
2. an "active" class that represents the style at the end of the animation
The name of the starting class is the name of the event that is fired (like `enter`, `move` or `leave`) prefixed with
`ng-`. So an `enter` event will result in a class called `ng-enter`.
The active class name is the same as the starting class's but with an `-active` suffix.
This two-class CSS naming convention allows the developer to craft an animation, beginning to end.
In our example above, elements are expanded from a height of **0** to **120 pixels** when they're added to the
list and are collapsed back down to **0 pixels** before being removed from the list.
There's also a nice fade-in and fade-out effect that occurs at the same time. All of this is handled
by the CSS transition declarations at the top of the example code above.
Although most modern browsers have good support for [CSS transitions](http://caniuse.com/#feat=css-transitions)
and [CSS animations](http://caniuse.com/#feat=css-animation), IE9 and earlier do not.
If you want animations that are backwards-compatible with older browsers, consider using JavaScript-based animations,
which are described in detail below.
## Animating `ngView` with CSS Keyframe Animations
Next let's add an animation for transitions between route changes in {@link ngRoute.directive:ngView `ngView`}.
To start, let's add a new CSS class to our HTML like we did in the example above.
This time, instead of the `ng-repeat` element, let's add it to the element containing the `ng-view` directive.
In order to do this, we'll have to make some small changes to the HTML code so that we can have more control over our
animations between view changes.
__`app/index.html`.__
```html
<div class="view-container">
<div ng-view class="view-frame"></div>
</div>
```
With this change, the `ng-view` directive is nested inside a parent element with
a `view-container` CSS class. This class adds a `position: relative` style so that the positioning of the `ng-view`
is relative to this parent as it animates transitions.
With this in place, let's add the CSS for this transition animation to our `animations.css` file:
__`app/css/animations.css`.__
```css
.view-container {
position: relative;
}
.view-frame.ng-enter, .view-frame.ng-leave {
background: white;
position: absolute;
top: 0;
left: 0;
right: 0;
}
.view-frame.ng-enter {
-webkit-animation: 0.5s fade-in;
-moz-animation: 0.5s fade-in;
-o-animation: 0.5s fade-in;
animation: 0.5s fade-in;
z-index: 100;
}
.view-frame.ng-leave {
-webkit-animation: 0.5s fade-out;
-moz-animation: 0.5s fade-out;
-o-animation: 0.5s fade-out;
animation: 0.5s fade-out;
z-index:99;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@-moz-keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@-webkit-keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
@-moz-keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
@-webkit-keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
/* don't forget about the vendor-prefixes! */
```
Nothing crazy here! Just a simple fade in and fade out effect between pages. The only out of the
ordinary thing here is that we're using absolute positioning to position the next page (identified
via `ng-enter`) on top of the previous page (the one that has the `ng-leave` class) while performing
a cross fade animation in between. So as the previous page is just about to be removed, it fades out
while the new page fades in right on top of it.
Once the leave animation is over then element is removed and once the enter animation is complete
then the `ng-enter` and `ng-enter-active` CSS classes are removed from the element, causing it to rerender and
reposition itself with its default CSS code (so no more absolute positioning once the animation is
over). This works fluidly so that pages flow naturally between route changes without anything
jumping around.
The CSS classes applied (the start and end classes) are much the same as with `ng-repeat`. Each time
a new page is loaded the `ng-view` directive will create a copy of itself, download the template and
append the contents. This ensures that all views are contained within a single HTML element which
allows for easy animation control.
For more on CSS animations, see the
[Web Platform documentation](http://docs.webplatform.org/wiki/css/properties/animations).
## Animating `ngClass` with JavaScript
Let's add another animation to our application. Switching to our `phone-detail.html` page,
we see that we have a nice thumbnail swapper. By hovering over the thumbnails listed on the page,
the profile phone image changes. But how can we change this around to add animations?
Let's think about it first. Basically, when you hover over a thumbnail image, you're changing the
state of the profile image to reflect the newly selected thumbnail image.
The best way to specify state changes within HTML is to use classes.
Much like before, how we used a CSS class to specify an animation, this time the animation will
occur whenever the CSS class itself changes.
Whenever a new phone thumbnail is selected, the state changes and the `.active` CSS class is added
to the matching profile image and the animation plays.
Let's get started and tweak our HTML code on the `phone-detail.html` page first. Notice that we
have changed the way we display our large image:
__`app/partials/phone-detail.html`.__
```html
<!-- We're only changing the top of the file -->
<div class="phone-images">
<img ng-src="{{img}}"
class="phone"
ng-repeat="img in phone.images"
ng-class="{active:mainImageUrl==img}">
</div>
<h1>{{phone.name}}</h1>
<p>{{phone.description}}</p>
<img ng-src="{{$ctrl.mainImageUrl}}" class="phone" />
...
<ul class="phone-thumbs">
<li ng-repeat="img in phone.images">
<img ng-src="{{img}}" ng-mouseenter="setImage(img)">
<li ng-repeat="img in $ctrl.phone.images">
<img ng-src="{{img}}" ng-click="$ctrl.setImage(img)" />
</li>
</ul>
```
Just like with the thumbnails, we're using a repeater to display **all** the profile images as a
list, however we're not animating any repeat-related animations. Instead, we're keeping our eye on
the ng-class directive since whenever the `active` class is true then it will be applied to the
element and will render as visible. Otherwise, the profile image is hidden. In our case, there is
always one element that has the active class, and, therefore, there will always be one phone profile
image visible on screen at all times.
When the active class is added to the element, the `active-add` and the `active-add-active` classes
are added just before to signal AngularJS to fire off an animation. When removed, the
`active-remove` and the `active-remove-active` classes are applied to the element which in turn
trigger another animation.
To ensure that the phone images are displayed correctly when the page is first loaded we also tweak
the detail page CSS styles:
__`app/css/app.css`__
```css
.phone-images {
background-color: white;
width: 450px;
height: 450px;
overflow: hidden;
position: relative;
float: left;
}
...
img.phone {
float: left;
margin-right: 3em;
margin-bottom: 2em;
background-color: white;
padding: 2em;
height: 400px;
width: 400px;
display: none;
}
img.phone:first-child {
display: block;
}
```
We bound the `ngSrc` directive of the large image to the `$ctrl.mainImageUrl` property.
You may be thinking that we're just going to create another CSS-enabled animation.
Although we could do that, let's take the opportunity to learn how to create JavaScript-enabled
animations with the `animation()` module method.
We also registered an {@link ng.directive:ngClick ngClick} handler with thumbnail images. When a
user clicks on one of the thumbnail images, the handler will use the `$ctrl.setImage()` method
callback to change the value of the `$ctrl.mainImageUrl` property to the URL of the clicked
thumbnail image.
__`app/js/animations.js`.__
<img class="diagram" src="img/tutorial/tutorial_12.png">
# Testing
To verify this new feature, we added two E2E tests. One verifies that `mainImageUrl` is set to the
first phone image URL by default. The second test clicks on several thumbnail images and verifies
that the main image URL changes accordingly.
<br />
**`e2e-tests/scenarios.js`:**
```js
var phonecatAnimations = angular.module('phonecatAnimations', ['ngAnimate']);
...
phonecatAnimations.animation('.phone', function() {
describe('View: Phone detail', function() {
var animateUp = function(element, className, done) {
if(className != 'active') {
return;
}
element.css({
position: 'absolute',
top: 500,
left: 0,
display: 'block'
...
it('should display the first phone image as the main phone image', function() {
var mainImage = element(by.css('img.phone'));
expect(mainImage.getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
});
jQuery(element).animate({
top: 0
}, done);
it('should swap the main image when clicking on a thumbnail image', function() {
var mainImage = element(by.css('img.phone'));
var thumbnails = element.all(by.css('.phone-thumbs img'));
return function(cancel) {
if(cancel) {
element.stop();
}
};
}
thumbnails.get(2).click();
expect(mainImage.getAttribute('src')).toMatch(/img\/phones\/nexus-s.2.jpg/);
var animateDown = function(element, className, done) {
if(className != 'active') {
return;
}
element.css({
position: 'absolute',
left: 0,
top: 0
thumbnails.get(0).click();
expect(mainImage.getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
});
jQuery(element).animate({
top: -500
}, done);
});
return function(cancel) {
if(cancel) {
element.stop();
}
};
}
return {
addClass: animateUp,
removeClass: animateDown
};
});
...
```
Note that we're using [jQuery](http://jquery.com/) to implement the animation. jQuery
isn't required to do JavaScript animations with AngularJS, but we're going to use it because writing
your own JavaScript animation library is beyond the scope of this tutorial. For more on
`jQuery.animate`, see the [jQuery documentation](http://api.jquery.com/animate/).
You can now rerun the tests with `npm run protractor`.
The `addClass` and `removeClass` callback functions are called whenever a class is added or removed
on the element that contains the class we registered, which is in this case `.phone`. When the `.active`
class is added to the element (via the `ng-class` directive) the `addClass` JavaScript callback will
be fired with `element` passed in as a parameter to that callback. The last parameter passed in is the
`done` callback function. The purpose of `done` is so you can let Angular know when the JavaScript
animation has ended by calling it.
We also have to refactor one of our unit tests, because of the addition of the `mainImageUrl` model
property to the controller. As previously, we will use a mocked response.
The `removeClass` callback works the same way, but instead gets triggered when a class is removed
from the element.
<br />
**`app/phone-detail/phone-detail.component.spec.js`:**
Within your JavaScript callback, you create the animation by manipulating the DOM. In the code above,
that's what the `element.css()` and the `element.animate()` are doing. The callback positions the next
element with an offset of `500 pixels` and animates both the previous and the new items together by
shifting each item up `500 pixels`. This results in a conveyor-belt like animation. After the `animate`
function does its business, it calls `done`.
```js
...
Notice that `addClass` and `removeClass` each return a function. This is an **optional** function that's
called when the animation is cancelled (when another animation takes place on the same element)
as well as when the animation has completed. A boolean parameter is passed into the function which
lets the developer know if the animation was cancelled or not. This function can be used to
do any cleanup necessary for when the animation finishes.
describe('controller', function() {
var $httpBackend, ctrl
var xyzPhoneData = {
name: 'phone xyz',
images: ['image/url1.png', 'image/url2.png']
};
beforeEach(inject(function($componentController, _$httpBackend_, _$routeParams_) {
$httpBackend = _$httpBackend_;
$httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData);
...
}));
it('should fetch phone details', function() {
expect(ctrl.phone).toBeUndefined();
$httpBackend.flush();
expect(ctrl.phone).toEqual(xyzPhoneData);
});
});
...
```
Our unit tests should now be passing again.
# Experiments
<div></div>
* Similar to the `ngClick` directive, which binds an Angular expression to the `click` event, there
are built-in directives for all native events, such as `dblclick`, `focus`/`blur`, mouse and key
events, etc.
Let's add a new controller method to the `phoneDetail` component's controller:
```js
self.onDblclick = function onDblclick(imageUrl) {
alert('You double-clicked image: ' + imageUrl);
};
```
and add the following to the `<img>` element in `phone-detail.template.html`:
```html
<img ... ng-dblclick="$ctrl.onDblclick(img)" />
```
Now, whenever you double-click on a thumbnail, an alert pops-up. Pretty annoying!
# Summary
There you have it! We have created a web app in a relatively short amount of time. In the {@link
the_end closing notes} we'll cover where to go from here.
With the phone image swapper in place, we are ready for {@link step_13 step 13} to learn an even
better way to fetch data.
<ul doc-tutorial-nav="12"></ul>
[bower]: http://bower.io/
+321
View File
@@ -0,0 +1,321 @@
@ngdoc tutorial
@name 13 - REST and Custom Services
@step 13
@description
<ul doc-tutorial-nav="13"></ul>
In this step, we will change the way our application fetches data.
* We define a custom service that represents a [RESTful][restful] client. Using this client we can
make requests for data to the server in an easier way, without having to deal with the lower-level
{@link ng.$http $http} API, HTTP methods and URLs.
<div doc-tutorial-reset="13"></div>
## Dependencies
The RESTful functionality is provided by Angular in the {@link ngResource ngResource} module, which
is distributed separately from the core Angular framework.
Since we are using [Bower][bower] to install client-side dependencies, this step updates the
`bower.json` configuration file to include the new dependency:
<br />
**`bower.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.5.x",
"angular-mocks": "1.5.x",
"angular-resource": "1.5.x",
"angular-route": "1.5.x",
"bootstrap": "3.3.x"
}
}
```
The new dependency `"angular-resource": "1.5.x"` tells bower to install a version of the
angular-resource module that is compatible with version 1.5.x of Angular. We must tell bower to
download and install this dependency.
```
npm install
```
<div class="alert alert-info">
**Note:** If you have bower installed globally, you can run `bower install`, but for this project
we have preconfigured `npm install` to run bower for us.
</div>
<div class="alert alert-warning">
**Warning:** If a new version of Angular has been released since you last ran `npm install`, then
you may have a problem with the `bower install` due to a conflict between the versions of
angular.js that need to be installed. If you run into this issue, simply delete your
`app/bower_components` directory and then run `npm install`.
</div>
## Service
We create our own service to provide access to the phone data on the server. We will put the service
in its own module, under `core`, so we can explicitly declare its dependency on `ngResource`:
<br />
**`app/core/phone/phone.module.js`:**
```js
angular.module('core.phone', ['ngResource']);
```
<br />
**`app/core/phone/phone.service.js`:**
```js
angular.
module('core.phone').
factory('Phone', ['$resource',
function($resource) {
return $resource('phones/:phoneId.json', {}, {
query: {
method: 'GET',
params: {phoneId: 'phones'},
isArray: true
}
});
}
]);
```
We used the {@link angular.Module module API} to register a custom service using a factory function.
We passed in the name of the service &mdash; `'Phone'` &mdash; and the factory function. The factory
function is similar to a controller's constructor in that both can declare dependencies to be
injected via function arguments. The `Phone` service declares a dependency on the `$resource`
service, provided by the `ngResource` module.
The {@link ngResource.$resource $resource} service makes it easy to create a [RESTful][restful]
client with just a few lines of code. This client can then be used in our application, instead of
the lower-level {@link ng.$http $http} service.
<br />
**`app/core/core.module.js`:**
```js
angular.module('core', ['core.phone']);
```
We need to add the `core.phone` module as a dependency of the `core` module.
## Template
Our custom resource service will be defined in `app/core/phone/phone.service.js`, so we need to
include this file and the associated `.module.js` file in our layout template. Additionally, we also
need to load the `angular-resource.js` file, which contains the `ngResource` module:
<br />
**`app/index.html`:**
```html
<head>
...
<script src="bower_components/angular-resource/angular-resource.js"></script>
...
<script src="core/phone/phone.module.js"></script>
<script src="core/phone/phone.service.js"></script>
...
</head>
```
## Component Controllers
We can now simplify our component controllers (`PhoneListController` and `PhoneDetailController`) by
factoring out the lower-level `$http` service, replacing it with the new `Phone` service. Angular's
`$resource` service is easier to use than `$http` for interacting with data sources exposed as
RESTful resources. It is also easier now to understand what the code in our controllers is doing.
<br />
**`app/phone-list/phone-list.module.js`:**
```js
angular.module('phoneList', ['core.phone']);
```
<br />
**`app/phone-list/phone-list.component.js`:**
```js
angular.
module('phoneList').
component('phoneList', {
templateUrl: 'phone-list/phone-list.template.html',
controller: ['Phone',
function PhoneListController(Phone) {
this.phones = Phone.query();
this.orderProp = 'age';
}
]
});
```
<br />
**`app/phone-detail/phone-detail.module.js`:**
```js
angular.module('phoneDetail', ['core.phone']);
```
<br />
**`app/phone-detail/phone-detail.component.js`:**
```js
angular.
module('phoneDetail').
component('phoneDetail', {
templateUrl: 'phone-detail/phone-detail.template.html',
controller: ['$routeParams', 'Phone',
function PhoneDetailController($routeParams, Phone) {
var self = this;
self.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) {
self.setImage(phone.images[0]);
});
self.setImage = function setImage(imageUrl) {
self.mainImageUrl = imageUrl;
};
}
]
});
```
Notice how in `PhoneListController` we replaced:
```js
$http.get('phones/phones.json').then(function(response) {
self.phones = response.data;
});
```
with just:
```js
this.phones = Phone.query();
```
This is a simple and declarative statement that we want to query for all phones.
An important thing to notice in the code above is that we don't pass any callback functions, when
invoking methods of our `Phone` service. Although it looks as if the results were returned
synchronously, that is not the case at all. What is returned synchronously is a "future" — an
object, which will be filled with data, when the XHR response is received. Because of the
data-binding in Angular, we can use this future and bind it to our template. Then, when the data
arrives, the view will be updated automatically.
Sometimes, relying on the future object and data-binding alone is not sufficient to do everything
we require, so in these cases, we can add a callback to process the server response. The
`phoneDetail` component's controller illustrates this by setting the `mainImageUrl` in a callback.
## Testing
Because we are now using the {@link ngResource ngResource} module, it is necessary to update the
Karma configuration file with angular-resource.
<br />
**`karma.conf.js`:**
```js
files: [
'bower_components/angular/angular.js',
'bower_components/angular-resource/angular-resource.js',
...
],
```
We have added a unit test to verify that our new service is issuing HTTP requests and returns the
expected "future" objects/arrays.
The {@link ngResource.$resource $resource} service augments the response object with extra methods
&mdash; e.g. for updating and deleting the resource &mdash; and properties (some of which are only
meant to be accessed by Angular). If we were to use Jasmine's standard `.toEqual()` matcher, our
tests would fail, because the test values would not match the responses exactly.
To solve the problem, we instruct Jasmine to use a [custom equality tester][jasmine-equality] for
comparing objects. We specify {@link angular.equals angular.equals} as our equality tester, which
ignores functions and `$`-prefixed properties, such as those added by the `$resource` service.<br />
(Remember that Angular uses the `$` prefix for its proprietary API.)
<br />
**`app/core/phone/phone.service.spec.js`:**
```js
describe('Phone', function() {
...
var phonesData = [...];
// Add a custom equality tester before each test
beforeEach(function() {
jasmine.addCustomEqualityTester(angular.equals);
});
// Load the module that contains the `Phone` service before each test
...
// Instantiate the service and "train" `$httpBackend` before each test
...
// Verify that there are no outstanding expectations or requests after each test
afterEach(function () {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should fetch the phones data from `/phones/phones.json`', function() {
var phones = Phone.query();
expect(phones).toEqual([]);
$httpBackend.flush();
expect(phones).toEqual(phonesData);
});
});
```
Here we are using `$httpBackend`'s
{@link ngMock.$httpBackend#verifyNoOutstandingExpectation verifyNoOutstandingExpectation()} and
{@link ngMock.$httpBackend#verifyNoOutstandingExpectation verifyNoOutstandingRequest()} methods to
verify that all expected requests have been sent and that no extra request is scheduled for later.
Note that we have also modified our component tests to use the custom matcher when appropriate.
You should now see the following output in the Karma tab:
```
Chrome 49.0: Executed 5 of 5 SUCCESS (0.123 secs / 0.104 secs)
```
# Summary
Now that we have seen how to build a custom service as a RESTful client, we are ready for
{@link step_14 step 14} to learn how to enhance the user experience with animations.
<ul doc-tutorial-nav="13"></ul>
[bower]: http://bower.io/
[jasmine-equality]: https://jasmine.github.io/2.4/custom_equality.html
[restful]: https://en.wikipedia.org/wiki/Representational_State_Transfer
+564
View File
@@ -0,0 +1,564 @@
@ngdoc tutorial
@name 14 - Animations
@step 14
@description
<ul doc-tutorial-nav="14"></ul>
In this step, we will enhance our web application by adding CSS and JavaScript animations on top of
the template code we created earlier.
* We now use the {@link ngAnimate ngAnimate} module to enable animations throughout the application.
* We also rely on built-in directives to automatically trigger hooks for animations to tap into.
* When an animation is found, it will run along with the actual DOM operation that is being issued
on the element at the given time (e.g. inserting/removing nodes on {@link ngRepeat ngRepeat} or
adding/removing classes on {@link ngClass ngClass}).
<div doc-tutorial-reset="14"></div>
## Dependencies
The animation functionality is provided by Angular in the `ngAnimate` module, which is distributed
separately from the core Angular framework. In addition we will use [jQuery][jquery] in this project
to do extra JavaScript animations.
Since we are using [Bower][bower] to install client-side dependencies, this step updates the
`bower.json` configuration file to include the new dependencies:
<br />
**`bower.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.5.x",
"angular-animate": "1.5.x",
"angular-mocks": "1.5.x",
"angular-resource": "1.5.x",
"angular-route": "1.5.x",
"bootstrap": "3.3.x",
"jquery": "2.2.x"
}
}
```
* `"angular-animate": "1.5.x"` tells bower to install a version of the angular-animate module that
is compatible with version 1.5.x of Angular.
* `"jquery": "2.2.x"` tells bower to install the latest patch release of the 2.2 version of jQuery.
Note that this is not an Angular library; it is the standard jQuery library. We can use bower to
install a wide range of 3rd party libraries.
Now, we must tell bower to download and install these dependencies.
```
npm install
```
<div class="alert alert-info">
**Note:** If you have bower installed globally, you can run `bower install`, but for this project
we have preconfigured `npm install` to run bower for us.
</div>
<div class="alert alert-warning">
**Warning:** If a new version of Angular has been released since you last ran `npm install`, then
you may have a problem with the `bower install` due to a conflict between the versions of
angular.js that need to be installed. If you run into this issue, simply delete your
`app/bower_components` directory and then run `npm install`.
</div>
## How Animations work with `ngAnimate`
To get an idea of how animations work with AngularJS, you might want to read the
[Animations](guide/animations) section of the Developer Guide first.
## Template
In order to enable animations, we need to update `index.html`, loading the necessary dependencies
(**angular-animate.js** and **jquery.js**) and the files that contain the CSS and JavaScript code
used in CSS/JavaScript animations. The animation module, {@link ngAnimate ngAnimate}, contains the
code necessary to make your application "animation aware".
<br />
**`app/index.html`:**
```html
...
<!-- Defines CSS necessary for animations -->
<link rel="stylesheet" href="app.animations.css" />
...
<!-- Used for JavaScript animations (include this before angular.js) -->
<script src="bower_components/jquery/dist/jquery.js"></script>
...
<!-- Adds animation support in AngularJS -->
<script src="bower_components/angular-animate/angular-animate.js"></script>
<!-- Defines JavaScript animations -->
<script src="app.animations.js"></script>
...
```
<div class="alert alert-error">
**Important:** Be sure to use jQuery version 2.1 or newer, when using Angular 1.5; jQuery 1.x is
not officially supported.
In order for Angular to detect jQuery and take advantage of it, make sure to include `jquery.js`
before `angular.js`.
</div>
Animations can now be created within the CSS code (`app.animations.css`) as well as the JavaScript
code (`app.animations.js`).
## Dependencies
We need to add a dependency on `ngAnimate` to our main module first:
<br />
**`app/app.module.js`:**
```js
angular.
module('phonecatApp', [
'ngAnimate',
...
]);
```
Now that our application is "animation aware", let's create some fancy animations!
## CSS Transition Animations: Animating `ngRepeat`
We will start off by adding CSS transition animations to our `ngRepeat` directive present on the
`phoneList` component's template. We need to add an extra CSS class to our repeated element, in
order to be able to hook into it with our CSS animation code.
<br />
**`app/phone-list/phone-list.template.html`:**
```html
...
<ul class="phones">
<li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp"
class="thumbnail phone-list-item">
<a href="#!/phones/{{phone.id}}" class="thumb">
<img ng-src="{{phone.imageUrl}}" alt="{{phone.name}}" />
</a>
<a href="#!/phones/{{phone.id}}">{{phone.name}}</a>
<p>{{phone.snippet}}</p>
</li>
</ul>
...
```
Did you notice the added `phone-list-item` CSS class? This is all we need in our HTML code to get
animations working.
Now for the actual CSS transition animation code:
<br />
**`app/app.animations.css`:**
```css
.phone-list-item.ng-enter,
.phone-list-item.ng-leave,
.phone-list-item.ng-move {
transition: 0.5s linear all;
}
.phone-list-item.ng-enter,
.phone-list-item.ng-move {
height: 0;
opacity: 0;
overflow: hidden;
}
.phone-list-item.ng-enter.ng-enter-active,
.phone-list-item.ng-move.ng-move-active {
height: 120px;
opacity: 1;
}
.phone-list-item.ng-leave {
opacity: 1;
overflow: hidden;
}
.phone-list-item.ng-leave.ng-leave-active {
height: 0;
opacity: 0;
padding-bottom: 0;
padding-top: 0;
}
```
As you can see, our `phone-list-item` CSS class is combined together with the animation hooks that
occur when items are inserted into and removed from the list:
* The `ng-enter` class is applied to the element when a new phone is added to the list and rendered
on the page.
* The `ng-move` class is applied to the element when a phone's relative position in the list
changes.
* The `ng-leave` class is applied to the element when a phone is removed from the list.
The phone list items are added and removed based on the data passed to the `ngRepeat` directive.
For example, if the filter data changes, the items will be animated in and out of the repeat list.
Something important to note is that, when an animation occurs, two sets of CSS classes are added to
the element:
1. A "starting" class that represents the style at the beginning of the animation.
2. An "active" class that represents the style at the end of the animation.
The name of the starting class is the name of the event that is fired (like `enter`, `move` or
`leave`) prefixed with `ng-`. So an `enter` event will result in adding the `ng-enter` class.
The active class name is derived from the starting class by appending an `-active` suffix.
This two-class CSS naming convention allows the developer to craft an animation, beginning to end.
In the example above, animated elements are expanded from a height of **0px** to **120px** when they
are added to the list and are collapsed back down to **0px** before being removed from the list.
There is also a catchy fade-in/fade-out effect that occurs at the same time. All this is handled by
the CSS transition declaration at the top of the CSS file.
<div class="alert alert-warning">
Although all modern browsers have good support for [CSS transitions][caniuse-css-transitions] and
[CSS animations][caniuse-css-animation], IE9 and earlier IE versions do not.
If you want animations that are backwards-compatible with older browsers, consider using
JavaScript-based animations, which are demonstrated below.
</div>
## CSS Keyframe Animations: Animating `ngView`
Next, let's add an animation for transitions between route changes in
{@link ngRoute.directive:ngView ngView}.
Again, we need to prepare our HTML template by adding a new CSS class, this time to the `ng-view`
element. In order to gain more "expressive power" for our animations, we will also wrap the
`[ng-view]` element in a container element.
<br />
**`app/index.html`:**
```html
<div class="view-container">
<div ng-view class="view-frame"></div>
</div>
```
We have applied a `position: relative` CSS style to the `.view-container` wrapper, so that it is
easier for us to manage the `.view-frame` element's positioning during the animation.
With our preparation code in place, let's move on to the actual CSS styles for this transition
animation.
<br />
**`app/app.animations.css`:**
```css
...
.view-container {
position: relative;
}
.view-frame.ng-enter,
.view-frame.ng-leave {
background: white;
left: 0;
position: absolute;
right: 0;
top: 0;
}
.view-frame.ng-enter {
animation: 1s fade-in;
z-index: 100;
}
.view-frame.ng-leave {
animation: 1s fade-out;
z-index: 99;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
/* Older browsers might need vendor-prefixes for keyframes and animation! */
```
Nothing fancy here! Just a simple fade-in/fade-out effect between pages. The only thing out of the
ordinary here is that we are using absolute positioning to position the entering page (identified
by the `ng-enter` class) on top of the leaving page (identified by the `ng-leave` class). At the
same time a cross-fade animation is performed. So, as the previous page is just about to be removed,
it fades out, while the new page fades in right on top of it.
Once the `leave` animation is over, the element is removed from the DOM. Likewise, once the `enter`
animation is complete, the `ng-enter` and `ng-enter-active` CSS classes are removed from the
element, causing it to rerender and reposition itself with its default CSS styles (so no more
absolute positioning once the animation is over). This works fluidly and the pages flow naturally
between route changes, without anything jumping around.
The applied CSS classes are much the same as with `ngRepeat`. Each time a new page is loaded the
`ngView` directive will create a copy of itself, download the template and append the contents. This
ensures that all views are contained within a single HTML element, which allows for easy animation
control.
For more on CSS animations, see the [Web Platform documentation][webplatform-animations].
## Animating `ngClass` with JavaScript
Let's add another animation to our application. On our `phone-detail.template.html` view, we have a
nice thumbnail swapper. By clicking on the thumbnails listed on the page, the profile phone image
changes. But how can we incorporate animations?
Let's give it some thought first. Basically, when a user clicks on a thumbnail image, they are
changing the state of the profile image to reflect the newly selected thumbnail image. The best way
to specify state changes within HTML is to use classes. Much like before &mdash; when we used a CSS
class to drive the animation &mdash; this time the animation will occur when the CSS class itself
changes.
Every time a phone thumbnail is selected, the state changes and the `.selected` CSS class is added
to the matching profile image. This will trigger the animation.
We will start by tweaking our HTML code in `phone-detail.template.html`. Notice that we have changed
the way we display our large image:
<br />
**`app/phone-detail/phone-detail.template.html`:**
```html
<div class="phone-images">
<img ng-src="{{img}}" class="phone"
ng-class="{selected: img === $ctrl.mainImageUrl}"
ng-repeat="img in $ctrl.phone.images" />
</div>
...
```
Just like with the thumbnails, we are using a repeater to display **all** the profile images as a
list, however we're not animating any repeat-related transitions. Instead, we will be keeping our
eye on each element's classes and especially the `selected` class, since its presence or absence
will determine if the element is visible or hidden. The addition/removal of the `selected` class is
managed by the {@link ngClass ngClass} directive, based on the specified condition
(`img === $ctrl.mainImageUrl`).
In our case, there is always exactly one element that has the `selected` class, and therefore there
will be exactly one phone profile image visible on the screen at all times.
When the `selected` class is added to an element, the `selected-add` and `selected-add-active`
classes are added just before to signal AngularJS to fire off an animation. When the `selected`
class is removed from an element, the `selected-remove` and `selected-remove-active` classes are
applied to the element, triggering another animation.
Finally, in order to ensure that the phone images are displayed correctly when the page is first
loaded, we also tweak the detail page CSS styles:
<br />
**`app/app.css`:**
```css
...
.phone {
background-color: white;
display: none;
float: left;
height: 400px;
margin-bottom: 2em;
margin-right: 3em;
padding: 2em;
width: 400px;
}
.phone:first-child {
display: block;
}
.phone-images {
background-color: white;
float: left;
height: 450px;
overflow: hidden;
position: relative;
width: 450px;
}
...
```
You may be thinking that we are just going to create another CSS-based animation. Although we could
do that, let's take the opportunity to learn how to create JavaScript-based animations with the
{@link ng.angular.Module#animation .animation()} module method.
<br />
**`app/app.animations.js`:**
```js
angular.
module('phonecatApp').
animation('.phone', function phoneAnimationFactory() {
return {
addClass: animateIn,
removeClass: animateOut
};
function animateIn(element, className, done) {
if (className !== 'selected') return;
element.
css({
display: 'block',
position: 'absolute',
top: 500,
left: 0
}).
animate({
top: 0
}, done);
return function animateInEnd(wasCanceled) {
if (wasCanceled) element.stop();
};
}
function animateOut(element, className, done) {
if (className !== 'selected') return;
element.
css({
position: 'absolute',
top: 0,
left: 0
}).
animate({
top: -500
}, done);
return function animateOutEnd(wasCanceled) {
if (wasCanceled) element.stop();
};
}
});
```
We are creating a custom animation by specifying the target elements via a CSS class selector (here
`.phone`) and an animation _factory_ function (here `phoneAnimationFactory()`). The factory function
returns an object associating specific _events_ (object keys) to animation _callbacks_ (object
values). The _events_ correspond to DOM actions that `ngAnimate` recognizes and can hook into, such
as `addClass`/`removeClass`/`setClass`, `enter`/`move`/`leave` and `animate`. The associated
callbacks are called by `ngAnimate` at appropriate times.
For more info on animation factories, check out the
{@link ng.$animateProvider#register API Reference}.
In this case, we are interested in a class getting added to/removed from a `.phone` element, thus we
specify callbacks for the `addClass` and `removeClass` events. When the `selected` class is added to
an element (via the `ngClass` directive), the `addClass` JavaScript callback will be executed with
`element` passed in as a parameter. The last parameter passed in is the `done` callback function. We
call `done()` to let Angular know that our custom JavaScript animation has ended. The `removeClass`
callback works the same way, but instead gets executed when a class is removed.
Note that we are using [jQuery][jquery]'s `animate()` helper to implement the animation. jQuery
isn't required to do JavaScript animations with AngularJS, but we use it here anyway in order to
keep the example simple. More info on `jQuery.animate()` can be found in the
[jQuery documentation][jquery-animate].
Within the event callbacks, we create the animation by manipulating the DOM. In the code above,
this is achieved using `element.css()` and `element.animate()`. As a result the new element is
positioned with an offset of **500px** and then both elements &mdash; the previous and the new
&mdash; are animated together by shifting each one up by **500px**. The outcome is a conveyor-belt
like animation. After the `animate()` function has completed the animation, it calls `done` to
notify Angular.
You may have noticed that each animation callback returns a function. This is an **optional**
function, which (if provided) will be called when the animation ends, either because it ran to
completion or because it was canceled (for example another animation took place on the same
element). A boolean parameter (`wasCanceled`) is passed to the function, letting the developer know
if the animation was canceled or not. Use this function to do any necessary clean-up.
# Experiments
<div></div>
* Reverse the animation, so that the elements animate downwards.
* Make the animation run faster or slower, by passing a `duration` argument to `.animate()`:
```js
element.css({...}).animate({...}, 1000 /* 1 second */, done);
```
* Make the animations "asymmetrical". For example, have the previous element fade out, while the new
element zooms in:
```js
// animateIn()
element.css({
display: 'block',
opacity: 1,
position: 'absolute',
width: 0,
height: 0,
top: 200,
left: 200
}).animate({
width: 400,
height: 400,
top: 0,
left: 0
}, done);
// animateOut()
element.animate({
opacity: 0
}, done);
```
* Go crazy and come up with your own funky animations!
# Summary
Our application is now much more pleasant to use, thanks to the smooth transitions between pages
and UI states.
There you have it! We have created a web application in a relatively short amount of time. In the
{@link the_end closing notes} we will cover where to go from here.
<ul doc-tutorial-nav="14"></ul>
[bower]: http://bower.io/
[caniuse-css-animation]: http://caniuse.com/#feat=css-animation
[caniuse-css-transitions]: http://caniuse.com/#feat=css-transitions
[jquery]: https://jquery.com/
[jquery-animate]: https://api.jquery.com/animate/
[webplatform-animations]: https://docs.webplatform.org/wiki/css/properties/animations
+13 -6
View File
@@ -9,11 +9,18 @@ previous steps using the `git checkout` command.
For more details and examples of the Angular concepts we touched on in this tutorial, see the
{@link guide/ Developer Guide}.
When you are ready to start developing a project using Angular, we recommend that you bootstrap
your development with the [angular-seed](https://github.com/angular/angular-seed) project.
When you are ready to start developing a project using AngularJS, we recommend that you bootstrap
your development with the [angular-seed project][angular-seed].
We hope this tutorial was useful to you and that you learned enough about Angular to make you want
to learn more. We especially hope you are inspired to go out and develop Angular web apps of your
own, and that you might be interested in {@link misc/contribute contributing} to Angular.
We hope this tutorial was useful to you and that you learned enough about AngularJS to make you want
to learn more. We especially hope you are inspired to go out and develop Angular web applications of
your own, and that you might be interested in {@link misc/contribute contributing} to AngularJS.
If you have questions or feedback or just want to say "hi", please post a message at (https://groups.google.com/forum/#!forum/angular).
If you have questions or feedback or just want to say "hi", please post a message to the
[mailing list][mailing-list]. You can also find us on [IRC][irc] or [Gitter][gitter].
[angular-seed]: https://github.com/angular/angular-seed
[gitter]: https://gitter.im/angular/angular.js
[irc]: http://webchat.freenode.net/?channels=angularjs&uio=d4
[mailing-list]: https://groups.google.com/forum/#!forum/angular
Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 24 KiB

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 58 KiB

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 27 KiB

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 26 KiB

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 65 KiB

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 72 KiB