mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-17 07:40:22 +00:00
new batch of tutorial docs
This commit is contained in:
parent
11e9572b95
commit
6181ca600d
13 changed files with 1900 additions and 1588 deletions
|
|
@ -1,4 +1,3 @@
|
|||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Tutorial
|
||||
@description
|
||||
|
|
@ -6,63 +5,56 @@
|
|||
A great way to get introduced to angular is to work through the {@link tutorial.step_00 angular
|
||||
tutorial}, which walks you through the construction of an angular web app. The app you will build
|
||||
in the tutorial is loosely based on the {@link http://www.google.com/phone/ Google phone gallery
|
||||
app}. The {@link http://angular.github.com/angular-phonecat/step-11/app/ end result of our effort}
|
||||
is visually simpler, but demonstrates many of the angular features without distractions in the
|
||||
form of CSS code.
|
||||
app}. The {@link http://angular.github.com/angular-phonecat/step-11/app/#/phones end result of our
|
||||
effort} is visually simpler, but demonstrates many of the angular features without distractions in
|
||||
the form of CSS code.
|
||||
|
||||
This tutorial app ends up like a Google phone gallery app, but is originally based on the {@link
|
||||
https://github.com/angular/angular-seed angular-seed project}. The angular seed app isn't
|
||||
necessary for building angular apps, but it helps you get started quickly and makes the
|
||||
development and testing process much easier. Angular-seed includes a simple example, the latest
|
||||
angular libraries, test libraries, and scripts. It provides all of these in an environment that
|
||||
is pre-configured for developing a typical web app.
|
||||
The starting point for our tutorial is the {@link https://github.com/angular/angular-seed
|
||||
angular-seed project}.
|
||||
|
||||
Once you set up your tutorial environment, you should be able to get through the material in less
|
||||
than a day and you'll have fun doing it. More experienced coders may be able to zip through the
|
||||
exercises in an afternoon. In any case, we promise that your time will be well spent!
|
||||
The angular-seed project includes a simple example app, the latest angular libraries, test
|
||||
libraries, and scripts. It provides all of these in an environment that is pre-configured for
|
||||
developing a typical web app. For this tutorial, we modified the angular-seed as follows:
|
||||
|
||||
* Removed the example app
|
||||
* Added phone images to `app/img/phones`
|
||||
* Added phone data files (JSON) to `app/phones`
|
||||
|
||||
Note: Using the angular seed app isn't required for building angular apps, but doing so helps
|
||||
you get started quickly and makes the development and testing process much easier.
|
||||
|
||||
When you finish the tutorial you will be able to:
|
||||
|
||||
* Create a simple dynamic application that works in any browser
|
||||
* Define the differences between angular and common JavaScript frameworks
|
||||
* Understand angular expressions
|
||||
* Understand how data binding works in angular
|
||||
* Use the angular-seed project to quickly boot-strap your own projects
|
||||
* Create and run tests
|
||||
* Identify resources for learning more about angular
|
||||
|
||||
You can work through the tutorial in any of the following ways:
|
||||
Mac and Linux users can work through the tutorial, run tests, and experiment with the code using
|
||||
Git or the snapshots described below. Windows users will be able follow the tutorial and read
|
||||
through the source code and view the application running on our servers at different stages.
|
||||
|
||||
* <a href="#UsingGit">Using Git</a>. Use the Git versioning system to get the files for each step.
|
||||
* <a href="#UsingSnapshots">Using Snapshots</a>. Download snapshots (files for each step of the
|
||||
tutorial) and tinker with them.
|
||||
* <a href="#ReadingExamples">Reading the Examples</a>. Read through the examples, and inspect
|
||||
results and code on our server.
|
||||
|
||||
The first two ways (Git and snapshots) give you a fuller experience, in that you can run the unit
|
||||
and end-to-end tests in addition to the tutorial app. They also give you the ability to play
|
||||
around with the code and get instant feedback in your browser. The last way (reading through the
|
||||
tutorial online) requires no setup on your machine, but you can't run the tests, and it won't be
|
||||
as easy to play around with the code.
|
||||
You can go through the whole tutorial in a couple of hours or you may want to spend a pleasant day
|
||||
really digging into it. In any case, we promise that your time will be well spent!
|
||||
|
||||
<a name="PreReqs"></a>
|
||||
# Prerequisites for Git and Snapshots
|
||||
# Prerequisites
|
||||
|
||||
To run the tutorial app and tests on your machine (using Git or the snapshots) you will need the
|
||||
following:
|
||||
To run the tutorial app and tests on your machine you will need the following:
|
||||
|
||||
* You need to be running on a Mac or Linux machine.
|
||||
* A Mac or Linux machine (required by the tutorial scripts, not angular)
|
||||
* An http server running on your system. If you don't already have one installed, you can install
|
||||
`node.js` ({@link https://github.com/joyent/node/wiki/Installation node.js install}) or another
|
||||
http sever (such as Apache, etc.).
|
||||
* Java. This is required for running tests. Angular itself doesn't require Java.
|
||||
* A modern browser (including IE8+). Needed for viewing and debugging code.
|
||||
* A text editor of your choice.
|
||||
* Java. This is only required for if you want to run tests via JsTestDriver.
|
||||
* A web browser.
|
||||
* A text editor.
|
||||
|
||||
<a name="UsingGit"></a>
|
||||
# Using Git
|
||||
|
||||
The following instructions are for developers who are comfortable with Git's versioning system:
|
||||
The following instructions are for developers who are comfortable with Git versioning system:
|
||||
|
||||
1. Check to be sure you have all of the <a href="#PreReqs">prerequisites</a> on your system.
|
||||
|
||||
|
|
@ -70,40 +62,19 @@ The following instructions are for developers who are comfortable with Git's ver
|
|||
https://github.com/angular/angular-phonecat angular-phonecat} by running the following command in
|
||||
a terminal:
|
||||
|
||||
git clone git://github.com/angular/angular-phonecat.git
|
||||
git clone git://github.com/angular/angular-phonecat.git
|
||||
|
||||
This will create a directory called `angular-phonecat`.
|
||||
This will create a directory called `angular-phonecat` in the current directory.
|
||||
|
||||
3. In terminal, navigate to the `angular-phonecat` directory and run:
|
||||
3. Change your current directory to `angular-phonecat`.
|
||||
|
||||
git checkout step-0
|
||||
cd angular-phonecat
|
||||
|
||||
(You can run `git checkout step-[0-11]` to go to any of the steps in the tutorial).
|
||||
The tutorial instructions assume you are running all commands from this directory.
|
||||
|
||||
4. To see the app running in a browser, do the following:
|
||||
* __For node.js users:__
|
||||
1. Run `./scripts/web-server.js` to start the app server.
|
||||
2. Open a browser window for the app and navigate to http://localhost:8000/app/index.html.
|
||||
|
||||
* __For other http servers:__
|
||||
1. Configure the server to serve the files in the `angular-phonecat` directory.
|
||||
2. Run `./scripts/web-server.js` to start the app server.
|
||||
3. Navigate in your browser to
|
||||
http://localhost:[*port-number*]/[*context-path*]/app/index.html.
|
||||
|
||||
5. To see tests running in a browser, do the following:
|
||||
* __For node.js users:__
|
||||
1. Run `./scripts/test-server.sh` to start the test web server.
|
||||
2. Open a browser window for the tests, navigate to http://localhost:9876, and choose
|
||||
"strict mode".
|
||||
* __For other http servers:__
|
||||
1. Configure the server to serve the files in the `angular-phonecat` directory.
|
||||
1. Run `./scripts/test-server.sh` to start the test web server.
|
||||
3. Navigate in your browser to http://localhost:[*port-number*]/, and choose "strict mode".
|
||||
Read the Tutorial Navigation section, then navigate to Step 0.
|
||||
|
||||
|
||||
|
||||
<a name="UsingSnapshots"></a>
|
||||
# Using Snapshots
|
||||
|
||||
Snapshots are the sets of files that reflect the state of the tutorial app at each step. These
|
||||
|
|
@ -113,60 +84,18 @@ knowledge of Git. You can download and install the snapshot files as follows:
|
|||
|
||||
1. Check to be sure you have all of the <a href="#PreReqs">prerequisites</a> on your system.
|
||||
|
||||
2. Navigate to [*the angular server*], and download and unzip [*the snapshot file*] to an
|
||||
[*install-dir*] of your choosing.
|
||||
2. Navigate to [*the angular server*], download and then unzip [*the snapshot file*] to an
|
||||
[*install-dir*].
|
||||
|
||||
3. Change directories to [*install-dir*]/sandbox.
|
||||
|
||||
4. Run the following command:
|
||||
* `./goto_step.sh 0`
|
||||
cd [*install-dir*]/sandbox
|
||||
|
||||
You have to start out at the beginning, which is Step 0. After you set up Step 0, you can skip
|
||||
around between any steps.
|
||||
Read the Tutorial Navigation section, then navigate to step-0.
|
||||
|
||||
1. To see the app running in your browser, do the following:
|
||||
* __For node.js users:__
|
||||
1. Run `./scripts/web-server.js` to run the web server.
|
||||
2. Open a browser window for the app and navigate to http://localhost:8000/app/index.html.
|
||||
3. Open a browser window for the tests, navigate to http://localhost:9876, and choose
|
||||
"strict mode".
|
||||
# Tutorial Navigation
|
||||
|
||||
* __For other http servers:__
|
||||
1. Configure servers to serve the app and test files in the [*install-dir*]/sandbox.
|
||||
2. Start the server.
|
||||
3. Navigate in your app browser to
|
||||
http://localhost:[*port-number*]/[*context-path*]/app/index.html.
|
||||
4. Navigate in your test browser to http://localhost:[*port-number*] and choose "strict
|
||||
mode".
|
||||
|
||||
1. To view the tutorial app at different steps, run `./goto_step.sh [0-11]` and then refresh your
|
||||
browser. For example, say you're on Step 5 of the tutorial, and you want to see the app in action:
|
||||
|
||||
1. Run `goto_step.sh 5` from the command line in the `sandbox` directory.
|
||||
1. Refresh your app browser.
|
||||
|
||||
<a name="ReadingExamples"></a>
|
||||
# Reading the Examples
|
||||
|
||||
If you don't want to set up anything on your local machine, you can read through the tutorial and
|
||||
inspect the tutorial files on our servers; doing this will give you a good idea of what angular
|
||||
does, but you won't be able to make any code changes and experiment on your own.
|
||||
|
||||
To see the running app at each tutorial step, click the "Example" link at the top or bottom of
|
||||
each tutorial page.
|
||||
|
||||
To view the code differences between tutorial steps, click the Code Diff link at top or bottom of
|
||||
each tutorial page. Additions are highlighted in green; deletions are highlighted in red.
|
||||
|
||||
|
||||
# Relative URLs
|
||||
Throughout the tutorial, we use relative URLs to refer to files hosted on our local http server.
|
||||
The absolute URL depends on your configuration. For example, if you are using the node.js server,
|
||||
`app/index.html` translates to:
|
||||
|
||||
http://localhost:8000/app/index.html
|
||||
|
||||
If you are using your own http server running on port 8080 and the tutorial files are hosted at
|
||||
`/angular_tutorial`, `app/index.html` translates to:
|
||||
|
||||
http://localhost:8080/angular_tutorial/app/index.html
|
||||
To see the app running on the angular server, click the "Live Demo" link at the top or bottom of
|
||||
any tutorial page. To view the code differences between tutorial steps, click the Code Diff link
|
||||
at top or bottom of each tutorial page. In the Code Diff, additions are highlighted in green;
|
||||
deletions are highlighted in red.
|
||||
|
|
|
|||
|
|
@ -1,77 +1,90 @@
|
|||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 0
|
||||
@description
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-0/app Example}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">Code Diff</td>
|
||||
<td id="next_step">{@link tutorial.step_01 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
The following sample code is our starting point. It is a static HTML page that displays next to
|
||||
nothing, but it has everything we need to proceed. You can think of this bit of code as our
|
||||
prototype template, consisting of basic HTML tags with a pair of angular specific attributes.
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
<!doctype html>
|
||||
<html xmlns:ng="http://angularjs.org/">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>my angular app</title>
|
||||
<link rel="stylesheet" href="css/app.css"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Nothing here yet!
|
||||
|
||||
<script src="lib/angular/angular.js" ng:autobind></script>
|
||||
</body>
|
||||
</html>
|
||||
</pre>
|
||||
|
||||
## Discussion:
|
||||
|
||||
Although our app doesn't appear to do anything dynamic, note the following:
|
||||
|
||||
* __... `xmlns:ng="http://angularjs.org"` ...__ This `xmlns` declaration for the `ng` namespace
|
||||
must be specified if you use XHTML, or if you are targeting IE older than 9 (regardless of whether
|
||||
you are using XHTML or HTML).
|
||||
|
||||
* __`<script src="lib/angular/angular.js"` ...__ This downloads the `angular.js` script and
|
||||
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
|
||||
angular.directive.ng:autobind ng:autobind} attribute. If `ng:autobind` is found, it signals
|
||||
angular to bootstrap and compile and manage the whole html page.
|
||||
|
||||
Note: If you elected not to download any tutorial files but still want to try out some angular
|
||||
code on your system, you can change the relative path to the `angular.js` script in your
|
||||
template from `./lib/angular/angular.js` to the following:
|
||||
|
||||
<script src="http://code.angularjs.org/angular-0.9.14.js" ng:autobind></script>
|
||||
|
||||
This will download the angular script from the angular server instead of from a local file.
|
||||
|
||||
* To try this code out in your browser, you need to navigate to the step-0 page (you are currently
|
||||
on Step 0 of the tutorial). If your http server is running, navigate to `app/index.html`.
|
||||
Remember, this is a relative URL (see the Relative URL section in {@link tutorial Tutorial}). The
|
||||
browser will display the same thing as you would see if you go to
|
||||
http://angular.github.com/angular-phonecat/step-0/app (accessible from Example link at the bottom
|
||||
of the page).
|
||||
|
||||
Now we can move on and add some content to our developing web app.
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-0/app Example}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">Code Diff</td>
|
||||
<td id="next_step">{@link tutorial.step_01 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 0
|
||||
@description
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-0/app Live Demo}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">Code Diff</td>
|
||||
<td id="next_step">{@link tutorial.step_0 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
You are now ready to build the phone cat application. In this step, you will become familiar with
|
||||
the most important source code files, learn how to start the web services, and run the application
|
||||
in the browser.
|
||||
|
||||
1. Do one of the following:
|
||||
|
||||
* Git users: In the `angular-phonecat` directory, run this command:
|
||||
|
||||
git checkout step-0
|
||||
|
||||
* Snapshot users: In the `[install directory]/sandbox` directory, run this command:
|
||||
|
||||
./goto_step.sh 0
|
||||
|
||||
This resets your workspace to Step 0 of the tutorial app.
|
||||
|
||||
2. To see the app running in a browser, do one of the following:
|
||||
* __For node.js users:__
|
||||
1. In a _separate_ terminal tab or window, run `./scripts/web-server.js` to start the app
|
||||
server.
|
||||
2. Open a browser window for the app and navigate to http://localhost:8000/app/index.html.
|
||||
|
||||
* __For other http servers:__
|
||||
1. Configure the server to serve the files in the `angular-phonecat` directory.
|
||||
2. Run `./scripts/web-server.js` to start the app server.
|
||||
3. Navigate in your browser to
|
||||
http://localhost:[*port-number*]/[*context-path*]/app/index.html.
|
||||
|
||||
You can now see the app in the browser. It's not very exciting, but that's OK.
|
||||
|
||||
The code that created this app is shown below. You will see that it creates a static HTML page
|
||||
that displays "Nothing here yet!"; the code does, however, have everything we need to proceed.
|
||||
This bit of code serves as a prototype template, consisting of basic HTML tags with a pair of
|
||||
angular-specific attributes.
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
<!doctype html>
|
||||
<html xmlns:ng="http://angularjs.org/">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>my angular app</title>
|
||||
<link rel="stylesheet" href="css/app.css"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Nothing here yet!
|
||||
|
||||
<script src="lib/angular/angular.js" ng:autobind></script>
|
||||
</body>
|
||||
</html>
|
||||
</pre>
|
||||
|
||||
## What is the code doing?
|
||||
|
||||
* __... `xmlns:ng="http://angularjs.org"` ...__ This `xmlns` declaration for the `ng` namespace
|
||||
must be specified in all angular applications if you use XHTML, or if you are targeting IE
|
||||
versions older than 9 (regardless of whether you are using XHTML or HTML).
|
||||
|
||||
* __`<script src="lib/angular/angular.js"` ...__ This downloads the `angular.js` script and
|
||||
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
|
||||
angular.directive.ng:autobind ng:autobind} attribute. If `ng:autobind` is found, it signals
|
||||
angular to bootstrap and compile and manage the whole html page.
|
||||
|
||||
Now let's go to Step 1 and add some content to the web app.
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-0/app Live Demo}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">Code Diff</td>
|
||||
<td id="next_step">{@link tutorial.step_01 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -1,88 +1,69 @@
|
|||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 1
|
||||
@description
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_00 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-1/app Example}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">
|
||||
{@link https://github.com/angular/angular-phonecat/compare/step-0...step-1 Code Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_02 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
Now that we have the basic ingredients in place, let's add some basic information about two cell
|
||||
phones to our app.
|
||||
|
||||
Note: We will usually include only the new code that we added for each step. In this and
|
||||
subsequent examples, we will leave out code from the previous step that hasn't changed, for
|
||||
example:
|
||||
|
||||
...
|
||||
<html xmlns:ng="http://angularjs.org">
|
||||
...
|
||||
|
||||
Let's add the following code to `index.html`:
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
<head>
|
||||
...
|
||||
<title>Google Phone Gallery</title>
|
||||
...
|
||||
</head>
|
||||
...
|
||||
<ul>
|
||||
<li>
|
||||
<span>Nexus S<span>
|
||||
<p>
|
||||
Fast just got faster with Nexus S.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<span>Motorola XOOM™ with Wi-Fi<span>
|
||||
<p>
|
||||
The Next, Next Generation tablet.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
</pre>
|
||||
|
||||
## Discussion:
|
||||
|
||||
* It's a static web page! We displayed info about two phones! Yay.
|
||||
|
||||
* For those of you playing along at home on your own web servers, did you switch to Step 1 and
|
||||
refresh your browsers?
|
||||
|
||||
* __{@link tutorial Using Git:}__
|
||||
|
||||
From your `angular-phonecat` directory, run this command:
|
||||
|
||||
git checkout step-1
|
||||
|
||||
* __{@link tutorial Using Snapshots:}__
|
||||
|
||||
From `[install directory]/sandbox`, run this command:
|
||||
|
||||
./goto_step.sh 1
|
||||
|
||||
* Now would be a good time to open up `app/index.html` in your browser and see the current state
|
||||
of our "application". It's not very exciting, but that's ok.
|
||||
|
||||
When you're ready, let's move on and start using some angular features to turn this static page
|
||||
into a dynamic web app.
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_00 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-1/app Example}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">
|
||||
{@link https://github.com/angular/angular-phonecat/compare/step-0...step-1 Code Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_02 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 1
|
||||
@description
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_00 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-1/app Live
|
||||
Demo}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">
|
||||
{@link https://github.com/angular/angular-phonecat/compare/step-0...step-1 Code Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_02 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
In this step you will add some basic information about two cell phones to our app.
|
||||
|
||||
1. Do one of the following to reset your workspace to Step 1; be aware that this will throw away
|
||||
any changes you might have made to the tutorial files:
|
||||
|
||||
* Git users run:
|
||||
|
||||
git checkout --force step-1
|
||||
|
||||
* Snapshot users run:
|
||||
|
||||
./goto_step.sh 1
|
||||
|
||||
2. Refresh your browser or check the app out on {@link
|
||||
http://angular.github.com/angular-phonecat/step-1/app, our server}. Your page now contains a list
|
||||
with information about two phones.
|
||||
|
||||
The most important changes are listed below. You can see the full diff on {@link
|
||||
https://github.com/angular/angular-phonecat/compare/step-0...step-1 GitHub}:
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
<ul>
|
||||
<li>
|
||||
<span>Nexus S<span>
|
||||
<p>
|
||||
Fast just got faster with Nexus S.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<span>Motorola XOOM™ with Wi-Fi<span>
|
||||
<p>
|
||||
The Next, Next Generation tablet.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
</pre>
|
||||
|
||||
This addition to your app uses static HTML to display the list. Now, let's go to Step 2 to learn
|
||||
how to use angular to dynamically generate the same list.
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_00 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-1/app Live
|
||||
Demo}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">
|
||||
{@link https://github.com/angular/angular-phonecat/compare/step-0...step-1 Code Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_02 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -1,137 +1,174 @@
|
|||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 2
|
||||
@description
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_01 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-2/app Example}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-1...step-2 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_03 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
In the last step, we remembered what a basic, static web page looks like, and now we want to get
|
||||
dynamic. There are many ways to do this, but an important feature of angular is the incorporation
|
||||
of the principles behind {@link http://en.wikipedia.org/wiki/Model–View–Controller the MVC design
|
||||
pattern} into client-side web apps. With that in mind, let's use a little angular and a little
|
||||
JavaScript to add Model, View, and Controller components to our app, and change the static page
|
||||
into one that is dynamically generated.
|
||||
|
||||
Our __View__ component is constructed by angular from this template:
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
<body ng:controller="PhoneListCtrl">
|
||||
|
||||
<ul>
|
||||
<li ng:repeat="phone in phones">
|
||||
{{phone.name}}
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<script src="lib/angular/angular.js" ng:autobind></script>
|
||||
<script src="js/controllers.js"></script>
|
||||
</body>
|
||||
...
|
||||
</pre>
|
||||
|
||||
Our data __Model__ (a short list of phones in object literal notation) is instantiated within our
|
||||
__Controller__ function (`PhoneListCtrl`):
|
||||
|
||||
__`app/js/controllers.js`:__
|
||||
<pre>
|
||||
/* App Controllers */
|
||||
|
||||
function PhoneListCtrl() {
|
||||
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."}];
|
||||
}
|
||||
</pre>
|
||||
|
||||
The "Angular way" urges us to test as we develop:
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
<pre>
|
||||
/* jasmine specs for controllers go here */
|
||||
describe('PhoneCat controllers', function() {
|
||||
|
||||
describe('PhoneListCtrl', function(){
|
||||
|
||||
it('should create "phones" model with 3 phones', function() {
|
||||
var ctrl = new PhoneListCtrl();
|
||||
expect(ctrl.phones.length).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
## Discussion:
|
||||
|
||||
So what were our changes from Step 1?
|
||||
|
||||
* __View template:__ We replaced the hard-coded phone list with the {@link
|
||||
angular.widget.@ng:repeat ng:repeat widget} and two {@link guide.expression angular expressions}
|
||||
enclosed in curly braces: `{{phone.name}}` and `{{phone.snippet}}`:
|
||||
|
||||
* The `ng:repeat="phone in phones"` statement in the `<li>` tag is an angular repeater. It
|
||||
tells angular to create a `<li>` element for each phone in the phones list, using the first
|
||||
`<li>` tag as the template.
|
||||
|
||||
* The curly braces around `phone.name` and `phone.snippet` are an example of {@link
|
||||
angular.markup angular markup}. The curly braces are shorthand for the angular directive
|
||||
{@link angular.directive.ng:bind ng:bind}. They indicate to angular that these are template
|
||||
binding points. Binding points are locations in the template where angular creates
|
||||
data-binding between the View and the Model. 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.
|
||||
|
||||
* __Controller:__ At this point, it doesn't appear as if our controller is doing very much
|
||||
controlling, but it is playing a crucial role: providing context for our data model so we can
|
||||
establish data-binding between the model and the view. Note in the following how we connected the
|
||||
dots between our presentation, data, and logic components:
|
||||
|
||||
* The name of our controller function (in the JavaScript file `controllers.js`) matches the
|
||||
{@link angular.directive.ng:controller ng:controller} directive in the `<body>` tag
|
||||
(`PhoneListCtrl`).
|
||||
* We instantiated our data within the scope of our controller function, and our template
|
||||
binding points are located within the block bounded by the `<body
|
||||
ng:controller="PhoneListCtrl>` tag.
|
||||
|
||||
Angular uses scopes, along with the information contained in the template, data model, and
|
||||
controller to keep the Model and View separated but in sync: any changes to the model are
|
||||
reflected in the view; any changes that occur in the view are reflected in the model.
|
||||
|
||||
* __Model:__ For our data model, we created a simple array of phone records, specified in object
|
||||
literal notation.
|
||||
|
||||
* __Testing:__ Ease of testing is another cornerstone of angular's design philosophy. All we are
|
||||
doing here is showing how easy it is to create a unit test using the technology baked into
|
||||
angular. The test verifies that we have 3 records in the phones array.
|
||||
|
||||
To run this test, make sure you have a {@link tutorial test server running}, and type
|
||||
`./scripts/test.sh` from the command line.
|
||||
|
||||
Angular developers prefer the syntax of Jasmine's Behavior-driven Development (BDD) framework
|
||||
when writing tests. So while Jasmine is not required by angular, we use it to write all tests
|
||||
in this tutorial. You can learn about Jasmine on the {@link http://pivotal.github.com/jasmine/
|
||||
Jasmine home page} and on the {@link https://github.com/pivotal/jasmine/wiki Jasmine wiki}.
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_01 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-2/app Example}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-1...step-2 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_03 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 2
|
||||
@description
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_01 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-2/app Live
|
||||
Demo}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-1...step-2 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_03 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
Now it's time to make this web page dynamic with angular. We'll 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. With angular, we encourage the use
|
||||
of {@link http://en.wikipedia.org/wiki/Model–View–Controller the MVC design pattern} to decouple
|
||||
the code and separate concerns. With that in mind, let's use a little angular and JavaScript to
|
||||
add Model, View, and Controller components to our app.
|
||||
|
||||
1. Reset your workspace to Step 2 using:
|
||||
|
||||
git checkout --force step-2
|
||||
or
|
||||
|
||||
./goto_step.sh 2
|
||||
|
||||
2. Refresh your browser or check the app out on {@link
|
||||
http://angular.github.com/angular-phonecat/step-2/app our server}. The app now contains a list
|
||||
with 3 phones.
|
||||
|
||||
The most important changes are listed below. You can see the full diff on {@link
|
||||
https://github.com/angular/angular-phonecat/compare/step-1...step-2 GitHub}:
|
||||
|
||||
|
||||
## Template for the View
|
||||
|
||||
The __View__ component is constructed by angular from this template:
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
<body ng:controller="PhoneListCtrl">
|
||||
|
||||
<ul>
|
||||
<li ng:repeat="phone in phones">
|
||||
{{phone.name}}
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<script src="lib/angular/angular.js" ng:autobind></script>
|
||||
<script src="js/controllers.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</pre>
|
||||
|
||||
We replaced the hard-coded phone list with the {@link angular.widget.@ng:repeat ng:repeat widget}
|
||||
and two {@link guide.expression angular expressions} enclosed in curly braces: `{{phone.name}}`
|
||||
and `{{phone.snippet}}`:
|
||||
|
||||
* The `ng:repeat="phone in phones"` statement in the `<li>` tag is an angular repeater. It
|
||||
tells angular to create a `<li>` element for each phone in the phones list, using the first
|
||||
`<li>` tag as the template.
|
||||
|
||||
* The curly braces around `phone.name` and `phone.snippet` are an example of {@link
|
||||
angular.markup angular markup}. The curly braces are shorthand for the angular directive
|
||||
{@link angular.directive.ng:bind ng:bind}. They indicate to angular that these are template
|
||||
binding points. Binding points are locations in the template where angular creates
|
||||
data-binding between the View and the Model. 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.
|
||||
|
||||
|
||||
## Model and Controller
|
||||
|
||||
The data __Model__ (a short list of phones in object literal notation) is instantiated within the
|
||||
__Controller__ function (`PhoneListCtrl`):
|
||||
|
||||
__`app/js/controllers.js`:__
|
||||
<pre>
|
||||
function PhoneListCtrl() {
|
||||
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."}];
|
||||
}
|
||||
</pre>
|
||||
|
||||
Although the controller is not yet doing very much controlling, it is playing a crucial role. By
|
||||
providing context for our data model, the controller allows us to establish data-binding between
|
||||
the model and the view. Note in the following how we connected the dots between our presentation,
|
||||
data, and logic components:
|
||||
|
||||
* The name of our controller function (in the JavaScript file `controllers.js`) matches the
|
||||
{@link angular.directive.ng:controller ng:controller} directive in the `<body>` tag
|
||||
(`PhoneListCtrl`).
|
||||
* We instantiated our data within the scope of our controller function, and our template
|
||||
binding points are located within the block bounded by the `<body
|
||||
ng:controller="PhoneListCtrl>` tag.
|
||||
|
||||
Angular uses scopes, along with the information contained in the template, data model, and
|
||||
controller to keep the Model and View separated but in sync: any changes to the model are
|
||||
reflected in the view; any changes that occur in the view are reflected in the model.
|
||||
|
||||
As for our data model, we created a simple array of phone records, specified in object literal
|
||||
notation.
|
||||
|
||||
## Tests
|
||||
|
||||
The "Angular way" makes it easy for us to test as we develop; the unit test for your newly created
|
||||
controller looks as follows:
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
<pre>
|
||||
describe('PhoneCat controllers', function() {
|
||||
|
||||
describe('PhoneListCtrl', function(){
|
||||
|
||||
it('should create "phones" model with 3 phones', function() {
|
||||
var ctrl = new PhoneListCtrl();
|
||||
expect(ctrl.phones.length).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
Ease of testing is another cornerstone of angular's design philosophy. All we are doing here is
|
||||
showing how easy it is to create a unit test. The test verifies that we have 3 records in the
|
||||
phones array.
|
||||
|
||||
Angular developers prefer the syntax of Jasmine's Behavior-driven Development (BDD) framework when
|
||||
writing tests. Although Jasmine is not required by angular, we used it to write all tests in this
|
||||
tutorial. You can learn about Jasmine on the {@link http://pivotal.github.com/jasmine/ Jasmine
|
||||
home page} and on the {@link https://github.com/pivotal/jasmine/wiki Jasmine wiki}.
|
||||
|
||||
angular-seed project is pre-configured to run all unit tests using {@link
|
||||
http://code.google.com/p/js-test-driver/ JsTestDriver}. To run the test, do the following:
|
||||
|
||||
1. In a _separate_ terminal window or tab, go to the `angular-phonecat` directory and run
|
||||
`./scripts/test-server.sh` to start the test web server.
|
||||
|
||||
2. Open a new browser tab or window, navigate to http://localhost:9876, and choose "strict
|
||||
mode". At this point, you can leave this tab open and forget about it. JsTestDriver will
|
||||
use it to execute our tests and report the results in the terminal.
|
||||
|
||||
3. Execute the test by running `./scripts/test.sh`
|
||||
|
||||
You should see the following or similar output:
|
||||
|
||||
Chrome: Runner reset.
|
||||
.
|
||||
Total 1 tests (Passed: 1; Fails: 0; Errors: 0) (2.00 ms)
|
||||
Chrome 11.0.696.57 Mac OS: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (2.00 ms)
|
||||
|
||||
Yay! The test passed! Now, let's go to Step 3 to learn how to add full text search to the app.
|
||||
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_01 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-2/app Live
|
||||
Demo}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-1...step-2 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_03 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -1,108 +1,142 @@
|
|||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 3
|
||||
@description
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_02 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-3/app Example}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link
|
||||
https://github.com/angular/angular-phonecat/commit/a03815f8fb00217f5f9c1d3ef83282f79818e706 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_04 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
We did a lot of work in laying the foundation of our app in the last step, so now we'll do
|
||||
something simple, and add full text search. 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.
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
Fulltext Search: <input name="query"/>
|
||||
|
||||
<ul class="phones">
|
||||
<li ng:repeat="phone in phones.$filter(query)">
|
||||
{{phone.name}}
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
</pre>
|
||||
__`test/e2e/scenarios.js`:__
|
||||
<pre>
|
||||
/* jasmine-like end2end tests go here */
|
||||
describe('PhoneCat App', function() {
|
||||
|
||||
describe('Phone list view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser().navigateTo('../../app/index.html');
|
||||
});
|
||||
|
||||
|
||||
it('should filter the phone list as user types into the search box', function() {
|
||||
expect(repeater('.phones li').count()).toBe(3);
|
||||
|
||||
input('query').enter('nexus');
|
||||
expect(repeater('.phones li').count()).toBe(1);
|
||||
|
||||
input('query').enter('motorola');
|
||||
expect(repeater('.phones li').count()).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
## Discussion:
|
||||
|
||||
We continued using the same controller that we set up in Step 2, but we added the following
|
||||
features to our app:
|
||||
|
||||
* __Search Box:__ A standard HTML `<input>` tag combined with angular's {@link
|
||||
angular.Array.filter $filter} utility (added to the repeater) lets a user type in search criteria
|
||||
and immediately see the effects of their search on the phone list. This new code demonstrates the
|
||||
following:
|
||||
|
||||
* Two way 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 example, the data that you type into the input box (named __`query`__) is immediately
|
||||
available as a filter input in the list repeater (`phone in phones.$filter(`__`query`__`)`).
|
||||
Whenever the data model changes and this change causes the input to the repeater to change, the
|
||||
repeater will efficiently update the DOM to reflect the current state of the model.
|
||||
|
||||
* Use of `$filter` in a template. The `$filter` function is one of several built-in {@link
|
||||
angular.Array angular functions} that augment JavaScript arrays during their evaluation as
|
||||
angular expressions. In {@link guide.expression angular expressions}, these array utilities are
|
||||
available as array methods. (They are prefixed with a $ to avoid naming collisions.)
|
||||
|
||||
* `ng:repeat` automatically shrinks and grows the number of phones in the View, via DOM
|
||||
manipulation that is completely transparent to the developer. If you've written any DOM
|
||||
manipulation code, this should make you happy.
|
||||
|
||||
* __CSS:__ We added in some minimal CSS to the file we set up in Step 0: `./css/app.css`.
|
||||
|
||||
* __Testing:__ To run the end to end test, open http://localhost:8000/test/e2e/runner.html in
|
||||
your browser. This end-to-end test shows the following:
|
||||
|
||||
* Proof that the search box and the repeater are correctly wired together.
|
||||
|
||||
* How easy it is to write end-to-end tests. This is just a simple test, but the point here is
|
||||
to show how easy it is to set up a functional, readable, end-to-end test.
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_02 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-3/app Example}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link
|
||||
https://github.com/angular/angular-phonecat/commit/a03815f8fb00217f5f9c1d3ef83282f79818e706 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_04 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 3
|
||||
@description
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_02 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-3/app Live
|
||||
Demo}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-2...step-3 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_04 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
We did a lot of work in laying a foundation for the app in the last step, so now we'll do
|
||||
something simple, and 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.
|
||||
|
||||
1. Reset your workspace to Step 3 using:
|
||||
|
||||
git checkout --force step-3
|
||||
|
||||
or
|
||||
|
||||
./goto_step.sh 3
|
||||
|
||||
2. Refresh your browser or check the app out on {@link
|
||||
http://angular.github.com/angular-phonecat/step-3/app our server}. The app now has a search box.
|
||||
The phone list on the page changes depending on what a user types into the search box.
|
||||
|
||||
The most important changes are listed below. You can see the full diff on {@link
|
||||
https://github.com/angular/angular-phonecat/compare/step-2...step-3
|
||||
GitHub}:
|
||||
|
||||
|
||||
## Controller
|
||||
|
||||
We made no changes to the controller.
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
Fulltext Search: <input name="query"/>
|
||||
|
||||
<ul class="phones">
|
||||
<li ng:repeat="phone in phones.$filter(query)">
|
||||
{{phone.name}}
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
</pre>
|
||||
|
||||
We added a standard HTML `<input>` tag and use angular's {@link angular.Array.filter $filter}
|
||||
function to process the input for the `ng:repeater`.
|
||||
|
||||
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.
|
||||
|
||||
* Use of `$filter`. The `{@link angular.Array.filter $filter}` method, uses the `query` value, to
|
||||
create a new array that contains only those records that match the `query`.
|
||||
|
||||
* `ng:repeat` automatically updates the view in response to the changing number of phones returned
|
||||
by the `$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`:__
|
||||
<pre>
|
||||
describe('PhoneCat App', function() {
|
||||
|
||||
describe('Phone list view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser().navigateTo('../../app/index.html');
|
||||
});
|
||||
|
||||
it('should filter the phone list as user types into the search box', function() {
|
||||
expect(repeater('.phones li').count()).toBe(3);
|
||||
|
||||
input('query').enter('nexus');
|
||||
expect(repeater('.phones li').count()).toBe(1);
|
||||
|
||||
input('query').enter('motorola');
|
||||
expect(repeater('.phones li').count()).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
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 {@link
|
||||
https://docs.google.com/document/d/11L8htLKrh6c92foV71ytYpiKkeKpM4_a5-9c3HywfIc/edit?hl=en&pli=1#
|
||||
angular's end-to-end test runner}.
|
||||
|
||||
To run the end-to-end test, open the following in a new browser tab:
|
||||
|
||||
* node.js users: http://localhost:8000/test/e2e/runner.html
|
||||
* users with other http servers:
|
||||
http://localhost:[*port-number*]/[*context-path*]/test/e2e/runner.html
|
||||
* casual reader: http://angular.github.com/angular-phonecat/step-3/test/e2e/runner.html
|
||||
|
||||
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.
|
||||
|
||||
Now that you've verified everything, go to Step 4 to learn how to add sorting capability to the
|
||||
phone list app.
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_02 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-3/app Live
|
||||
Demo}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-2...step-3 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_04 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -1,161 +1,201 @@
|
|||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 4
|
||||
@description
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_03 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-4/app Example}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-3...step-4 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_05 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
In this step, we add a feature that lets our users choose which way to order the phone list.
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
<ul class="predicates">
|
||||
<li>
|
||||
Search: <input type="text" name="query"/>
|
||||
</li>
|
||||
<li>
|
||||
Sort by:
|
||||
<select name="orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="age">Newest</option>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="phones">
|
||||
<li ng:repeat="phone in phones.$filter(query).$orderBy(orderProp)">
|
||||
{{phone.name}}
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
</pre>
|
||||
|
||||
__`app/js/controller.js`:__
|
||||
<pre>
|
||||
/* App Controllers */
|
||||
|
||||
function PhoneListCtrl() {
|
||||
this.phones = [{"name": "Nexus S",
|
||||
"snippet": "Fast just got faster with Nexus S.",
|
||||
"age": 0},
|
||||
{"name": "Motorola XOOM™ with Wi-Fi",
|
||||
"snippet": "The Next, Next Generation tablet.",
|
||||
"age": 1},
|
||||
{"name": "MOTOROLA XOOM™",
|
||||
"snippet": "The Next, Next Generation tablet.",
|
||||
"age": 2}];
|
||||
|
||||
this.orderProp = 'age';
|
||||
}
|
||||
</pre>
|
||||
|
||||
__`test/unit/controllerSpec.js`:__
|
||||
<pre>
|
||||
/* jasmine specs for controllers go here */
|
||||
describe('PhoneCat controllers', function() {
|
||||
|
||||
describe('PhoneListCtrl', function(){
|
||||
var scope, $browser, ctrl;
|
||||
|
||||
beforeEach(function() {
|
||||
ctrl = new PhoneListCtrl();
|
||||
});
|
||||
|
||||
|
||||
it('should create "phones" model with 3 phones', function() {
|
||||
expect(ctrl.phones.length).toBe(3);
|
||||
});
|
||||
|
||||
|
||||
it('should set the default value of orderProp model', function() {
|
||||
expect(ctrl.orderProp).toBe('age');
|
||||
});
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
__`test/e2e/scenarios.js`:__
|
||||
<pre>
|
||||
/* jasmine-like end2end tests go here */
|
||||
describe('PhoneCat App', function() {
|
||||
|
||||
describe('Phone list view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser().navigateTo('../../app/index.html');
|
||||
});
|
||||
|
||||
|
||||
it('should filter the phone list as user types into the search box', function() {
|
||||
expect(repeater('.phones li').count()).toBe(3);
|
||||
|
||||
input('query').enter('nexus');
|
||||
expect(repeater('.phones li').count()).toBe(1);
|
||||
|
||||
input('query').enter('motorola');
|
||||
expect(repeater('.phones li').count()).toBe(2);
|
||||
});
|
||||
|
||||
|
||||
it('should be possible to control phone order via the drop down select box', function() {
|
||||
input('query').enter('tablet'); //let's narrow the dataset to make the test assertions
|
||||
shorter
|
||||
|
||||
expect(repeater('.phones li', 'Phone List').column('a')).
|
||||
toEqual(["Motorola XOOM\u2122 with Wi-Fi",
|
||||
"MOTOROLA XOOM\u2122"]);
|
||||
|
||||
select('orderProp').option('alphabetical');
|
||||
|
||||
expect(repeater('.phones li', 'Phone List').column('a')).
|
||||
toEqual(["MOTOROLA XOOM\u2122",
|
||||
"Motorola XOOM\u2122 with Wi-Fi"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
## Discussion:
|
||||
|
||||
To provide dynamic ordering, we employ another one of angular's "array type augmenters" and let
|
||||
the data binding do the rest of the work for us:
|
||||
|
||||
* First, we provide a `<select>` element named `orderProp` for our users so they can choose to
|
||||
sort the phone list either alphabetically or by the age of the phone. We added the `age` property
|
||||
to each phone record so we can sort by that field.
|
||||
|
||||
* Like {@link angular.Array.filter $filter}, {@link angular.Array.orderBy $orderBy} is a built-in
|
||||
method available on array objects in angular expressions. In our UI template, we set up a select
|
||||
box that lets the user set the `orderProp` model variable to one of the string constants: `age` or
|
||||
`name`.
|
||||
|
||||
* In our controller, we added a line to set the default value of `orderProp` to `age`. If we
|
||||
don't override the default value, angular uses the value of the first `<option>` element when it
|
||||
initializes the data model.
|
||||
|
||||
* Our unit test now verifies that our default ordering property is set.
|
||||
|
||||
* We added an end-to-end test to verify that our select box ordering mechanism works properly.
|
||||
|
||||
* Once again we added a little more CSS to improve the View.
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_03 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-4/app Example}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-3...step-4 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_05 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 4
|
||||
@description
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_03 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-4/app Live Demo}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-3...step-4 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_05 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
In this step, you will add a feature to let your users select 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.
|
||||
|
||||
|
||||
1. Reset your workspace to Step 4 using:
|
||||
|
||||
git checkout --force step-4
|
||||
|
||||
or
|
||||
|
||||
./goto_step.sh 4
|
||||
|
||||
2. Refresh your browser or check the app out on {@link
|
||||
http://angular.github.com/angular-phonecat/step-4/app our server}. You should see that 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.
|
||||
|
||||
The most important changes are listed below. You can see the full diff on {@link
|
||||
https://github.com/angular/angular-phonecat/compare/step-3...step-4
|
||||
GitHub}:
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
<ul class="predicates">
|
||||
<li>
|
||||
Search: <input type="text" name="query"/>
|
||||
</li>
|
||||
<li>
|
||||
Sort by:
|
||||
<select name="orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="age">Newest</option>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="phones">
|
||||
<li ng:repeat="phone in phones.$filter(query).$orderBy(orderProp)">
|
||||
{{phone.name}}
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
</pre>
|
||||
|
||||
In the `index.html` template we made the following changes:
|
||||
|
||||
* First, we added a `<select>` html element named `orderProp`, so that our users can pick from the
|
||||
two provided sorting options.
|
||||
|
||||
* We then chained the `$filter` method with `{@link angular.Array.orderBy $orderBy}` method to
|
||||
further process the input into the repeater.
|
||||
|
||||
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` method.
|
||||
|
||||
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/controller.js`:__
|
||||
<pre>
|
||||
/* App Controllers */
|
||||
|
||||
function PhoneListCtrl() {
|
||||
this.phones = [{"name": "Nexus S",
|
||||
"snippet": "Fast just got faster with Nexus S.",
|
||||
"age": 0},
|
||||
{"name": "Motorola XOOM™ with Wi-Fi",
|
||||
"snippet": "The Next, Next Generation tablet.",
|
||||
"age": 1},
|
||||
{"name": "MOTOROLA XOOM™",
|
||||
"snippet": "The Next, Next Generation tablet.",
|
||||
"age": 2}];
|
||||
|
||||
this.orderProp = 'age';
|
||||
}
|
||||
</pre>
|
||||
|
||||
* 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.
|
||||
|
||||
* We added a line to the controller that sets the default value of `orderProp` to `age`. If we had
|
||||
not set the default value here, angular would have used the value of the first `<option>` element
|
||||
(`'name'`) when it initialized the data model.
|
||||
|
||||
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
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.
|
||||
|
||||
__`test/unit/controllerSpec.js`:__
|
||||
<pre>
|
||||
/* jasmine specs for controllers go here */
|
||||
describe('PhoneCat controllers', function() {
|
||||
|
||||
describe('PhoneListCtrl', function(){
|
||||
var scope, $browser, ctrl;
|
||||
|
||||
beforeEach(function() {
|
||||
ctrl = new PhoneListCtrl();
|
||||
});
|
||||
|
||||
|
||||
it('should create "phones" model with 3 phones', function() {
|
||||
expect(ctrl.phones.length).toBe(3);
|
||||
});
|
||||
|
||||
|
||||
it('should set the default value of orderProp model', function() {
|
||||
expect(ctrl.orderProp).toBe('age');
|
||||
});
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
|
||||
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 nearest `describe` block.
|
||||
|
||||
To run the unit tests, once again execute the `./scripts/test.sh` script and you should see the
|
||||
following output.
|
||||
|
||||
Chrome: Runner reset.
|
||||
..
|
||||
Total 2 tests (Passed: 2; Fails: 0; Errors: 0) (3.00 ms)
|
||||
Chrome 11.0.696.57 Mac OS: Run 2 tests (Passed: 2; Fails: 0; Errors 0) (3.00 ms)
|
||||
|
||||
|
||||
Let's turn our attention to the end-to-end test.
|
||||
|
||||
__`test/e2e/scenarios.js`:__
|
||||
<pre>
|
||||
...
|
||||
it('should be possible to control phone order via the drop down select box', function() {
|
||||
input('query').enter('tablet'); //let's narrow the dataset to make the test assertions
|
||||
shorter
|
||||
|
||||
expect(repeater('.phones li', 'Phone List').column('a')).
|
||||
toEqual(["Motorola XOOM\u2122 with Wi-Fi",
|
||||
"MOTOROLA XOOM\u2122"]);
|
||||
|
||||
select('orderProp').option('alphabetical');
|
||||
|
||||
expect(repeater('.phones li', 'Phone List').column('a')).
|
||||
toEqual(["MOTOROLA XOOM\u2122",
|
||||
"Motorola XOOM\u2122 with Wi-Fi"]);
|
||||
});
|
||||
...
|
||||
</pre>
|
||||
|
||||
The end-to-end test verifies that the ordering mechanism of the select box is working correctly.
|
||||
|
||||
You can now refresh the browser tab with the end-to-end test runner to see the tests run, or you
|
||||
can see them running on {@link
|
||||
http://angular.github.com/angular-phonecat/step-4/test/e2e/runner.html
|
||||
angular's server}.
|
||||
|
||||
Now that you have added list sorting and tested the app, go to Step 5 to learn about angular
|
||||
services and how angular uses dependency injection.
|
||||
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_03 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-4/app Live Demo}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-3...step-4 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_05 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -1,147 +1,219 @@
|
|||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 5
|
||||
@description
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_04 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-5/app Example}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-4...step-5 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_06 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
In this step, the View template remains the same but the Model and Controller change. We'll
|
||||
introduce the use of an angular {@link angular.service service}, which we will use to implement an
|
||||
`XMLHttpRequest` request to communicate with a server. Angular provides the built-in {@link
|
||||
angular.service.$xhr $xhr} service to make this easy.
|
||||
|
||||
The addition of the `$xhr` service to our app gives us the opportunity to talk about {@link
|
||||
guide.di Dependency Injection} (DI). The use of DI is another cornerstone of the angular
|
||||
philosophy. DI helps make your web apps well structured, loosely coupled, and ultimately easier to
|
||||
test.
|
||||
|
||||
__`app/js/controllers.js:`__
|
||||
<pre>
|
||||
/* App Controllers */
|
||||
|
||||
function PhoneListCtrl($xhr) {
|
||||
var self = this;
|
||||
|
||||
$xhr('GET', 'phones/phones.json', function(code, response) {
|
||||
self.phones = response;
|
||||
});
|
||||
|
||||
self.orderProp = 'age';
|
||||
}
|
||||
|
||||
//PhoneListCtrl.$inject = ['$xhr'];
|
||||
</pre>
|
||||
|
||||
__`test/unit/controllerSpec.js`:__
|
||||
<pre>
|
||||
/* jasmine specs for controllers go here */
|
||||
describe('PhoneCat controllers', function() {
|
||||
|
||||
describe('PhoneListCtrl', function(){
|
||||
var scope, $browser, ctrl;
|
||||
|
||||
beforeEach(function() {
|
||||
scope = angular.scope();
|
||||
$browser = scope.$service('$browser');
|
||||
|
||||
$browser.xhr.expectGET('phones/phones.json').respond([{name: 'Nexus S'},
|
||||
{name: 'Motorola DROID'}]);
|
||||
ctrl = scope.$new(PhoneListCtrl);
|
||||
});
|
||||
|
||||
|
||||
it('should create "phones" model with 2 phones fetched from xhr', function() {
|
||||
expect(ctrl.phones).toBeUndefined();
|
||||
$browser.xhr.flush();
|
||||
|
||||
expect(ctrl.phones).toEqual([{name: 'Nexus S'},
|
||||
{name: 'Motorola DROID'}]);
|
||||
});
|
||||
|
||||
|
||||
it('should set the default value of orderProp model', function() {
|
||||
expect(ctrl.orderProp).toBe('age');
|
||||
});
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
## Discussion:
|
||||
|
||||
* __Services:__ {@link angular.service Services} are substitutable objects managed by angular's
|
||||
{@link guide.di DI subsystem}. Angular services simplify some of the standard operations common
|
||||
to web apps. Angular provides several built-in services (such as {@link angular.service.$xhr
|
||||
$xhr}). You can also create your own custom services.
|
||||
|
||||
* __Dependency Injection:__ To use an angular service, you simply provide the name of the service
|
||||
as an argument to the controller's constructor function. The name of the argument is significant,
|
||||
because angular's {@link guide.di DI subsystem} recognizes the identity of a service by its name,
|
||||
and provides the name of the service to the controller during the controller's construction. The
|
||||
dependency injector also takes care of creating any transitive dependencies the service may have
|
||||
(services often depend upon other services).
|
||||
|
||||
Note: if you minify the javascript code for this controller, all function arguments will be
|
||||
minified as well. This will result in the dependency injector not being able to identify
|
||||
services correctly. To overcome this issue, just assign an array with service identifier strings
|
||||
into the `$inject` property of the controller function.
|
||||
|
||||
* __`$xhr`:__ We moved our data set out of the controller and into the file
|
||||
`app/phones/phones.json` (and added some more phones). We used the `$xhr` service to make a GET
|
||||
HTTP request to our web server, asking for `phone/phones.json` (the url is relative to our
|
||||
`index.html` file). The server responds with the contents of the json file, which serves as the
|
||||
source of our data. Keep in mind that the response might just as well have been dynamically
|
||||
generated by a sophisticated backend server. To our web server they both look the same, but using
|
||||
a real backend server to generate a response would make our tutorial unnecessarily complicated.
|
||||
|
||||
Notice that the $xhr service takes a callback as the last parameter. This callback is used to
|
||||
process the response. In our case, we just assign the response to the current scope controlled
|
||||
by the controller, as a model called `phones`. Have you realized that we didn't even have to
|
||||
parse the response? Angular took care of that for us.
|
||||
|
||||
* __Testing:__ The unit tests have been expanded. Because of the dependency injection business,
|
||||
we now need to create the controller the same way that angular does it behind the scenes. For this
|
||||
reason, we need to:
|
||||
|
||||
* Create a root scope object by calling `angular.scope()`
|
||||
|
||||
* Call `scope.$new(PhoneListCtrl)` to get angular to create the child scope associated with
|
||||
our controller.
|
||||
|
||||
At the same time, we need to tell the testing harness that it should expect an incoming
|
||||
request from our controller. To do this we:
|
||||
|
||||
* Use the `$service` method to retrieve the `$browser` service - this is a service that in
|
||||
angular represents various browser APIs. In tests, angular automatically uses a mock version
|
||||
of this service that allows you to write tests without having to deal with these native APIs
|
||||
and the global state associated with them.
|
||||
|
||||
* We use the `$browser.expectGET` method to train the `$browser` object to expect an incoming
|
||||
http request and tell it what to respond with. Note that the responses are not returned before
|
||||
we call the `$browser.xhr.flush()` method.
|
||||
|
||||
* We then make assertions to verify that the `phones` model doesn't exist on the scope, before
|
||||
the response is received.
|
||||
|
||||
* We flush the xhr queue in the browser by calling `$browser.xhr.flush()`. This causes the
|
||||
callback we passed into the `$xhr` service to be executed with the trained response.
|
||||
|
||||
* Finally, we make the assertions, verifying that the phone model now exists on the scope.
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_04 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-5/app Example}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-4...step-5
|
||||
Code Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_06 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 5
|
||||
@description
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_04 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-5/app Live Demo
|
||||
}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-4...step-5 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_06 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
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 angular.service services} called {@link
|
||||
angular.service.$xhr $xhr}. We will use angular's dependency injection to provide the service to
|
||||
the `PhoneListCtrl` controller.
|
||||
|
||||
1. Reset your workspace to Step 5 using:
|
||||
|
||||
git checkout --force step-5
|
||||
|
||||
or
|
||||
|
||||
./goto_step.sh 5
|
||||
|
||||
2. Refresh your browser or check the app out on {@link
|
||||
http://angular.github.com/angular-phonecat/step-5/app our server}. You should now see a list of 20
|
||||
phones.
|
||||
|
||||
|
||||
The most important changes are listed below. You can see the full diff on {@link
|
||||
https://github.com/angular/angular-phonecat/compare/step-4...step-5
|
||||
GitHub}:
|
||||
|
||||
## Data
|
||||
|
||||
The `app/phones/phone.json` file in your project is a dataset that contains a larger list of
|
||||
phones stored in the JSON format.
|
||||
|
||||
Following is a sample of the file:
|
||||
<pre>
|
||||
[
|
||||
{
|
||||
"age": 13,
|
||||
"id": "motorola-defy-with-motoblur",
|
||||
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
|
||||
"snippet": "Are you ready for everything life throws your way?"
|
||||
...
|
||||
},
|
||||
...
|
||||
]
|
||||
</pre>
|
||||
|
||||
|
||||
## Controller
|
||||
|
||||
In this step, the view template will remain the same but the model and controller will change.
|
||||
We'll use angular's {@link angular.service.$xhr} service to make an HTTP request to your web
|
||||
server to fetch the data in the `phones.json` file.
|
||||
|
||||
__`app/js/controllers.js:`__
|
||||
<pre>
|
||||
function PhoneListCtrl($xhr) {
|
||||
var self = this;
|
||||
|
||||
$xhr('GET', 'phones/phones.json', function(code, response) {
|
||||
self.phones = response;
|
||||
});
|
||||
|
||||
self.orderProp = 'age';
|
||||
}
|
||||
|
||||
//PhoneListCtrl.$inject = ['$xhr'];
|
||||
</pre>
|
||||
|
||||
We removed the hard-coded dataset from the controller and instead are using the `$xhr` service to
|
||||
access the data stored in `app/phones/phones.json`. The `$xhr` service makes a HTTP GET request to
|
||||
our web server, asking for `phone/phones.json` (the url is relative to our `index.html` file). The
|
||||
server responds by providing the data in the json file.
|
||||
|
||||
Keep in mind that 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.
|
||||
|
||||
Notice that the `$xhr` service takes a callback as the last parameter. This callback is used to
|
||||
process the response. In our case, we just assign the response to the current scope controlled by
|
||||
the controller, as a model called `phones`. Have you realized that we didn't even have to parse
|
||||
the response? Angular took care of that for us.
|
||||
|
||||
We already mentioned that the `$xhr` function we just used is an angular service. {@link
|
||||
angular.service Angular services} are substitutable objects managed by angular's {@link guide.di
|
||||
DI subsystem}.
|
||||
|
||||
Dependency injection helps to make your web apps well structured, loosely coupled, and much easier
|
||||
to test. What's important to understand is how the controllers get access to these services
|
||||
through dependency injection.
|
||||
|
||||
The dependency injection pattern is based on declaring the dependencies we require and letting the
|
||||
system provide them to us. To do this in angular, you simply provide the names of the services you
|
||||
need as arguments to the controller's constructor function, as follows:
|
||||
|
||||
function PhoneListCtrl($xhr) {
|
||||
|
||||
The name of the argument is significant, because angular recognizes the identity of a service by
|
||||
the argument name. Once angular knows what services are being requested, it provides them to the
|
||||
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).
|
||||
|
||||
As we mentioned earlier, angular infers the controller's dependencies from the names of arguments
|
||||
of the controller's constructor function. If you were to minify the JavaScript code for this
|
||||
controller, all of these function arguments would be minified as well, and the dependency injector
|
||||
would not being able to identify services correctly.
|
||||
|
||||
To overcome issues caused by minification, just assign an array with service identifier strings
|
||||
into the `$inject` property of the controller function, just like the last line in the snippet
|
||||
(commented out) suggests:
|
||||
|
||||
PhoneListCtrl.$inject = ['$xhr'];
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
<pre>
|
||||
describe('PhoneCat controllers', function() {
|
||||
|
||||
describe('PhoneListCtrl', function(){
|
||||
var scope, $browser, ctrl;
|
||||
|
||||
beforeEach(function() {
|
||||
scope = angular.scope();
|
||||
$browser = scope.$service('$browser');
|
||||
|
||||
$browser.xhr.expectGET('phones/phones.json').respond([{name: 'Nexus S'},
|
||||
{name: 'Motorola DROID'}]);
|
||||
ctrl = scope.$new(PhoneListCtrl);
|
||||
});
|
||||
|
||||
|
||||
it('should create "phones" model with 2 phones fetched from xhr', function() {
|
||||
expect(ctrl.phones).toBeUndefined();
|
||||
$browser.xhr.flush();
|
||||
|
||||
expect(ctrl.phones).toEqual([{name: 'Nexus S'},
|
||||
{name: 'Motorola DROID'}]);
|
||||
});
|
||||
|
||||
|
||||
it('should set the default value of orderProp model', function() {
|
||||
expect(ctrl.orderProp).toBe('age');
|
||||
});
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
|
||||
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 `$xhr` implementation. However, the recommended (and
|
||||
easier) way is to create a controller in the test environment in the same way that angular does it
|
||||
in the production code behind the scenes.
|
||||
|
||||
To create the controller in the test environment, do the following:
|
||||
|
||||
* Create a root scope object by calling `angular.scope()`
|
||||
|
||||
* Call `scope.$new(PhoneListCtrl)` to get angular to create the child scope associated with
|
||||
the `PhoneListCtrl` controller.
|
||||
|
||||
Because our code now uses the `$xhr` 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:
|
||||
|
||||
* Use the `{@link angular.scope.$service $service}` method to retrieve the `$browser` service,
|
||||
a service that angular uses to represent various browser APIs. In tests, angular automatically
|
||||
uses a mock version of this service that allows you to write tests without having to deal with
|
||||
these native APIs and the global state associated with them.
|
||||
|
||||
* We use the `$browser.expectGET` method to train the `$browser` object to expect an incoming
|
||||
HTTP request and tell it what to respond with. Note that the responses are not returned before
|
||||
we call the `$browser.xhr.flush()` method.
|
||||
|
||||
* We then make assertions to verify that the `phones` model doesn't exist on the scope, before
|
||||
the response is received.
|
||||
|
||||
* We flush the xhr queue in the browser by calling `$browser.xhr.flush()`. This causes the
|
||||
callback we passed into the `$xhr` service to be executed with the trained response.
|
||||
|
||||
* Finally, we make the assertions, verifying that the phone model now exists on the scope.
|
||||
|
||||
To run the unit tests, execute the `./scripts/test.sh` script and you should see the following
|
||||
output.
|
||||
|
||||
Chrome: Runner reset.
|
||||
..
|
||||
Total 2 tests (Passed: 2; Fails: 0; Errors: 0) (3.00 ms)
|
||||
Chrome 11.0.696.57 Mac OS: Run 2 tests (Passed: 2; Fails: 0; Errors 0) (3.00 ms)
|
||||
|
||||
|
||||
Now that you have learned how easy it is to use angular services (thanks to angular's
|
||||
implementation of dependency injection), go to Step 6, where you will add some thumbnail images of
|
||||
phones and some links.
|
||||
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_04 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-5/app Live Demo
|
||||
}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-4...step-5
|
||||
Code Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_06 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -1,113 +1,119 @@
|
|||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 6
|
||||
@description
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_05 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-6/app Example}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-5...step-6 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_07 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
In this step, we add thumbnail images, links, and a little more CSS to our app. For now, our
|
||||
links go nowhere. One step at a time; in the next step we'll implement new views that these links
|
||||
will open.
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
<ul class="predicates">
|
||||
<li>
|
||||
Search: <input type="text" name="query"/>
|
||||
</li>
|
||||
<li>
|
||||
Sort by:
|
||||
<select name="orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="age">Newest</option>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="phones">
|
||||
<li ng:repeat="phone in phones.$filter(query).$orderBy(orderProp)">
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<a href="#/phones/{{phone.id}}" class="thumb"><img ng:src="{{phone.imageUrl}}"></a>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
</pre>
|
||||
|
||||
__`app/js/controller.js`__ (Unchanged):
|
||||
<pre>
|
||||
/* App Controllers */
|
||||
|
||||
function PhoneListCtrl($xhr) {
|
||||
var self = this;
|
||||
|
||||
$xhr('GET', 'phones/phones.json', function(code, response) {
|
||||
self.phones = response;
|
||||
});
|
||||
|
||||
self.orderProp = 'age';
|
||||
}
|
||||
|
||||
//PhoneListCtrl.$inject = ['$xhr'];
|
||||
</pre>
|
||||
|
||||
__`app/phones/phones.json`__ (sample snippet):
|
||||
<pre>
|
||||
[
|
||||
{
|
||||
"age": 4,
|
||||
...
|
||||
"carrier": "T-Mobile",
|
||||
"id": "motorola-defy-with-motoblur",
|
||||
"imageUrl": "http://google.com/phone/image/small/640001",
|
||||
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
|
||||
"snippet": "Are you ready for everything life throws your way?"
|
||||
},
|
||||
…
|
||||
]
|
||||
</pre>
|
||||
|
||||
__`test/e2e/scenarios.js`__:
|
||||
<pre>
|
||||
...
|
||||
it('should render phone specific links', function() {
|
||||
input('query').enter('nexus');
|
||||
element('.phones li a').click();
|
||||
expect(browser().location().hash()).toBe('/phones/nexus-s');
|
||||
});
|
||||
...
|
||||
</pre>
|
||||
|
||||
## Discussion:
|
||||
|
||||
* Note that we're using {@link guide.expression angular expressions} enclosed in the now-familiar
|
||||
{@link angular.markup double-curly brace markup} in the href attribute values. These represent
|
||||
attribute bindings, and work the same way as the bindings we saw in previous steps.
|
||||
|
||||
* Note also the use of the {@link angular.directive.ng:src ng:src} directive in the `<img>` tag.
|
||||
That directive prevents the browser from treating the angular `{{ exppression }}` markup
|
||||
literally, as it would do if we tried to use markup in a regular `src` attribute. Use `ng:src` to
|
||||
keep the browser from eagerly making an extra http request to an invalid location.
|
||||
|
||||
* We expanded our end-to-end test to verify that the app is generating correct links to the phone
|
||||
views we will implement in the upcoming steps.
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_05 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-6/app Example}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-5...step-6 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_07 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 6
|
||||
@description
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_05 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-6/app Live Demo
|
||||
}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-5...step-6 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_07 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
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.
|
||||
|
||||
1. Reset your workspace to Step 6 using:
|
||||
|
||||
git checkout --force step-6
|
||||
|
||||
or
|
||||
|
||||
./goto_step.sh 6
|
||||
|
||||
2. Refresh your browser or check the app out on {@link
|
||||
http://angular.github.com/angular-phonecat/step-6/app our server}. You should now see links and
|
||||
images of the phones in the list.
|
||||
|
||||
The most important changes are listed below. You can see the full diff on {@link
|
||||
https://github.com/angular/angular-phonecat/compare/step-5...step-6
|
||||
GitHub}:
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
__`app/phones/phones.json`__ (sample snippet):
|
||||
<pre>
|
||||
[
|
||||
{
|
||||
...
|
||||
"id": "motorola-defy-with-motoblur",
|
||||
"imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg",
|
||||
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
|
||||
...
|
||||
},
|
||||
...
|
||||
]
|
||||
</pre>
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
<ul class="phones">
|
||||
<li ng:repeat="phone in phones.$filter(query).$orderBy(orderProp)">
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<a href="#/phones/{{phone.id}}" class="thumb"><img ng:src="{{phone.imageUrl}}"></a>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
</pre>
|
||||
|
||||
To dynamically generate links that will in the future lead to phone detail pages, we used the
|
||||
now-familiar {@link angular.markup double-curly brace markup} 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
|
||||
angular.directive.ng:src ng:src} directive. That directive prevents the browser from treating the
|
||||
angular `{{ exppression }}` markup literally, which it would have done if we had only specified an
|
||||
attribute binding in a regular `src` attribute (`<img src="{{phone.imageUrl}}">`). Using `ng:src`
|
||||
prevents the browser from making an http request to an invalid location.
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
__`test/e2e/scenarios.js`__:
|
||||
<pre>
|
||||
...
|
||||
it('should render phone specific links', function() {
|
||||
input('query').enter('nexus');
|
||||
element('.phones li a').click();
|
||||
expect(browser().location().hash()).toBe('/phones/nexus-s');
|
||||
});
|
||||
...
|
||||
</pre>
|
||||
|
||||
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.
|
||||
|
||||
You can now refresh the browser tab with the end-to-end test runner to see the tests run, or you
|
||||
can see them running on {@link
|
||||
http://angular.github.com/angular-phonecat/step-6/test/e2e/runner.html
|
||||
angular's server}.
|
||||
|
||||
Now that you have added phone images and links, go to Step 7 to learn about angular layout
|
||||
templates and how angular makes it easy to create applications that have multiple views.
|
||||
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_05 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-6/app Live Demo
|
||||
}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-5...step-6 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_07 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -1,181 +1,211 @@
|
|||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 7
|
||||
@description
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_06 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-7/app Live Demo
|
||||
}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-6...step-7 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_08 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
Our app is slowly growing and becoming more complex. Up until now, the app provided our users with
|
||||
just one view (the list of all phones), and all of our template code was located in the
|
||||
`index.html` file. The next step in building our app is the addition of 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.
|
||||
|
||||
Similarly as with templates, angular also allows for controllers and scopes managed by these
|
||||
controllers to be nested. We are going to create a "root" controller called `PhoneCatCtrl`, which
|
||||
will contain the declaration of routes for the application.
|
||||
|
||||
Application routes in angular are declared via the {@link angular.service.$route $route} service.
|
||||
This services makes it easy to wire together controllers, View templates, and the current URL
|
||||
location in the browser. Using this feature we can implement {@link
|
||||
http://en.wikipedia.org/wiki/Deep_linking deep linking}, which lets us utilize the browser's
|
||||
History, and Back and Forward browser navigation.
|
||||
|
||||
We'll use the $route service to declare that our application consists of two different views: one
|
||||
view presents the phone listing, and the other view presents the details for a particular phone.
|
||||
Each view will have the template stored in a separate file in the `app/partials/` directory.
|
||||
Similarly each view will have a controller associated with it. These will be stored in the
|
||||
existing `app/js/controllers.js` file.
|
||||
|
||||
The `$route` service is usually used in conjunction with the {@link angular.widget.ng:view
|
||||
ng:view} widget. The role of the `ng:view` widget is to include the view template for the current
|
||||
route into the layout template, which makes it a perfect fit for our `index.html` template.
|
||||
|
||||
For now we are going to get all the routing going, and move the phone listing template into a
|
||||
separate file. We'll save the implementation of the phone details View for the next step.
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
<body ng:controller="PhoneCatCtrl">
|
||||
|
||||
<ng:view></ng:view>
|
||||
|
||||
<script src="lib/angular/angular.js" ng:autobind></script>
|
||||
<script src="js/controllers.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</pre>
|
||||
|
||||
__`app/partials/phone-list.html`:__
|
||||
<pre>
|
||||
<ul class="predicates">
|
||||
<li>
|
||||
Search: <input type="text" name="query"/>
|
||||
</li>
|
||||
<li>
|
||||
Sort by:
|
||||
<select name="orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="age">Newest</option>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="phones">
|
||||
<li ng:repeat="phone in phones.$filter(query).$orderBy(orderProp)">
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<a href="#/phones/{{phone.id}}" class="thumb"><img ng:src="{{phone.imageUrl}}"></a>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</pre>
|
||||
|
||||
__`app/partials/phone-list.html`:__
|
||||
<pre>
|
||||
TBD: detail view for {{params.phoneId}}
|
||||
</pre>
|
||||
|
||||
__`app/js/controller.js`:__
|
||||
<pre>
|
||||
/* App Controllers */
|
||||
|
||||
function PhoneCatCtrl($route) {
|
||||
var self = this;
|
||||
|
||||
$route.when('/phones',
|
||||
{template: 'partials/phone-list.html', controller: PhoneListCtrl});
|
||||
$route.when('/phones/:phoneId',
|
||||
{template: 'partials/phone-detail.html', controller: PhoneDetailCtrl});
|
||||
$route.otherwise({redirectTo: '/phones'});
|
||||
|
||||
$route.onChange(function(){
|
||||
self.params = $route.current.params;
|
||||
});
|
||||
|
||||
$route.parent(this);
|
||||
}
|
||||
|
||||
//PhoneCatCtrl.$inject = ['$route'];
|
||||
|
||||
|
||||
function PhoneListCtrl($xhr) {
|
||||
var self = this;
|
||||
|
||||
$xhr('GET', 'phones/phones.json', function(code, response) {
|
||||
self.phones = response;
|
||||
});
|
||||
|
||||
self.orderProp = 'age';
|
||||
}
|
||||
|
||||
//PhoneListCtrl.$inject = ['$xhr'];
|
||||
|
||||
|
||||
function PhoneDetailCtrl() {}
|
||||
</pre>
|
||||
|
||||
## Discussion:
|
||||
|
||||
* __The View.__ Our View template in `index.html` has been reduced down to this:
|
||||
`<ng:view></ng:view>`. As described above, it is now a "layout template". We added the following
|
||||
two new View templates:
|
||||
|
||||
* `app/partials/phone-list.html` for the phone list. The phone-list view was formerly our
|
||||
main view. We simply moved the code from `index.html` to here.
|
||||
|
||||
* `app/partials/phone-detail.html` for the phone details (just a placeholder template for now).
|
||||
|
||||
* __The Controller(s).__ We now have a new root controller (`PhoneCatCtrl`) and two
|
||||
sub-controllers (`PhoneListCtrl` and `PhoneDetailCtrl`). These inherit the model properties and
|
||||
behavior from the root controller.
|
||||
|
||||
* __`$route.`__ The root controller's job now is to set up the `$route` configuration:
|
||||
|
||||
* When the fragment part of the URL in the browser ends in "/phones", `$route` service
|
||||
grabs the `phone-list.html` template, compiles it, and links it with a new scope that is
|
||||
controlled by our `PhoneListCtrl` controller.
|
||||
|
||||
* When the URL ends in "/phones/:phoneId", `$route` compiles and links the
|
||||
`phone-detail.html` template as it did with `phone-list.html`. But note the use of the
|
||||
`:phoneId` parameter declaration in the `path` argument of `$route.when()`: `$route`
|
||||
services provides all the values for variables defined in this way as
|
||||
`$route.current.params` map. In our route, `$route.current.params.phoneId` always holds
|
||||
the current contents of the `:phoneId` portion of the URL. We will use the `phoneId`
|
||||
parameter when we fetch the phone details in Step 8.
|
||||
|
||||
* Any other URL fragment gets redirected to `/phones`.
|
||||
|
||||
* __Controller/Scope inheritance.__ In the function passed into `$route`'s `onChange()`
|
||||
method, we copied url parameters extracted from the current route to the `params` property in
|
||||
the root scope. This property is inherited by child scopes created for our view controllers
|
||||
and accessible by these controllers.
|
||||
|
||||
* __Tests.__ To automatically verify that everything is wired properly, we write end to end
|
||||
tests that navigate to various URLs and verify that the correct view was rendered.
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_06 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-7/app Live Demo
|
||||
}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-6...step-7 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_08 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 7
|
||||
@description
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_06 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-7/app Live Demo
|
||||
}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-6...step-7 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_08 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
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.
|
||||
|
||||
1. Reset your workspace to Step 7 using:
|
||||
|
||||
git checkout --force step-7
|
||||
|
||||
or
|
||||
|
||||
./goto_step.sh 7
|
||||
|
||||
2. Refresh your browser, but be sure that there is nothing in the url after app/index.html, or
|
||||
check the app out on {@link http://angular.github.com/angular-phonecat/step-7/app our server}.
|
||||
Note that you are redirected to `app/index.html#/phones` and the same phone list appears in the
|
||||
browser. When you click on a phone link the stub of a phone detail page is displayed.
|
||||
|
||||
|
||||
The most important changes are listed below. You can see the full diff on {@link
|
||||
https://github.com/angular/angular-phonecat/compare/step-6...step-7
|
||||
GitHub}:
|
||||
|
||||
## What's going on here?
|
||||
|
||||
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 the addition of 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 angular.service.$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 {@link
|
||||
http://en.wikipedia.org/wiki/Deep_linking deep linking}, which lets us utilize the browser's
|
||||
history (back and forward navigation) and bookmarks.
|
||||
|
||||
|
||||
## Controllers
|
||||
|
||||
__`app/js/controller.js`:__
|
||||
<pre>
|
||||
function PhoneCatCtrl($route) {
|
||||
var self = this;
|
||||
|
||||
$route.when('/phones',
|
||||
{template: 'partials/phone-list.html', controller: PhoneListCtrl});
|
||||
$route.when('/phones/:phoneId',
|
||||
{template: 'partials/phone-detail.html', controller: PhoneDetailCtrl});
|
||||
$route.otherwise({redirectTo: '/phones'});
|
||||
|
||||
$route.onChange(function(){
|
||||
self.params = $route.current.params;
|
||||
});
|
||||
|
||||
$route.parent(this);
|
||||
}
|
||||
|
||||
//PhoneCatCtrl.$inject = ['$route'];
|
||||
...
|
||||
</pre>
|
||||
|
||||
We created a new controller called `PhoneCatCtrl`. We declared its dependency on the `$route`
|
||||
service and used this service to declare that our application consists of two different views:
|
||||
|
||||
* The phone list view will be shown when the URL hash fragment is `/phone`. To construct this
|
||||
view, angular will use the `phone-list.html` template and the `PhoneListCtrl` controller.
|
||||
|
||||
* The phone details view will be show when the URL hash fragment matches '/phone/[phoneId]'. To
|
||||
construct this view, angular will use the `phone-detail.html` template and the `PhoneDetailCtrl`
|
||||
controller.
|
||||
|
||||
We reused the `PhoneListCtrl` controller for the first view and we added an empty
|
||||
`PhoneDetailCtrl` controller to the `app/js/controllers.js` file for the second one.
|
||||
The statement `$route.otherwise({redirectTo: '/phones'});`, triggers a redirection to `/phones`
|
||||
when none of our routes is matched.
|
||||
|
||||
Thanks to the `$route.parent(this);` statement and `ng:controller="PhoneCatCtrl"` declaration in
|
||||
the `index.html` template, the `PhoneCatCtrl` controller has a special role in our app. It is the
|
||||
"root" controller or the parent controller for the other two sub-controllers (`PhoneListCtrl` and
|
||||
`PhoneDetailCtrl`). The sub-controllers inherit the model properties and behavior from the root
|
||||
controller.
|
||||
|
||||
|
||||
Note the use of the `:phoneId` parameter in the second route declaration (`'/phones/:phoneId'`).
|
||||
When the current URL matches this route, the `$route` service extracts the phoneId string from the
|
||||
current URL and provides it to our controller via the `$route.current.params` map. We will use the
|
||||
`phoneId` parameter in the `phone-details.html` template.
|
||||
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
The `$route` service is usually used in conjunction with the {@link angular.widget.ng:view
|
||||
ng:view} widget. The role of the `ng:view` widget is to include the view template for the current
|
||||
route into the layout template, which makes it a perfect fit for our `index.html` template.
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
<body ng:controller="PhoneCatCtrl">
|
||||
|
||||
<ng:view></ng:view>
|
||||
|
||||
<script src="lib/angular/angular.js" ng:autobind></script>
|
||||
<script src="js/controllers.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</pre>
|
||||
|
||||
Note that we removed most of the code in the `index.html` template and replaced it with a single
|
||||
line containing the `ng:view` tag. The code that we removed was placed into the `phone-list.html`
|
||||
template:
|
||||
|
||||
__`app/partials/phone-list.html`:__
|
||||
<pre>
|
||||
<ul class="predicates">
|
||||
<li>
|
||||
Search: <input type="text" name="query"/>
|
||||
</li>
|
||||
<li>
|
||||
Sort by:
|
||||
<select name="orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="age">Newest</option>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="phones">
|
||||
<li ng:repeat="phone in phones.$filter(query).$orderBy(orderProp)">
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<a href="#/phones/{{phone.id}}" class="thumb"><img ng:src="{{phone.imageUrl}}"></a>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</pre>
|
||||
|
||||
We also added a placeholder template for the phone details view:
|
||||
|
||||
__`app/partials/phone-list.html`:__
|
||||
<pre>
|
||||
TBD: detail view for {{params.phoneId}}
|
||||
</pre>
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
<pre>
|
||||
...
|
||||
it('should redirect index.html to index.html#/phones', function() {
|
||||
browser().navigateTo('../../app/index.html');
|
||||
expect(browser().location().hash()).toBe('/phones');
|
||||
});
|
||||
...
|
||||
|
||||
describe('Phone detail view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser().navigateTo('../../app/index.html#/phones/nexus-s');
|
||||
});
|
||||
|
||||
|
||||
it('should display placeholder page with phoneId', function() {
|
||||
expect(binding('params.phoneId')).toBe('nexus-s');
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
|
||||
You can now refresh the browser tab with the end-to-end test runner to see the tests run, or you
|
||||
can see them running on {@link
|
||||
http://angular.github.com/angular-phonecat/step-7/test/e2e/runner.html
|
||||
angular's server}.
|
||||
|
||||
|
||||
With the routing set up and the phone list view implemented, we're ready to go to Step 8 to
|
||||
implement the phone details view.
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_06 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-7/app Live Demo
|
||||
}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-6...step-7 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_08 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -1,148 +1,200 @@
|
|||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 8
|
||||
@description
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_07 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-8/app Live Demo
|
||||
}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-7...step-8 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_09 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
In this step, we implement the Phone Details View template. Once again we will use {@link
|
||||
angular.services.$xhr $xhr} to fetch our data, and we'll flesh out the `phone-details.html` View
|
||||
template.
|
||||
|
||||
__`app/partials/phone-details.html`:__
|
||||
<pre>
|
||||
<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}}"/>
|
||||
</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>
|
||||
...
|
||||
</li>
|
||||
<span>Additional Features</span>
|
||||
<dd>{{phone.additionalFeatures}}</dd>
|
||||
</li>
|
||||
</ul>
|
||||
</pre>
|
||||
|
||||
__`app/js/controller.js`:__
|
||||
<pre>
|
||||
function PhoneCatCtrl($route) (same as Step 7)
|
||||
|
||||
function PhoneListCtrl($xhr) (same as Step 7)
|
||||
|
||||
function PhoneDetailCtrl($xhr) {
|
||||
var self = this;
|
||||
|
||||
$xhr('GET', 'phones/' + self.params.phoneId + '.json', function(code, response) {
|
||||
self.phone = response;
|
||||
});
|
||||
}
|
||||
|
||||
//PhoneDetailCtrl.$inject = ['$xhr'];
|
||||
</pre>
|
||||
|
||||
__`app/phones/nexus-s.json`:__ (sample snippet)
|
||||
<pre>
|
||||
{
|
||||
"additionalFeatures": "Contour Display, Near Field Communications (NFC), Three-axis gyroscope,
|
||||
Anti-fingerprint display coating, Internet Calling support (VoIP/SIP)",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
|
||||
__`test/unit/controllerSpec.js`:__
|
||||
<pre>
|
||||
...
|
||||
it('should fetch phone detail', function(){
|
||||
scope.params = {phoneId:'xyz'};
|
||||
$browser.xhr.expectGET('phones/xyz.json').respond({name:'phone xyz'});
|
||||
ctrl = scope.$new(PhoneDetailCtrl);
|
||||
|
||||
expect(ctrl.phone).toBeUndefined();
|
||||
$browser.xhr.flush();
|
||||
|
||||
expect(ctrl.phone).toEqual({name:'phone xyz'});
|
||||
});
|
||||
...
|
||||
</pre>
|
||||
|
||||
__`test/e2e/scenarios.js`:__
|
||||
<pre>
|
||||
...
|
||||
describe('Phone detail view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser().navigateTo('../../app/index.html#/phones/nexus-s');
|
||||
});
|
||||
|
||||
|
||||
it('should display nexus-s page', function() {
|
||||
expect(binding('phone.name')).toBe('Nexus S');
|
||||
});
|
||||
});
|
||||
...
|
||||
</pre>
|
||||
|
||||
## Discussion:
|
||||
|
||||
* Phone Details View Template. There is nothing fancy or new here, just note where we use the
|
||||
angular `{{ expression }}` markup and directives to project phone data from our model into the
|
||||
view.
|
||||
|
||||
* Note how we used the `$route` `params` object from the scope managed by the root controller
|
||||
(`PhoneCatCtrl`), to construct the path for the phone details xhr request. The rest of this step
|
||||
is simply applying the previously learned concepts and angular APIs to create a large template
|
||||
that displays a lot of data about a phone.
|
||||
|
||||
* Tests. We updated the existing end to end test and wrote a new unit test that is similar in
|
||||
spirit to the one we wrote for the `PhoneListCtrl` controller.
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_07 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-8/app Live Demo
|
||||
}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-7...step-8 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_09 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 8
|
||||
@description
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_07 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-8/app Live Demo
|
||||
}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-7...step-8 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_09 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
In this step, you will implement the phone details view, which is displayed when a user clicks on
|
||||
a phone in the phone list.
|
||||
|
||||
1. Reset your workspace to Step 8 using:
|
||||
|
||||
git checkout --force step-8
|
||||
|
||||
or
|
||||
|
||||
./goto_step.sh 8
|
||||
|
||||
2. Refresh your browser or check the app out on {@link
|
||||
http://angular.github.com/angular-phonecat/step-8/app our server}. Now 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 will use {@link angular.services.$xhr $xhr} to fetch our
|
||||
data, and we'll flesh out the `phone-details.html` view template.
|
||||
|
||||
The most important changes are listed below. You can see the full diff on {@link
|
||||
https://github.com/angular/angular-phonecat/compare/step-7...step-8
|
||||
GitHub}:
|
||||
|
||||
## Data
|
||||
|
||||
In addition to `phones.json`, the `app/phones/` directory also contains one json file for each
|
||||
phone:
|
||||
|
||||
__`app/phones/nexus-s.json`:__ (sample snippet)
|
||||
<pre>
|
||||
{
|
||||
"additionalFeatures": "Contour Display, Near Field Communications (NFC), Three-axis gyroscope,
|
||||
Anti-fingerprint display coating, Internet Calling support (VoIP/SIP)",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## Controller
|
||||
|
||||
We'll expand the `PhoneDetailCtrl` by using the `$xhr` service to fetch the json files. This works
|
||||
the same way as the phone list controller.
|
||||
|
||||
__`app/js/controller.js`:__
|
||||
<pre>
|
||||
function PhoneDetailCtrl($xhr) {
|
||||
var self = this;
|
||||
|
||||
$xhr('GET', 'phones/' + self.params.phoneId + '.json', function(code, response) {
|
||||
self.phone = response;
|
||||
});
|
||||
}
|
||||
|
||||
//PhoneDetailCtrl.$inject = ['$xhr'];
|
||||
</pre>
|
||||
|
||||
|
||||
|
||||
|
||||
## 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 `ng:repeater`s to project
|
||||
phone data from our model into the view.
|
||||
|
||||
|
||||
__`app/partials/phone-details.html`:__
|
||||
<pre>
|
||||
<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}}"/>
|
||||
</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>
|
||||
...
|
||||
</li>
|
||||
<span>Additional Features</span>
|
||||
<dd>{{phone.additionalFeatures}}</dd>
|
||||
</li>
|
||||
</ul>
|
||||
</pre>
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
We wrote a new unit test that is similar to the one we wrote for the `PhoneListCtrl` controller in
|
||||
Step 5.
|
||||
|
||||
__`test/unit/controllerSpec.js`:__
|
||||
<pre>
|
||||
...
|
||||
it('should fetch phone detail', function(){
|
||||
scope.params = {phoneId:'xyz'};
|
||||
$browser.xhr.expectGET('phones/xyz.json').respond({name:'phone xyz'});
|
||||
ctrl = scope.$new(PhoneDetailCtrl);
|
||||
|
||||
expect(ctrl.phone).toBeUndefined();
|
||||
$browser.xhr.flush();
|
||||
|
||||
expect(ctrl.phone).toEqual({name:'phone xyz'});
|
||||
});
|
||||
...
|
||||
</pre>
|
||||
|
||||
To run the unit tests, execute the `./scripts/test.sh` script and you should see the following
|
||||
output.
|
||||
|
||||
Chrome: Runner reset.
|
||||
...
|
||||
Total 3 tests (Passed: 3; Fails: 0; Errors: 0) (5.00 ms)
|
||||
Chrome 11.0.696.57 Mac OS: Run 3 tests (Passed: 3; Fails: 0; Errors 0) (5.00 ms)
|
||||
|
||||
|
||||
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`:__
|
||||
<pre>
|
||||
...
|
||||
describe('Phone detail view', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser().navigateTo('../../app/index.html#/phones/nexus-s');
|
||||
});
|
||||
|
||||
|
||||
it('should display nexus-s page', function() {
|
||||
expect(binding('phone.name')).toBe('Nexus S');
|
||||
});
|
||||
});
|
||||
...
|
||||
</pre>
|
||||
|
||||
|
||||
You can now refresh the browser tab with the end-to-end test runner to see the tests run, or you
|
||||
can see them running on {@link
|
||||
http://angular.github.com/angular-phonecat/step-8/test/e2e/runner.html
|
||||
angular's server}.
|
||||
|
||||
Now the phone details view is in place, proceed to Step 9 to learn how to write your own custom
|
||||
display filter.
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_07 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-8/app Live Demo
|
||||
}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-7...step-8 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_09 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -1,108 +1,127 @@
|
|||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 9
|
||||
@description
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_08 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-9/app Live Demo
|
||||
}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-8...step-9 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_10 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
In this step, we have determined that the built-in angular display filters ({@link
|
||||
angular.filter.number number}, {@link angular.filter.currency currency}, {@link
|
||||
angular.filter.date date}, etc.) don't handle what we want to do, so we get to create our own
|
||||
custom {@link angular.filter filter}.
|
||||
|
||||
In the previous step, the details page displayed either "true" or "false" to indicate whether
|
||||
certain phone features were present or not. Our custom "checkmark" filter replaces those text
|
||||
strings with glyphs: ✓ for "true", and ✘ for "false".
|
||||
|
||||
Our filter code lives in `app/js/filters.js`:
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
<script src="lib/angular/angular.js" ng:autobind></script>
|
||||
<script src="js/controllers.js"></script>
|
||||
<script src="app/js/filters.js"></script>
|
||||
...
|
||||
</pre>
|
||||
|
||||
In the phone details template, we employ our filter for angular expressions whose values are
|
||||
"true" or "false"; `{{ [phone_feature] | checkmark }}`:
|
||||
|
||||
__`app/partials/phone-detail.html`:__
|
||||
<pre>
|
||||
<img ng:src="{{phone.images[0].large}}" class="phone"/>
|
||||
<h1>{{phone.name}}</h1>
|
||||
<p>{{phone.description}}</p>
|
||||
...
|
||||
<ul class="specs">
|
||||
...
|
||||
<li>
|
||||
<span>Connectivity</span>
|
||||
<dl>
|
||||
<dt>Network Support</dt>
|
||||
<dd>{{phone.connectivity.cell}}</dd>
|
||||
<dt>WiFi</dt>
|
||||
<dd>{{phone.connectivity.wifi}}</dd>
|
||||
<dt>Bluetooth</dt>
|
||||
<dd>{{phone.connectivity.bluetooth}}</dd>
|
||||
<dt>Infrared</dt>
|
||||
<dd>{{phone.connectivity.infrared | checkmark}}</dd>
|
||||
<dt>GPS</dt>
|
||||
<dd>{{phone.connectivity.gps | checkmark}}</dd>
|
||||
</dl>
|
||||
</li>
|
||||
...
|
||||
</ul>
|
||||
</pre>
|
||||
|
||||
__`app/js/filters.js`:__ (New)
|
||||
<pre>
|
||||
angular.filter('checkmark', function(input) {
|
||||
return input ? '\u2713' : '\u2718';
|
||||
});
|
||||
</pre>
|
||||
|
||||
__`test/unit/filtersSpec.js`:__ (New)
|
||||
<pre>
|
||||
describe('checkmark filter', function() {
|
||||
|
||||
it('should convert boolean values to unicode checkmark or cross', function() {
|
||||
expect(angular.filter.checkmark(true)).toBe('\u2713');
|
||||
expect(angular.filter.checkmark(false)).toBe('\u2718');
|
||||
});
|
||||
})
|
||||
</pre>
|
||||
|
||||
## Discussion:
|
||||
|
||||
* This example shows how easy it is to roll your own filters for displaying data. As explained in
|
||||
the "Writing your own Filters" section of the {@link angular.filter angular.filter} page, you
|
||||
simply register your custom filter function on to the `angular.filter` function.
|
||||
|
||||
* In this example, our filter name is "checkmark"; our input is either "true" or "false", and we
|
||||
return one of two unicode characters we have chosen to represent true or false (`\u2713` and
|
||||
`\u2718`).
|
||||
|
||||
* We created a new unit test to verify that our custom filter converts boolean values to unicode
|
||||
characters.
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_08 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-9/app Live Demo
|
||||
}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-8...step-9 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_10 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 9
|
||||
@description
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_08 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-9/app Live Demo
|
||||
}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-8...step-9 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_10 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
In this step you will learn how to create your own custom display filter.
|
||||
|
||||
1. Reset your workspace to Step 9 using:
|
||||
|
||||
git checkout --force step-9
|
||||
|
||||
or
|
||||
|
||||
./goto_step.sh 9
|
||||
|
||||
2. Refresh your browser or check the app out on {@link
|
||||
http://angular.github.com/angular-phonecat/step-9/app our server}. Navigate to one of the detail
|
||||
pages.
|
||||
|
||||
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.
|
||||
|
||||
The most important changes are listed below. You can see the full diff on {@link
|
||||
https://github.com/angular/angular-phonecat/compare/step-8...step-9
|
||||
GitHub}:
|
||||
|
||||
|
||||
## Custom Filter
|
||||
|
||||
In order to create a new filter, simply register your custom filter function with the `{@link
|
||||
angular.filter angular.filter}` API.
|
||||
|
||||
__`app/js/filters.js`:__
|
||||
<pre>
|
||||
angular.filter('checkmark', function(input) {
|
||||
return input ? '\u2713' : '\u2718';
|
||||
});
|
||||
</pre>
|
||||
|
||||
The name of our filter is "checkmark". The `input` evaluates to either "true" or "false", and we
|
||||
return one of two unicode characters we have chosen to represent true or false (`\u2713` and
|
||||
`\u2718`).
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
Since the filter code lives in the `app/js/filters.js` file, we need to include this file in our
|
||||
layout template.
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
<script src="js/controllers.js"></script>
|
||||
<script src="js/filters.js"></script>
|
||||
...
|
||||
</pre>
|
||||
|
||||
The syntax for using filters in angular templates is as follows:
|
||||
|
||||
{{ expression | filter }}
|
||||
|
||||
Let's employ the filter in the phone details template:
|
||||
|
||||
|
||||
|
||||
__`app/partials/phone-detail.html`:__
|
||||
<pre>
|
||||
...
|
||||
<dl>
|
||||
<dt>Infrared</dt>
|
||||
<dd>{{phone.connectivity.infrared | checkmark}}</dd>
|
||||
<dt>GPS</dt>
|
||||
<dd>{{phone.connectivity.gps | checkmark}}</dd>
|
||||
</dl>
|
||||
...
|
||||
</pre>
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
Filters, like any other component, should be tested and these tests are very easy to write.
|
||||
|
||||
__`test/unit/filtersSpec.js`:__
|
||||
<pre>
|
||||
describe('checkmark filter', function() {
|
||||
|
||||
it('should convert boolean values to unicode checkmark or cross', function() {
|
||||
expect(angular.filter.checkmark(true)).toBe('\u2713');
|
||||
expect(angular.filter.checkmark(false)).toBe('\u2718');
|
||||
});
|
||||
})
|
||||
</pre>
|
||||
|
||||
To run the unit tests, execute the `./scripts/test.sh` script and you should see the following
|
||||
output.
|
||||
|
||||
Chrome: Runner reset.
|
||||
....
|
||||
Total 4 tests (Passed: 4; Fails: 0; Errors: 0) (3.00 ms)
|
||||
Chrome 11.0.696.57 Mac OS: Run 4 tests (Passed: 4; Fails: 0; Errors 0) (3.00 ms)
|
||||
|
||||
|
||||
Now that you have learned how to write and test a custom filter, go to Step 10 to learn how we can
|
||||
use angular to enhance the phone details page further.
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_08 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-9/app Live Demo
|
||||
}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-8...step-9 Code
|
||||
Diff}</td>
|
||||
<td id="next_step">{@link tutorial.step_10 Next}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 10
|
||||
@description
|
||||
|
|
@ -14,25 +13,29 @@ Code Diff}</td>
|
|||
</tr>
|
||||
</table>
|
||||
|
||||
In this step, you will add a clickable phone image swapper to the phone details page.
|
||||
|
||||
1. Reset your workspace to Step 10 using:
|
||||
|
||||
git checkout --force step-10
|
||||
|
||||
or
|
||||
|
||||
./goto_step.sh 10
|
||||
|
||||
2. Refresh your browser or check the app out on {@link
|
||||
http://angular.github.com/angular-phonecat/step-10/app our server}.
|
||||
|
||||
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 how we can do this with angular.
|
||||
|
||||
__`app/partials/phone-detail.html`:__
|
||||
<pre>
|
||||
<img ng:src="{{mainImageUrl}}" class="phone"/>
|
||||
The most important changes are listed below. You can see the full diff on {@link
|
||||
https://github.com/angular/angular-phonecat/compare/step-9...step-10
|
||||
GitHub}:
|
||||
|
||||
<h1>{{phone.name}}</h1>
|
||||
|
||||
<p>{{phone.description}}</p>
|
||||
|
||||
<ul class="phone-thumbs">
|
||||
<li ng:repeat="img in phone.images">
|
||||
<img ng:src="{{img}}" ng:click="setImage(img)">
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
</pre>
|
||||
## Controller
|
||||
|
||||
__`app/js/controllers.js`:__
|
||||
<pre>
|
||||
|
|
@ -53,9 +56,43 @@ function PhoneDetailCtrl($xhr) {
|
|||
//PhoneDetailCtrl.$inject = ['$xhr'];
|
||||
</pre>
|
||||
|
||||
In the `PhoneDetailCtrl` controller, the statement `self.mainImageUrl = response.images[0];`
|
||||
creates the `mainImageUrl` model property and set its default value to the first phone image url.
|
||||
|
||||
We also created a `setImage` controller method to change the value of `mainImageUrl`.
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
__`app/partials/phone-detail.html`:__
|
||||
<pre>
|
||||
<img ng:src="{{mainImageUrl}}" class="phone"/>
|
||||
|
||||
...
|
||||
|
||||
<ul class="phone-thumbs">
|
||||
<li ng:repeat="img in phone.images">
|
||||
<img ng:src="{{img}}" ng:click="setImage(img)">
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
</pre>
|
||||
|
||||
We bound the `ng:src` attribute of the large image to the `mainImageUrl` property.
|
||||
|
||||
We also registered an `{@link angular.directive.ng:click ng:click}` handler with thumbnail images.
|
||||
When a user clicks on one of the thumbnail images, the handler will use the `setImage` controller
|
||||
method to change the value of the `mainImageUrl` property to the url of the thumbnail image.
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
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`:__
|
||||
<pre>
|
||||
/* jasmine-like end2end tests go here */
|
||||
...
|
||||
describe('Phone detail view', function() {
|
||||
|
||||
|
|
@ -64,10 +101,6 @@ __`test/e2e/scenarios.js`:__
|
|||
});
|
||||
|
||||
|
||||
it('should display nexus-s page', function() {
|
||||
expect(binding('phone.name')).toBe('Nexus S');
|
||||
});
|
||||
|
||||
it('should display the first phone image as the main phone image', function() {
|
||||
expect(element('img.phone').attr('src')).toBe('img/phones/nexus-s.0.jpg');
|
||||
});
|
||||
|
|
@ -84,18 +117,14 @@ __`test/e2e/scenarios.js`:__
|
|||
});
|
||||
</pre>
|
||||
|
||||
## Discussion:
|
||||
You can now refresh the browser tab with the end-to-end test runner to see the tests run, or you
|
||||
can see them running on {@link
|
||||
http://angular.github.com/angular-phonecat/step-8/test/e2e/runner.html
|
||||
angular's server}.
|
||||
|
||||
Adding the phone image swapping feature is fairly straightforward:
|
||||
|
||||
* We defined the `mainImageUrl` model property in the details controller (`PhoneDetailCtrl`) and
|
||||
set the default value of `mainImageUrl` to the first image in the array of images.
|
||||
* We created a `setImage` controller method to change `mainImageUrl` to the image clicked on by
|
||||
the user.
|
||||
* We registered an `{@link angular.directive.ng:click ng:click}` handler for thumb images to use
|
||||
the `setImage` controller method.
|
||||
* We expanded the end-to-end test to verify that our new feature is swapping images correctly.
|
||||
|
||||
With the phone image swapper in place, we're ready for Step 11 (the last step!) to learn an even
|
||||
better way to fetch data.
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -1,178 +1,248 @@
|
|||
@workInProgress
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 11
|
||||
@description
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_10 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-11/app Live Demo
|
||||
}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-10...step-11
|
||||
Code Diff}</td>
|
||||
<td id="next_step">Next</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
And so we arrive at the last step of this tutorial. Here we define a custom service that
|
||||
represents a {@link http://en.wikipedia.org/wiki/Representational_State_Transfer RESTful} client.
|
||||
Using this client we can make xhr requests for data in an easier way, without having to deal with
|
||||
the lower-level {@link angular.service.$xhr $xhr} APIs, HTTP methods and URLs.
|
||||
|
||||
__`app/index.html`.__
|
||||
<pre>
|
||||
...
|
||||
<script src="js/services.js"></script>
|
||||
...
|
||||
</pre>
|
||||
|
||||
|
||||
__`app/js/services.js`.__ (New)
|
||||
<pre>
|
||||
angular.service('Phone', function($resource){
|
||||
return $resource('phones/:phoneId.json', {}, {
|
||||
query: {method:'GET', params:{phoneId:'phones'}, isArray:true}
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
__`app/js/controllers.js`.__
|
||||
<pre>
|
||||
...
|
||||
|
||||
function PhoneListCtrl(Phone_) {
|
||||
this.orderProp = 'age';
|
||||
this.phones = Phone_.query();
|
||||
}
|
||||
//PhoneListCtrl.$inject = ['Phone'];
|
||||
|
||||
|
||||
function PhoneDetailCtrl(Phone_) {
|
||||
this.phone = Phone_.get({phoneId:this.params.phoneId});
|
||||
}
|
||||
//PhoneDetailCtrl.$inject = ['Phone'];
|
||||
</pre>
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
<pre>
|
||||
/* jasmine specs for controllers go here */
|
||||
describe('PhoneCat controllers', function() {
|
||||
|
||||
beforeEach(function(){
|
||||
this.addMatchers({
|
||||
toEqualData: function(expected) {
|
||||
return angular.equals(this.actual, expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('PhoneListCtrl', function(){
|
||||
var scope, $browser, ctrl;
|
||||
|
||||
beforeEach(function() {
|
||||
scope = angular.scope();
|
||||
$browser = scope.$service('$browser');
|
||||
|
||||
$browser.xhr.expectGET('phones/phones.json').respond([{name: 'Nexus S'},
|
||||
{name: 'Motorola DROID'}]);
|
||||
ctrl = scope.$new(PhoneListCtrl);
|
||||
});
|
||||
|
||||
it('should create "phones" model with 2 phones fetched from xhr', function() {
|
||||
expect(ctrl.phones).toEqual([]);
|
||||
$browser.xhr.flush();
|
||||
|
||||
expect(ctrl.phones).toEqualData([{name: 'Nexus S'},
|
||||
{name: 'Motorola DROID'}]);
|
||||
});
|
||||
|
||||
it('should set the default value of orderProp model', function() {
|
||||
expect(ctrl.orderProp).toBe('age');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('PhoneDetailCtrl', function(){
|
||||
var scope, $browser, ctrl;
|
||||
|
||||
beforeEach(function() {
|
||||
scope = angular.scope();
|
||||
$browser = scope.$service('$browser');
|
||||
});
|
||||
|
||||
beforeEach(function() {
|
||||
scope = angular.scope();
|
||||
$browser = scope.$service('$browser');
|
||||
});
|
||||
|
||||
it('should fetch phone detail', function(){
|
||||
scope.params = {phoneId:'xyz'};
|
||||
$browser.xhr.expectGET('phones/xyz.json').respond({name:'phone xyz'});
|
||||
ctrl = scope.$new(PhoneDetailCtrl);
|
||||
|
||||
expect(ctrl.phone).toEqualData({});
|
||||
$browser.xhr.flush();
|
||||
|
||||
expect(ctrl.phone).toEqualData({name:'phone xyz'});
|
||||
});
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
|
||||
## Discussion:
|
||||
|
||||
* We simplified our sub-controllers (`PhoneListCtrl` and `PhoneDetailCtrl`) by factoring out the
|
||||
lower-level `$xhr` service, replacing it with a new service called `Phone`. Angular's {@link
|
||||
angular.service.$resource `$resource`} service is easier to use than `$xhr` for interacting with
|
||||
data sources exposed as RESTful resources. It is also easier now to understand what the code in
|
||||
our controllers is doing.
|
||||
|
||||
An important thing to notice in our controller code is that we don't pass any callback
|
||||
functions when invoking methods of our Phone services. 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. See? Angular tries hard to make simple
|
||||
stuff simple.
|
||||
|
||||
* Once again we make use of `$route's` params, this time to construct the URL passed as a
|
||||
parameter to `$resource` in our `services.js` script.
|
||||
|
||||
* Last, but certainly not least, we expanded and modified our unit test to verify that our new
|
||||
service is returning data as we expect it to.
|
||||
|
||||
In our assertions we use a newly-defined `toEqualData` {@link
|
||||
http://pivotal.github.com/jasmine/jsdoc/symbols/jasmine.Matchers.html Jasmine matcher}, which
|
||||
compares only object properties and ignores methods. This is necessary, because the `$resource`
|
||||
client will augment the response object with handy methods for updating and deleting the
|
||||
resource (we don't use these in our tutorial though).
|
||||
|
||||
There you have it! We have created a web app in a relatively short amount of time.
|
||||
|
||||
## Closing Notes:
|
||||
|
||||
* For more details and examples of the angular concepts we touched on in this tutorial, see the
|
||||
{@link guide Developer Guide}.
|
||||
|
||||
* For several more examples of sample code, see the {@link cookbook Cookbook}.
|
||||
|
||||
* When you are ready to start developing a project using angular, be sure to begin with the {@link
|
||||
https://github.com/angular/angular-seed angular seed app}.
|
||||
|
||||
* We hope this tutorial was useful to you, and that you learned enough about angular to make you
|
||||
want to learn more. Of course, we especially hope you are inspired to go out and develop angular
|
||||
web apps of your own, and perhaps you might even be interested in {@link contribute contributing}
|
||||
to angular.
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_10 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-11/app Live Demo
|
||||
}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-10...step-11
|
||||
Code Diff}</td>
|
||||
<td id="next_step">Next</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ngdoc overview
|
||||
@name Tutorial: Step 11
|
||||
@description
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_10 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-11/app Live Demo
|
||||
}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-10...step-11
|
||||
Code Diff}</td>
|
||||
<td id="next_step">Next</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
In this step, you will improve the way our app fetches data.
|
||||
|
||||
1. Reset your workspace to Step 11 using:
|
||||
|
||||
git checkout --force step-11
|
||||
|
||||
or
|
||||
|
||||
./goto_step.sh 11
|
||||
|
||||
2. Refresh your browser or check the app out on {@link
|
||||
http://angular.github.com/angular-phonecat/step-11/app our server}.
|
||||
|
||||
|
||||
The last improvement we will make to our app is to define a custom service that represents a
|
||||
{@link http://en.wikipedia.org/wiki/Representational_State_Transfer RESTful} client. Using this
|
||||
client we can make xhr requests for data in an easier way, without having to deal with the
|
||||
lower-level {@link angular.service.$xhr $xhr} API, HTTP methods and URLs.
|
||||
|
||||
The most important changes are listed below. You can see the full diff on {@link
|
||||
https://github.com/angular/angular-phonecat/compare/step-10...step-11
|
||||
GitHub}:
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
The custom service is defined in `app/js/services.js` so we need to include this file in our
|
||||
layout template:
|
||||
|
||||
__`app/index.html`.__
|
||||
<pre>
|
||||
...
|
||||
<script src="js/services.js"></script>
|
||||
...
|
||||
</pre>
|
||||
|
||||
## Service
|
||||
|
||||
__`app/js/services.js`.__
|
||||
<pre>
|
||||
angular.service('Phone', function($resource){
|
||||
return $resource('phones/:phoneId.json', {}, {
|
||||
query: {method:'GET', params:{phoneId:'phones'}, isArray:true}
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
We used the {@link angular.service} API to register a custom service. We passed in the name of the
|
||||
service - 'Phone' - and a factory function. The factory function is similar to a controller's
|
||||
constructor in that both can declare dependencies via function arguments. The Phone service
|
||||
declared a dependency on the `$resource` service.
|
||||
|
||||
The `{@link angular.service.$resource $resource}` service makes it easy to create a {@link
|
||||
http://en.wikipedia.org/wiki/Representational_State_Transfer RESTful} client with just a few lines
|
||||
of code. This client can then be used in our application, instead of the lower-level `$xhr`
|
||||
service.
|
||||
|
||||
|
||||
## Controller
|
||||
|
||||
We simplified our sub-controllers (`PhoneListCtrl` and `PhoneDetailCtrl`) by factoring out the
|
||||
lower-level `$xhr` service, replacing it with a new service called `Phone`. Angular's {@link
|
||||
angular.service.$resource `$resource`} service is easier to use than `$xhr` 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`.__
|
||||
<pre>
|
||||
...
|
||||
|
||||
function PhoneListCtrl(Phone_) {
|
||||
this.orderProp = 'age';
|
||||
this.phones = Phone_.query();
|
||||
}
|
||||
//PhoneListCtrl.$inject = ['Phone'];
|
||||
|
||||
|
||||
function PhoneDetailCtrl(Phone_) {
|
||||
var self = this;
|
||||
|
||||
self.phone = Phone_.get({phoneId: self.params.phoneId}, function(phone) {
|
||||
self.mainImageUrl = phone.images[0];
|
||||
});
|
||||
|
||||
...
|
||||
}
|
||||
//PhoneDetailCtrl.$inject = ['Phone'];
|
||||
</pre>
|
||||
|
||||
Notice how in `PhoneListCtrl` we replaced:
|
||||
|
||||
$xhr('GET', 'phones/phones.json', function(code, response) {
|
||||
self.phones = response;
|
||||
});
|
||||
|
||||
with:
|
||||
|
||||
this.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
|
||||
|
||||
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 `$resource` client 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` {@link http://pivotal.github.com/jasmine/jsdoc/symbols/jasmine.Matchers.html Jasmine
|
||||
matcher}. When the `toEqualData` matcher compares two objects, it takes only object properties
|
||||
into account and ignores methods.
|
||||
|
||||
|
||||
__`test/unit/controllersSpec.js`:__
|
||||
<pre>
|
||||
describe('PhoneCat controllers', function() {
|
||||
|
||||
beforeEach(function(){
|
||||
this.addMatchers({
|
||||
toEqualData: function(expected) {
|
||||
return angular.equals(this.actual, expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('PhoneListCtrl', function(){
|
||||
var scope, $browser, ctrl;
|
||||
|
||||
beforeEach(function() {
|
||||
scope = angular.scope();
|
||||
$browser = scope.$service('$browser');
|
||||
|
||||
$browser.xhr.expectGET('phones/phones.json').respond([{name: 'Nexus S'},
|
||||
{name: 'Motorola DROID'}]);
|
||||
ctrl = scope.$new(PhoneListCtrl);
|
||||
});
|
||||
|
||||
it('should create "phones" model with 2 phones fetched from xhr', function() {
|
||||
expect(ctrl.phones).toEqual([]);
|
||||
$browser.xhr.flush();
|
||||
|
||||
expect(ctrl.phones).toEqualData([{name: 'Nexus S'},
|
||||
{name: 'Motorola DROID'}]);
|
||||
});
|
||||
|
||||
it('should set the default value of orderProp model', function() {
|
||||
expect(ctrl.orderProp).toBe('age');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('PhoneDetailCtrl', function(){
|
||||
var scope, $browser, ctrl;
|
||||
|
||||
beforeEach(function() {
|
||||
scope = angular.scope();
|
||||
$browser = scope.$service('$browser');
|
||||
});
|
||||
|
||||
beforeEach(function() {
|
||||
scope = angular.scope();
|
||||
$browser = scope.$service('$browser');
|
||||
});
|
||||
|
||||
it('should fetch phone detail', function(){
|
||||
scope.params = {phoneId:'xyz'};
|
||||
$browser.xhr.expectGET('phones/xyz.json').respond({name:'phone xyz'});
|
||||
ctrl = scope.$new(PhoneDetailCtrl);
|
||||
|
||||
expect(ctrl.phone).toEqualData({});
|
||||
$browser.xhr.flush();
|
||||
|
||||
expect(ctrl.phone).toEqualData({name:'phone xyz'});
|
||||
});
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
To run the unit tests, execute the `./scripts/test.sh` script and you should see the following
|
||||
output.
|
||||
|
||||
Chrome: Runner reset.
|
||||
....
|
||||
Total 4 tests (Passed: 4; Fails: 0; Errors: 0) (3.00 ms)
|
||||
Chrome 11.0.696.57 Mac OS: Run 4 tests (Passed: 4; Fails: 0; Errors 0) (3.00 ms)
|
||||
|
||||
|
||||
There you have it! We have created a web app in a relatively short amount of time.
|
||||
|
||||
## Closing Notes:
|
||||
|
||||
* For more details and examples of the angular concepts we touched on in this tutorial, see the
|
||||
{@link guide Developer Guide}.
|
||||
|
||||
* For several more examples of code, see the {@link cookbook Cookbook}.
|
||||
|
||||
* When you are ready to start developing a project using angular, we recommend that you bootstrap
|
||||
your development with the {@link https://github.com/angular/angular-seed angular seed} project.
|
||||
|
||||
* 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 contribute contributing} to angular.
|
||||
|
||||
* If you have questions or feedback or just want to say "hi", please post a message at
|
||||
https://groups.google.com/forum/#!forum/angular.
|
||||
|
||||
<table id="tutorial_nav">
|
||||
<tr>
|
||||
<td id="previous_step">{@link tutorial.step_10 Previous}</td>
|
||||
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-11/app Live Demo
|
||||
}</td>
|
||||
<td id="tut_home">{@link tutorial Tutorial Home}</td>
|
||||
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-10...step-11
|
||||
Code Diff}</td>
|
||||
<td id="next_step">Next</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
|||
Loading…
Reference in a new issue