new batch of tutorial docs

This commit is contained in:
Igor Minar 2011-05-02 10:16:50 -07:00
parent 11e9572b95
commit 6181ca600d
13 changed files with 1900 additions and 1588 deletions

View file

@ -1,4 +1,3 @@
@workInProgress
@ngdoc overview @ngdoc overview
@name Tutorial @name Tutorial
@description @description
@ -6,63 +5,56 @@
A great way to get introduced to angular is to work through the {@link tutorial.step_00 angular 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 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 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} app}. The {@link http://angular.github.com/angular-phonecat/step-11/app/#/phones end result of our
is visually simpler, but demonstrates many of the angular features without distractions in the effort} is visually simpler, but demonstrates many of the angular features without distractions in
form of CSS code. the form of CSS code.
This tutorial app ends up like a Google phone gallery app, but is originally based on the {@link The starting point for our tutorial is the {@link https://github.com/angular/angular-seed
https://github.com/angular/angular-seed angular-seed project}. The angular seed app isn't angular-seed project}.
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.
Once you set up your tutorial environment, you should be able to get through the material in less The angular-seed project includes a simple example app, the latest angular libraries, test
than a day and you'll have fun doing it. More experienced coders may be able to zip through the libraries, and scripts. It provides all of these in an environment that is pre-configured for
exercises in an afternoon. In any case, we promise that your time will be well spent! 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: When you finish the tutorial you will be able to:
* Create a simple dynamic application that works in any browser * Create a simple dynamic application that works in any browser
* Define the differences between angular and common JavaScript frameworks * Define the differences between angular and common JavaScript frameworks
* Understand angular expressions
* Understand how data binding works in angular * Understand how data binding works in angular
* Use the angular-seed project to quickly boot-strap your own projects * Use the angular-seed project to quickly boot-strap your own projects
* Create and run tests * Create and run tests
* Identify resources for learning more about angular * 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. You can go through the whole tutorial in a couple of hours or you may want to spend a pleasant day
* <a href="#UsingSnapshots">Using Snapshots</a>. Download snapshots (files for each step of the really digging into it. In any case, we promise that your time will be well spent!
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.
<a name="PreReqs"></a> <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 To run the tutorial app and tests on your machine you will need the following:
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 * 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 `node.js` ({@link https://github.com/joyent/node/wiki/Installation node.js install}) or another
http sever (such as Apache, etc.). http sever (such as Apache, etc.).
* Java. This is required for running tests. Angular itself doesn't require Java. * Java. This is only required for if you want to run tests via JsTestDriver.
* A modern browser (including IE8+). Needed for viewing and debugging code. * A web browser.
* A text editor of your choice. * A text editor.
<a name="UsingGit"></a>
# Using Git # 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. 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 https://github.com/angular/angular-phonecat angular-phonecat} by running the following command in
a terminal: 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: Read the Tutorial Navigation section, then navigate to Step 0.
* __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".
<a name="UsingSnapshots"></a>
# Using Snapshots # Using Snapshots
Snapshots are the sets of files that reflect the state of the tutorial app at each step. These 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. 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 2. Navigate to [*the angular server*], download and then unzip [*the snapshot file*] to an
[*install-dir*] of your choosing. [*install-dir*].
3. Change directories to [*install-dir*]/sandbox. 3. Change directories to [*install-dir*]/sandbox.
4. Run the following command: cd [*install-dir*]/sandbox
* `./goto_step.sh 0`
You have to start out at the beginning, which is Step 0. After you set up Step 0, you can skip Read the Tutorial Navigation section, then navigate to step-0.
around between any steps.
1. To see the app running in your browser, do the following: # Tutorial Navigation
* __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".
* __For other http servers:__ To see the app running on the angular server, click the "Live Demo" link at the top or bottom of
1. Configure servers to serve the app and test files in the [*install-dir*]/sandbox. any tutorial page. To view the code differences between tutorial steps, click the Code Diff link
2. Start the server. at top or bottom of each tutorial page. In the Code Diff, additions are highlighted in green;
3. Navigate in your app browser to deletions are highlighted in red.
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

View file

@ -1,77 +1,90 @@
@workInProgress @ngdoc overview
@ngdoc overview @name Tutorial: Step 0
@name Tutorial: Step 0 @description
@description
<table id="tutorial_nav">
<table id="tutorial_nav"> <tr>
<tr> <td id="previous_step">{@link tutorial Previous}</td>
<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="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="tut_home">{@link tutorial Tutorial Home}</td> <td id="code_diff">Code Diff</td>
<td id="code_diff">Code Diff</td> <td id="next_step">{@link tutorial.step_0 Next}</td>
<td id="next_step">{@link tutorial.step_01 Next}</td> </tr>
</tr> </table>
</table>
You are now ready to build the phone cat application. In this step, you will become familiar with
The following sample code is our starting point. It is a static HTML page that displays next to the most important source code files, learn how to start the web services, and run the application
nothing, but it has everything we need to proceed. You can think of this bit of code as our in the browser.
prototype template, consisting of basic HTML tags with a pair of angular specific attributes.
1. Do one of the following:
__`app/index.html`:__
<pre> * Git users: In the `angular-phonecat` directory, run this command:
<!doctype html>
<html xmlns:ng="http://angularjs.org/"> git checkout step-0
<head>
<meta charset="utf-8"> * Snapshot users: In the `[install directory]/sandbox` directory, run this command:
<title>my angular app</title>
<link rel="stylesheet" href="css/app.css"/> ./goto_step.sh 0
</head>
<body> This resets your workspace to Step 0 of the tutorial app.
Nothing here yet! 2. To see the app running in a browser, do one of the following:
* __For node.js users:__
<script src="lib/angular/angular.js" ng:autobind></script> 1. In a _separate_ terminal tab or window, run `./scripts/web-server.js` to start the app
</body> server.
</html> 2. Open a browser window for the app and navigate to http://localhost:8000/app/index.html.
</pre>
* __For other http servers:__
## Discussion: 1. Configure the server to serve the files in the `angular-phonecat` directory.
2. Run `./scripts/web-server.js` to start the app server.
Although our app doesn't appear to do anything dynamic, note the following: 3. Navigate in your browser to
http://localhost:[*port-number*]/[*context-path*]/app/index.html.
* __... `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 can now see the app in the browser. It's not very exciting, but that's OK.
you are using XHTML or HTML).
The code that created this app is shown below. You will see that it creates a static HTML page
* __`<script src="lib/angular/angular.js"` ...__ This downloads the `angular.js` script and that displays "Nothing here yet!"; the code does, however, have everything we need to proceed.
registers a callback that will be executed by the browser when the containing HTML page is fully This bit of code serves as a prototype template, consisting of basic HTML tags with a pair of
downloaded. When the callback is executed, angular looks for the {@link angular-specific attributes.
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. __`app/index.html`:__
<pre>
Note: If you elected not to download any tutorial files but still want to try out some angular <!doctype html>
code on your system, you can change the relative path to the `angular.js` script in your <html xmlns:ng="http://angularjs.org/">
template from `./lib/angular/angular.js` to the following: <head>
<meta charset="utf-8">
<script src="http://code.angularjs.org/angular-0.9.14.js" ng:autobind></script> <title>my angular app</title>
<link rel="stylesheet" href="css/app.css"/>
This will download the angular script from the angular server instead of from a local file. </head>
<body>
* 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`. Nothing here yet!
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 <script src="lib/angular/angular.js" ng:autobind></script>
http://angular.github.com/angular-phonecat/step-0/app (accessible from Example link at the bottom </body>
of the page). </html>
</pre>
Now we can move on and add some content to our developing web app.
## What is the code doing?
<table id="tutorial_nav">
<tr> * __... `xmlns:ng="http://angularjs.org"` ...__ This `xmlns` declaration for the `ng` namespace
<td id="previous_step">{@link tutorial Previous}</td> must be specified in all angular applications if you use XHTML, or if you are targeting IE
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-0/app Example}</td> versions older than 9 (regardless of whether you are using XHTML or HTML).
<td id="tut_home">{@link tutorial Tutorial Home}</td>
<td id="code_diff">Code Diff</td> * __`<script src="lib/angular/angular.js"` ...__ This downloads the `angular.js` script and
<td id="next_step">{@link tutorial.step_01 Next}</td> registers a callback that will be executed by the browser when the containing HTML page is fully
</tr> downloaded. When the callback is executed, angular looks for the {@link
</table> 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>

View file

@ -1,88 +1,69 @@
@workInProgress @ngdoc overview
@ngdoc overview @name Tutorial: Step 1
@name Tutorial: Step 1 @description
@description <table id="tutorial_nav">
<table id="tutorial_nav"> <tr>
<tr> <td id="previous_step">{@link tutorial.step_00 Previous}</td>
<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
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-1/app Example}</td> Demo}</td>
<td id="tut_home">{@link tutorial Tutorial Home}</td> <td id="tut_home">{@link tutorial Tutorial Home}</td>
<td id="code_diff"> <td id="code_diff">
{@link https://github.com/angular/angular-phonecat/compare/step-0...step-1 Code Diff}</td> {@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> <td id="next_step">{@link tutorial.step_02 Next}</td>
</tr> </tr>
</table> </table>
Now that we have the basic ingredients in place, let's add some basic information about two cell In this step you will add some basic information about two cell phones to our app.
phones to our app.
1. Do one of the following to reset your workspace to Step 1; be aware that this will throw away
Note: We will usually include only the new code that we added for each step. In this and any changes you might have made to the tutorial files:
subsequent examples, we will leave out code from the previous step that hasn't changed, for
example: * Git users run:
... git checkout --force step-1
<html xmlns:ng="http://angularjs.org">
... * Snapshot users run:
Let's add the following code to `index.html`: ./goto_step.sh 1
__`app/index.html`:__ 2. Refresh your browser or check the app out on {@link
<pre> http://angular.github.com/angular-phonecat/step-1/app, our server}. Your page now contains a list
<head> with information about two phones.
...
<title>Google Phone Gallery</title> 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}:
</head>
... __`app/index.html`:__
<ul> <pre>
<li> ...
<span>Nexus S<span> <ul>
<p> <li>
Fast just got faster with Nexus S. <span>Nexus S<span>
</p> <p>
</li> Fast just got faster with Nexus S.
<li> </p>
<span>Motorola XOOM™ with Wi-Fi<span> </li>
<p> <li>
The Next, Next Generation tablet. <span>Motorola XOOM™ with Wi-Fi<span>
</p> <p>
</li> The Next, Next Generation tablet.
</ul> </p>
... </li>
</pre> </ul>
...
## Discussion: </pre>
* It's a static web page! We displayed info about two phones! Yay. 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.
* For those of you playing along at home on your own web servers, did you switch to Step 1 and
refresh your browsers? <table id="tutorial_nav">
<tr>
* __{@link tutorial Using Git:}__ <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
From your `angular-phonecat` directory, run this command: Demo}</td>
<td id="tut_home">{@link tutorial Tutorial Home}</td>
git checkout step-1 <td id="code_diff">
{@link https://github.com/angular/angular-phonecat/compare/step-0...step-1 Code Diff}</td>
* __{@link tutorial Using Snapshots:}__ <td id="next_step">{@link tutorial.step_02 Next}</td>
</tr>
From `[install directory]/sandbox`, run this command: </table>
./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>

View file

@ -1,137 +1,174 @@
@workInProgress @ngdoc overview
@ngdoc overview @name Tutorial: Step 2
@name Tutorial: Step 2 @description
@description <table id="tutorial_nav">
<table id="tutorial_nav"> <tr>
<tr> <td id="previous_step">{@link tutorial.step_01 Previous}</td>
<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
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-2/app Example}</td> Demo}</td>
<td id="tut_home">{@link tutorial Tutorial Home}</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 <td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-1...step-2 Code
Diff}</td> Diff}</td>
<td id="next_step">{@link tutorial.step_03 Next}</td> <td id="next_step">{@link tutorial.step_03 Next}</td>
</tr> </tr>
</table> </table>
In the last step, we remembered what a basic, static web page looks like, and now we want to get Now it's time to make this web page dynamic with angular. We'll also add a test that verifies the
dynamic. There are many ways to do this, but an important feature of angular is the incorporation code for the controller we are going to add.
of the principles behind {@link http://en.wikipedia.org/wiki/ModelViewController the MVC design
pattern} into client-side web apps. With that in mind, let's use a little angular and a little There are many ways to structure the code for an application. With angular, we encourage the use
JavaScript to add Model, View, and Controller components to our app, and change the static page of {@link http://en.wikipedia.org/wiki/ModelViewController the MVC design pattern} to decouple
into one that is dynamically generated. 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.
Our __View__ component is constructed by angular from this template:
1. Reset your workspace to Step 2 using:
__`app/index.html`:__
<pre> git checkout --force step-2
... or
<body ng:controller="PhoneListCtrl">
./goto_step.sh 2
<ul>
<li ng:repeat="phone in phones"> 2. Refresh your browser or check the app out on {@link
{{phone.name}} http://angular.github.com/angular-phonecat/step-2/app our server}. The app now contains a list
<p>{{phone.snippet}}</p> with 3 phones.
</li>
</ul> 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}:
<script src="lib/angular/angular.js" ng:autobind></script>
<script src="js/controllers.js"></script>
</body> ## Template for the View
...
</pre> The __View__ component is constructed by angular from this template:
Our data __Model__ (a short list of phones in object literal notation) is instantiated within our __`app/index.html`:__
__Controller__ function (`PhoneListCtrl`): <pre>
...
__`app/js/controllers.js`:__ <body ng:controller="PhoneListCtrl">
<pre>
/* App Controllers */ <ul>
<li ng:repeat="phone in phones">
function PhoneListCtrl() { {{phone.name}}
this.phones = [{"name": "Nexus S", <p>{{phone.snippet}}</p>
"snippet": "Fast just got faster with Nexus S."}, </li>
{"name": "Motorola XOOM™ with Wi-Fi", </ul>
"snippet": "The Next, Next Generation tablet."},
{"name": "MOTOROLA XOOM™", <script src="lib/angular/angular.js" ng:autobind></script>
"snippet": "The Next, Next Generation tablet."}]; <script src="js/controllers.js"></script>
} </body>
</pre> </html>
</pre>
The "Angular way" urges us to test as we develop:
We replaced the hard-coded phone list with the {@link angular.widget.@ng:repeat ng:repeat widget}
__`test/unit/controllersSpec.js`:__ and two {@link guide.expression angular expressions} enclosed in curly braces: `{{phone.name}}`
<pre> and `{{phone.snippet}}`:
/* jasmine specs for controllers go here */
describe('PhoneCat controllers', function() { * 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
describe('PhoneListCtrl', function(){ `<li>` tag as the template.
it('should create "phones" model with 3 phones', function() { * The curly braces around `phone.name` and `phone.snippet` are an example of {@link
var ctrl = new PhoneListCtrl(); angular.markup angular markup}. The curly braces are shorthand for the angular directive
expect(ctrl.phones.length).toBe(3); {@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
</pre> appropriate binding points, which updates the view.
## Discussion:
## Model and Controller
So what were our changes from Step 1?
The data __Model__ (a short list of phones in object literal notation) is instantiated within the
* __View template:__ We replaced the hard-coded phone list with the {@link __Controller__ function (`PhoneListCtrl`):
angular.widget.@ng:repeat ng:repeat widget} and two {@link guide.expression angular expressions}
enclosed in curly braces: `{{phone.name}}` and `{{phone.snippet}}`: __`app/js/controllers.js`:__
<pre>
* The `ng:repeat="phone in phones"` statement in the `<li>` tag is an angular repeater. It function PhoneListCtrl() {
tells angular to create a `<li>` element for each phone in the phones list, using the first this.phones = [{"name": "Nexus S",
`<li>` tag as the template. "snippet": "Fast just got faster with Nexus S."},
{"name": "Motorola XOOM™ with Wi-Fi",
* The curly braces around `phone.name` and `phone.snippet` are an example of {@link "snippet": "The Next, Next Generation tablet."},
angular.markup angular markup}. The curly braces are shorthand for the angular directive {"name": "MOTOROLA XOOM™",
{@link angular.directive.ng:bind ng:bind}. They indicate to angular that these are template "snippet": "The Next, Next Generation tablet."}];
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 </pre>
through the HTML template. This means that whenever the model changes, angular refreshes the
appropriate binding points, which updates the view. 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
* __Controller:__ At this point, it doesn't appear as if our controller is doing very much the model and the view. Note in the following how we connected the dots between our presentation,
controlling, but it is playing a crucial role: providing context for our data model so we can data, and logic components:
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
* The name of our controller function (in the JavaScript file `controllers.js`) matches the (`PhoneListCtrl`).
{@link angular.directive.ng:controller ng:controller} directive in the `<body>` tag * We instantiated our data within the scope of our controller function, and our template
(`PhoneListCtrl`). binding points are located within the block bounded by the `<body
* We instantiated our data within the scope of our controller function, and our template ng:controller="PhoneListCtrl>` tag.
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
Angular uses scopes, along with the information contained in the template, data model, and reflected in the view; any changes that occur in the view are reflected in the model.
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.
* __Model:__ For our data model, we created a simple array of phone records, specified in object
literal notation. ## Tests
* __Testing:__ Ease of testing is another cornerstone of angular's design philosophy. All we are The "Angular way" makes it easy for us to test as we develop; the unit test for your newly created
doing here is showing how easy it is to create a unit test using the technology baked into controller looks as follows:
angular. The test verifies that we have 3 records in the phones array.
__`test/unit/controllersSpec.js`:__
To run this test, make sure you have a {@link tutorial test server running}, and type <pre>
`./scripts/test.sh` from the command line. describe('PhoneCat controllers', function() {
Angular developers prefer the syntax of Jasmine's Behavior-driven Development (BDD) framework describe('PhoneListCtrl', function(){
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/ it('should create "phones" model with 3 phones', function() {
Jasmine home page} and on the {@link https://github.com/pivotal/jasmine/wiki Jasmine wiki}. var ctrl = new PhoneListCtrl();
expect(ctrl.phones.length).toBe(3);
<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> </pre>
<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 Ease of testing is another cornerstone of angular's design philosophy. All we are doing here is
Diff}</td> showing how easy it is to create a unit test. The test verifies that we have 3 records in the
<td id="next_step">{@link tutorial.step_03 Next}</td> phones array.
</tr>
</table> 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>

View file

@ -1,108 +1,142 @@
@workInProgress @ngdoc overview
@ngdoc overview @name Tutorial: Step 3
@name Tutorial: Step 3 @description
@description <table id="tutorial_nav">
<table id="tutorial_nav"> <tr>
<tr> <td id="previous_step">{@link tutorial.step_02 Previous}</td>
<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
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-3/app Example}</td> Demo}</td>
<td id="tut_home">{@link tutorial Tutorial Home}</td> <td id="tut_home">{@link tutorial Tutorial Home}</td>
<td id="code_diff">{@link <td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-2...step-3 Code
https://github.com/angular/angular-phonecat/commit/a03815f8fb00217f5f9c1d3ef83282f79818e706 Code Diff}</td>
Diff}</td> <td id="next_step">{@link tutorial.step_04 Next}</td>
<td id="next_step">{@link tutorial.step_04 Next}</td> </tr>
</tr> </table>
</table>
We did a lot of work in laying a foundation for the app in the last step, so now we'll do
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 (yes, it will be simple!). We will also write an
something simple, and add full text search. We will also write an end-to-end test, because a good end-to-end test, because a good end-to-end test is a good friend. It stays with your app, keeps an
end-to-end test is a good friend. It stays with your app, keeps an eye on it, and quickly detects eye on it, and quickly detects regressions.
regressions.
1. Reset your workspace to Step 3 using:
__`app/index.html`:__
<pre> git checkout --force step-3
...
Fulltext Search: <input name="query"/> or
<ul class="phones"> ./goto_step.sh 3
<li ng:repeat="phone in phones.$filter(query)">
{{phone.name}} 2. Refresh your browser or check the app out on {@link
<p>{{phone.snippet}}</p> http://angular.github.com/angular-phonecat/step-3/app our server}. The app now has a search box.
</li> The phone list on the page changes depending on what a user types into the search box.
</ul>
... The most important changes are listed below. You can see the full diff on {@link
</pre> https://github.com/angular/angular-phonecat/compare/step-2...step-3
__`test/e2e/scenarios.js`:__ GitHub}:
<pre>
/* jasmine-like end2end tests go here */
describe('PhoneCat App', function() { ## Controller
describe('Phone list view', function() { We made no changes to the controller.
beforeEach(function() {
browser().navigateTo('../../app/index.html'); ## Template
});
__`app/index.html`:__
<pre>
it('should filter the phone list as user types into the search box', function() { ...
expect(repeater('.phones li').count()).toBe(3); Fulltext Search: <input name="query"/>
input('query').enter('nexus'); <ul class="phones">
expect(repeater('.phones li').count()).toBe(1); <li ng:repeat="phone in phones.$filter(query)">
{{phone.name}}
input('query').enter('motorola'); <p>{{phone.snippet}}</p>
expect(repeater('.phones li').count()).toBe(2); </li>
}); </ul>
}); ...
}); </pre>
</pre>
We added a standard HTML `<input>` tag and use angular's {@link angular.Array.filter $filter}
## Discussion: function to process the input for the `ng:repeater`.
We continued using the same controller that we set up in Step 2, but we added the following This lets a user enter search criteria and immediately see the effects of their search on the
features to our app: phone list. This new code demonstrates the following:
* __Search Box:__ A standard HTML `<input>` tag combined with angular's {@link * Data-binding. This is one of the core features in angular. When the page loads, angular binds
angular.Array.filter $filter} utility (added to the repeater) lets a user type in search criteria the name of the input box to a variable of the same name in the data model and keeps the two in
and immediately see the effects of their search on the phone list. This new code demonstrates the sync.
following:
In this code, the data that a user types into the input box (named __`query`__) is immediately
* Two way Data-binding. This is one of the core features in angular. When the page loads, available as a filter input in the list repeater (`phone in phones.$filter(`__`query`__`)`).
angular binds the name of the input box to a variable of the same name in the data model and When changes to the data model cause the repeater's input to change, the repeater efficiently
keeps the two in sync. updates the DOM to reflect the current state of the model.
In this example, the data that you type into the input box (named __`query`__) is immediately * Use of `$filter`. The `{@link angular.Array.filter $filter}` method, uses the `query` value, to
available as a filter input in the list repeater (`phone in phones.$filter(`__`query`__`)`). create a new array that contains only those records that match the `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. * `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.
* 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 ## Test
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.) 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
* `ng:repeat` automatically shrinks and grows the number of phones in the View, via DOM test DOM manipulation or the wiring of our application. For these, an end-to-end test is a much
manipulation that is completely transparent to the developer. If you've written any DOM better choice.
manipulation code, this should make you happy.
The search feature was fully implemented via templates and data-binding, so we'll write our first
* __CSS:__ We added in some minimal CSS to the file we set up in Step 0: `./css/app.css`. end-to-end test, to verify that the feature works.
* __Testing:__ To run the end to end test, open http://localhost:8000/test/e2e/runner.html in __`test/e2e/scenarios.js`:__
your browser. This end-to-end test shows the following: <pre>
describe('PhoneCat App', function() {
* Proof that the search box and the repeater are correctly wired together.
describe('Phone list view', function() {
* 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. beforeEach(function() {
browser().navigateTo('../../app/index.html');
<table id="tutorial_nav"> });
<tr>
<td id="previous_step">{@link tutorial.step_02 Previous}</td> it('should filter the phone list as user types into the search box', function() {
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-3/app Example}</td> expect(repeater('.phones li').count()).toBe(3);
<td id="tut_home">{@link tutorial Tutorial Home}</td>
<td id="code_diff">{@link input('query').enter('nexus');
https://github.com/angular/angular-phonecat/commit/a03815f8fb00217f5f9c1d3ef83282f79818e706 Code expect(repeater('.phones li').count()).toBe(1);
Diff}</td>
<td id="next_step">{@link tutorial.step_04 Next}</td> input('query').enter('motorola');
</tr> expect(repeater('.phones li').count()).toBe(2);
</table> });
});
});
</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>

View file

@ -1,161 +1,201 @@
@workInProgress @ngdoc overview
@ngdoc overview @name Tutorial: Step 4
@name Tutorial: Step 4 @description
@description <table id="tutorial_nav">
<table id="tutorial_nav"> <tr>
<tr> <td id="previous_step">{@link tutorial.step_03 Previous}</td>
<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="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="tut_home">{@link tutorial Tutorial Home}</td> <td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-3...step-4 Code
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-3...step-4 Code Diff}</td>
Diff}</td> <td id="next_step">{@link tutorial.step_05 Next}</td>
<td id="next_step">{@link tutorial.step_05 Next}</td> </tr>
</tr> </table>
</table>
In this step, you will add a feature to let your users select the order of the items in the phone
In this step, we add a feature that lets our users choose which way to order the phone list. 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.
__`app/index.html`:__
<pre>
... 1. Reset your workspace to Step 4 using:
<ul class="predicates">
<li> git checkout --force step-4
Search: <input type="text" name="query"/>
</li> or
<li>
Sort by: ./goto_step.sh 4
<select name="orderProp">
<option value="name">Alphabetical</option> 2. Refresh your browser or check the app out on {@link
<option value="age">Newest</option> http://angular.github.com/angular-phonecat/step-4/app our server}. You should see that in addition
</select> to the search box, the app displays a drop down menu that allows users to control the order in
</li> which the phones are listed.
</ul>
The most important changes are listed below. You can see the full diff on {@link
<ul class="phones"> https://github.com/angular/angular-phonecat/compare/step-3...step-4
<li ng:repeat="phone in phones.$filter(query).$orderBy(orderProp)"> GitHub}:
{{phone.name}}
<p>{{phone.snippet}}</p>
</li> ## Template
</ul>
... __`app/index.html`:__
</pre> <pre>
...
__`app/js/controller.js`:__ <ul class="predicates">
<pre> <li>
/* App Controllers */ Search: <input type="text" name="query"/>
</li>
function PhoneListCtrl() { <li>
this.phones = [{"name": "Nexus S", Sort by:
"snippet": "Fast just got faster with Nexus S.", <select name="orderProp">
"age": 0}, <option value="name">Alphabetical</option>
{"name": "Motorola XOOM™ with Wi-Fi", <option value="age">Newest</option>
"snippet": "The Next, Next Generation tablet.", </select>
"age": 1}, </li>
{"name": "MOTOROLA XOOM™", </ul>
"snippet": "The Next, Next Generation tablet.",
"age": 2}]; <ul class="phones">
<li ng:repeat="phone in phones.$filter(query).$orderBy(orderProp)">
this.orderProp = 'age'; {{phone.name}}
} <p>{{phone.snippet}}</p>
</pre> </li>
</ul>
__`test/unit/controllerSpec.js`:__ ...
<pre> </pre>
/* jasmine specs for controllers go here */
describe('PhoneCat controllers', function() { In the `index.html` template we made the following changes:
describe('PhoneListCtrl', function(){ * First, we added a `<select>` html element named `orderProp`, so that our users can pick from the
var scope, $browser, ctrl; two provided sorting options.
beforeEach(function() { * We then chained the `$filter` method with `{@link angular.Array.orderBy $orderBy}` method to
ctrl = new PhoneListCtrl(); 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.
it('should create "phones" model with 3 phones', function() {
expect(ctrl.phones.length).toBe(3); 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!
it('should set the default value of orderProp model', function() {
expect(ctrl.orderProp).toBe('age');
});
}); ## Controller
});
</pre> __`app/js/controller.js`:__
<pre>
__`test/e2e/scenarios.js`:__ /* App Controllers */
<pre>
/* jasmine-like end2end tests go here */ function PhoneListCtrl() {
describe('PhoneCat App', function() { this.phones = [{"name": "Nexus S",
"snippet": "Fast just got faster with Nexus S.",
describe('Phone list view', function() { "age": 0},
{"name": "Motorola XOOM™ with Wi-Fi",
beforeEach(function() { "snippet": "The Next, Next Generation tablet.",
browser().navigateTo('../../app/index.html'); "age": 1},
}); {"name": "MOTOROLA XOOM™",
"snippet": "The Next, Next Generation tablet.",
"age": 2}];
it('should filter the phone list as user types into the search box', function() {
expect(repeater('.phones li').count()).toBe(3); this.orderProp = 'age';
}
input('query').enter('nexus'); </pre>
expect(repeater('.phones li').count()).toBe(1);
* We modified the `phones` model - the array of phones - and added an `age` property to each phone
input('query').enter('motorola'); record. This property is used to order phones by age.
expect(repeater('.phones li').count()).toBe(2);
}); * 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.
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')). ## Test
toEqual(["Motorola XOOM\u2122 with Wi-Fi",
"MOTOROLA XOOM\u2122"]); 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.
select('orderProp').option('alphabetical');
__`test/unit/controllerSpec.js`:__
expect(repeater('.phones li', 'Phone List').column('a')). <pre>
toEqual(["MOTOROLA XOOM\u2122", /* jasmine specs for controllers go here */
"Motorola XOOM\u2122 with Wi-Fi"]); describe('PhoneCat controllers', function() {
});
}); describe('PhoneListCtrl', function(){
}); var scope, $browser, ctrl;
</pre>
beforeEach(function() {
## Discussion: ctrl = new PhoneListCtrl();
});
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:
it('should create "phones" model with 3 phones', function() {
* First, we provide a `<select>` element named `orderProp` for our users so they can choose to expect(ctrl.phones.length).toBe(3);
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 it('should set the default value of orderProp model', function() {
method available on array objects in angular expressions. In our UI template, we set up a select expect(ctrl.orderProp).toBe('age');
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 </pre>
don't override the default value, angular uses the value of the first `<option>` element when it
initializes the data model.
The unit test now verifies that the default ordering property is set.
* Our unit test now verifies that our default ordering property is set.
We used Jasmine's API to extract the controller construction into a `beforeEach` block, which is
* We added an end-to-end test to verify that our select box ordering mechanism works properly. shared by all tests in the nearest `describe` block.
* Once again we added a little more CSS to improve the View. To run the unit tests, once again execute the `./scripts/test.sh` script and you should see the
following output.
<table id="tutorial_nav">
<tr> Chrome: Runner reset.
<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> Total 2 tests (Passed: 2; Fails: 0; Errors: 0) (3.00 ms)
<td id="tut_home">{@link tutorial Tutorial Home}</td> Chrome 11.0.696.57 Mac OS: Run 2 tests (Passed: 2; Fails: 0; Errors 0) (3.00 ms)
<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> Let's turn our attention to the end-to-end test.
</tr>
</table> __`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>

View file

@ -1,147 +1,219 @@
@workInProgress @ngdoc overview
@ngdoc overview @name Tutorial: Step 5
@name Tutorial: Step 5 @description
@description <table id="tutorial_nav">
<table id="tutorial_nav"> <tr>
<tr> <td id="previous_step">{@link tutorial.step_04 Previous}</td>
<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 id="step_result">{@link http://angular.github.com/angular-phonecat/step-5/app Example}</td> }</td>
<td id="tut_home">{@link tutorial Tutorial Home}</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 <td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-4...step-5 Code
Diff}</td> Diff}</td>
<td id="next_step">{@link tutorial.step_06 Next}</td> <td id="next_step">{@link tutorial.step_06 Next}</td>
</tr> </tr>
</table> </table>
In this step, the View template remains the same but the Model and Controller change. We'll Enough of building an app with three phones in a hard-coded dataset! Let's fetch a larger dataset
introduce the use of an angular {@link angular.service service}, which we will use to implement an from our server using one of angular's built-in {@link angular.service services} called {@link
`XMLHttpRequest` request to communicate with a server. Angular provides the built-in {@link angular.service.$xhr $xhr}. We will use angular's dependency injection to provide the service to
angular.service.$xhr $xhr} service to make this easy. the `PhoneListCtrl` controller.
The addition of the `$xhr` service to our app gives us the opportunity to talk about {@link 1. Reset your workspace to Step 5 using:
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 git checkout --force step-5
test.
or
__`app/js/controllers.js:`__
<pre> ./goto_step.sh 5
/* App Controllers */
2. Refresh your browser or check the app out on {@link
function PhoneListCtrl($xhr) { http://angular.github.com/angular-phonecat/step-5/app our server}. You should now see a list of 20
var self = this; phones.
$xhr('GET', 'phones/phones.json', function(code, response) {
self.phones = response; 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}:
self.orderProp = 'age';
} ## Data
//PhoneListCtrl.$inject = ['$xhr']; The `app/phones/phone.json` file in your project is a dataset that contains a larger list of
</pre> phones stored in the JSON format.
__`test/unit/controllerSpec.js`:__ Following is a sample of the file:
<pre> <pre>
/* jasmine specs for controllers go here */ [
describe('PhoneCat controllers', function() { {
"age": 13,
describe('PhoneListCtrl', function(){ "id": "motorola-defy-with-motoblur",
var scope, $browser, ctrl; "name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
"snippet": "Are you ready for everything life throws your way?"
beforeEach(function() { ...
scope = angular.scope(); },
$browser = scope.$service('$browser'); ...
]
$browser.xhr.expectGET('phones/phones.json').respond([{name: 'Nexus S'}, </pre>
{name: 'Motorola DROID'}]);
ctrl = scope.$new(PhoneListCtrl);
}); ## Controller
In this step, the view template will remain the same but the model and controller will change.
it('should create "phones" model with 2 phones fetched from xhr', function() { We'll use angular's {@link angular.service.$xhr} service to make an HTTP request to your web
expect(ctrl.phones).toBeUndefined(); server to fetch the data in the `phones.json` file.
$browser.xhr.flush();
__`app/js/controllers.js:`__
expect(ctrl.phones).toEqual([{name: 'Nexus S'}, <pre>
{name: 'Motorola DROID'}]); function PhoneListCtrl($xhr) {
}); var self = this;
$xhr('GET', 'phones/phones.json', function(code, response) {
it('should set the default value of orderProp model', function() { self.phones = response;
expect(ctrl.orderProp).toBe('age'); });
});
}); self.orderProp = 'age';
}); }
</pre>
//PhoneListCtrl.$inject = ['$xhr'];
## Discussion: </pre>
* __Services:__ {@link angular.service Services} are substitutable objects managed by angular's We removed the hard-coded dataset from the controller and instead are using the `$xhr` service to
{@link guide.di DI subsystem}. Angular services simplify some of the standard operations common access the data stored in `app/phones/phones.json`. The `$xhr` service makes a HTTP GET request to
to web apps. Angular provides several built-in services (such as {@link angular.service.$xhr our web server, asking for `phone/phones.json` (the url is relative to our `index.html` file). The
$xhr}). You can also create your own custom services. server responds by providing the data in the json file.
* __Dependency Injection:__ To use an angular service, you simply provide the name of the service Keep in mind that the response might just as well have been dynamically generated by a backend
as an argument to the controller's constructor function. The name of the argument is significant, server. To the browser and our app they both look the same. For the sake of simplicity we used a
because angular's {@link guide.di DI subsystem} recognizes the identity of a service by its name, json file in this tutorial.
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 Notice that the `$xhr` service takes a callback as the last parameter. This callback is used to
(services often depend upon other services). 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
Note: if you minify the javascript code for this controller, all function arguments will be the response? Angular took care of that for us.
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 We already mentioned that the `$xhr` function we just used is an angular service. {@link
into the `$inject` property of the controller function. angular.service Angular services} are substitutable objects managed by angular's {@link guide.di
DI subsystem}.
* __`$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 Dependency injection helps to make your web apps well structured, loosely coupled, and much easier
HTTP request to our web server, asking for `phone/phones.json` (the url is relative to our to test. What's important to understand is how the controllers get access to these services
`index.html` file). The server responds with the contents of the json file, which serves as the through dependency injection.
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 The dependency injection pattern is based on declaring the dependencies we require and letting the
a real backend server to generate a response would make our tutorial unnecessarily complicated. 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:
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 function PhoneListCtrl($xhr) {
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. 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
* __Testing:__ The unit tests have been expanded. Because of the dependency injection business, controller when the controller is being constructed. The dependency injector also takes care of
we now need to create the controller the same way that angular does it behind the scenes. For this creating any transitive dependencies the service may have (services often depend upon other
reason, we need to: services).
* Create a root scope object by calling `angular.scope()` 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
* Call `scope.$new(PhoneListCtrl)` to get angular to create the child scope associated with controller, all of these function arguments would be minified as well, and the dependency injector
our controller. would not being able to identify services correctly.
At the same time, we need to tell the testing harness that it should expect an incoming To overcome issues caused by minification, just assign an array with service identifier strings
request from our controller. To do this we: into the `$inject` property of the controller function, just like the last line in the snippet
(commented out) suggests:
* 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 PhoneListCtrl.$inject = ['$xhr'];
of this service that allows you to write tests without having to deal with these native APIs
and the global state associated with them.
## Test
* 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 __`test/unit/controllersSpec.js`:__
we call the `$browser.xhr.flush()` method. <pre>
describe('PhoneCat controllers', function() {
* We then make assertions to verify that the `phones` model doesn't exist on the scope, before
the response is received. describe('PhoneListCtrl', function(){
var scope, $browser, ctrl;
* 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. beforeEach(function() {
scope = angular.scope();
* Finally, we make the assertions, verifying that the phone model now exists on the scope. $browser = scope.$service('$browser');
<table id="tutorial_nav"> $browser.xhr.expectGET('phones/phones.json').respond([{name: 'Nexus S'},
<tr> {name: 'Motorola DROID'}]);
<td id="previous_step">{@link tutorial.step_04 Previous}</td> ctrl = scope.$new(PhoneListCtrl);
<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> it('should create "phones" model with 2 phones fetched from xhr', function() {
<td id="next_step">{@link tutorial.step_06 Next}</td> expect(ctrl.phones).toBeUndefined();
</tr> $browser.xhr.flush();
</table>
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>

View file

@ -1,113 +1,119 @@
@workInProgress @ngdoc overview
@ngdoc overview @name Tutorial: Step 6
@name Tutorial: Step 6 @description
@description <table id="tutorial_nav">
<table id="tutorial_nav"> <tr>
<tr> <td id="previous_step">{@link tutorial.step_05 Previous}</td>
<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 id="step_result">{@link http://angular.github.com/angular-phonecat/step-6/app Example}</td> }</td>
<td id="tut_home">{@link tutorial Tutorial Home}</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 <td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-5...step-6 Code
Diff}</td> Diff}</td>
<td id="next_step">{@link tutorial.step_07 Next}</td> <td id="next_step">{@link tutorial.step_07 Next}</td>
</tr> </tr>
</table> </table>
In this step, we add thumbnail images, links, and a little more CSS to our app. For now, our In this step, you will add thumbnail images for the phones in the phone list, and links that, for
links go nowhere. One step at a time; in the next step we'll implement new views that these links now, will go nowhere. In subsequent steps you will use the links to display additional information
will open. about the phones in the catalog.
__`app/index.html`:__ 1. Reset your workspace to Step 6 using:
<pre>
... git checkout --force step-6
<ul class="predicates">
<li> or
Search: <input type="text" name="query"/>
</li> ./goto_step.sh 6
<li>
Sort by: 2. Refresh your browser or check the app out on {@link
<select name="orderProp"> http://angular.github.com/angular-phonecat/step-6/app our server}. You should now see links and
<option value="name">Alphabetical</option> images of the phones in the list.
<option value="age">Newest</option>
</select> The most important changes are listed below. You can see the full diff on {@link
</li> https://github.com/angular/angular-phonecat/compare/step-5...step-6
</ul> GitHub}:
<ul class="phones">
<li ng:repeat="phone in phones.$filter(query).$orderBy(orderProp)"> ## Data
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
<a href="#/phones/{{phone.id}}" class="thumb"><img ng:src="{{phone.imageUrl}}"></a> Note that the `phones.json` file contains unique ids and image urls for each of the phones. The
<p>{{phone.snippet}}</p> urls point to the `app/img/phones/` directory.
</li>
</ul> __`app/phones/phones.json`__ (sample snippet):
... <pre>
</pre> [
{
__`app/js/controller.js`__ (Unchanged): ...
<pre> "id": "motorola-defy-with-motoblur",
/* App Controllers */ "imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg",
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
function PhoneListCtrl($xhr) { ...
var self = this; },
...
$xhr('GET', 'phones/phones.json', function(code, response) { ]
self.phones = response; </pre>
});
self.orderProp = 'age'; ## Template
}
__`app/index.html`:__
//PhoneListCtrl.$inject = ['$xhr']; <pre>
</pre> ...
<ul class="phones">
__`app/phones/phones.json`__ (sample snippet): <li ng:repeat="phone in phones.$filter(query).$orderBy(orderProp)">
<pre> <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>
"age": 4, </li>
... </ul>
"carrier": "T-Mobile", ...
"id": "motorola-defy-with-motoblur", </pre>
"imageUrl": "http://google.com/phone/image/small/640001",
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122", To dynamically generate links that will in the future lead to phone detail pages, we used the
"snippet": "Are you ready for everything life throws your way?" 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.
]
</pre> 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
__`test/e2e/scenarios.js`__: angular `{{ exppression }}` markup literally, which it would have done if we had only specified an
<pre> 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.
it('should render phone specific links', function() {
input('query').enter('nexus');
element('.phones li a').click(); ## Test
expect(browser().location().hash()).toBe('/phones/nexus-s');
}); __`test/e2e/scenarios.js`__:
... <pre>
</pre> ...
it('should render phone specific links', function() {
## Discussion: input('query').enter('nexus');
element('.phones li a').click();
* Note that we're using {@link guide.expression angular expressions} enclosed in the now-familiar expect(browser().location().hash()).toBe('/phones/nexus-s');
{@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. ...
</pre>
* 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 We added a new end-to-end test to verify that the app is generating correct links to the phone
literally, as it would do if we tried to use markup in a regular `src` attribute. Use `ng:src` to views that we will implement in the upcoming steps.
keep the browser from eagerly making an extra http request to an invalid location.
You can now refresh the browser tab with the end-to-end test runner to see the tests run, or you
* We expanded our end-to-end test to verify that the app is generating correct links to the phone can see them running on {@link
views we will implement in the upcoming steps. http://angular.github.com/angular-phonecat/step-6/test/e2e/runner.html
angular's server}.
<table id="tutorial_nav">
<tr> Now that you have added phone images and links, go to Step 7 to learn about angular layout
<td id="previous_step">{@link tutorial.step_05 Previous}</td> templates and how angular makes it easy to create applications that have multiple views.
<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 <table id="tutorial_nav">
Diff}</td> <tr>
<td id="next_step">{@link tutorial.step_07 Next}</td> <td id="previous_step">{@link tutorial.step_05 Previous}</td>
</tr> <td id="step_result">{@link http://angular.github.com/angular-phonecat/step-6/app Live Demo
</table> }</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>

View file

@ -1,181 +1,211 @@
@workInProgress @ngdoc overview
@ngdoc overview @name Tutorial: Step 7
@name Tutorial: Step 7 @description
@description <table id="tutorial_nav">
<table id="tutorial_nav"> <tr>
<tr> <td id="previous_step">{@link tutorial.step_06 Previous}</td>
<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 id="step_result">{@link http://angular.github.com/angular-phonecat/step-7/app Live Demo }</td>
}</td> <td id="tut_home">{@link tutorial Tutorial Home}</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
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-6...step-7 Code Diff}</td>
Diff}</td> <td id="next_step">{@link tutorial.step_08 Next}</td>
<td id="next_step">{@link tutorial.step_08 Next}</td> </tr>
</tr> </table>
</table>
In this step, you will learn how to create a layout template and how to build an app that has
Our app is slowly growing and becoming more complex. Up until now, the app provided our users with multiple views by adding routing.
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 1. Reset your workspace to Step 7 using:
detailed information about each of the devices in our list.
git checkout --force step-7
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` or
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 ./goto_step.sh 7
on the current "route" — the view that is currently displayed to the user.
2. Refresh your browser, but be sure that there is nothing in the url after app/index.html, or
Similarly as with templates, angular also allows for controllers and scopes managed by these check the app out on {@link http://angular.github.com/angular-phonecat/step-7/app our server}.
controllers to be nested. We are going to create a "root" controller called `PhoneCatCtrl`, which Note that you are redirected to `app/index.html#/phones` and the same phone list appears in the
will contain the declaration of routes for the application. browser. When you click on a phone link the stub of a phone detail page is displayed.
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 The most important changes are listed below. You can see the full diff on {@link
location in the browser. Using this feature we can implement {@link https://github.com/angular/angular-phonecat/compare/step-6...step-7
http://en.wikipedia.org/wiki/Deep_linking deep linking}, which lets us utilize the browser's GitHub}:
History, and Back and Forward browser navigation.
## What's going on here?
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. Our app is slowly growing and becoming more complex. Before Step 7, the app provided our users
Each view will have the template stored in a separate file in the `app/partials/` directory. with a single view (the list of all phones), and all of the template code was located in the
Similarly each view will have a controller associated with it. These will be stored in the `index.html` file. The next step in building the app is the addition of a view that will show
existing `app/js/controllers.js` file. detailed information about each of the devices in our list.
The `$route` service is usually used in conjunction with the {@link angular.widget.ng:view To add the detailed view, we could expand the `index.html` file to contain template code for both
ng:view} widget. The role of the `ng:view` widget is to include the view template for the current views, but that would get messy very quickly. Instead, we are going to turn the `index.html`
route into the layout template, which makes it a perfect fit for our `index.html` template. 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
For now we are going to get all the routing going, and move the phone listing template into a on the current "route" — the view that is currently displayed to the user.
separate file. We'll save the implementation of the phone details View for the next step.
Application routes in angular are declared via the {@link angular.service.$route $route} service.
__`app/index.html`:__ This service makes it easy to wire together controllers, view templates, and the current URL
<pre> 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
<body ng:controller="PhoneCatCtrl"> history (back and forward navigation) and bookmarks.
<ng:view></ng:view>
## Controllers
<script src="lib/angular/angular.js" ng:autobind></script>
<script src="js/controllers.js"></script> __`app/js/controller.js`:__
</body> <pre>
</html> function PhoneCatCtrl($route) {
</pre> var self = this;
__`app/partials/phone-list.html`:__ $route.when('/phones',
<pre> {template: 'partials/phone-list.html', controller: PhoneListCtrl});
<ul class="predicates"> $route.when('/phones/:phoneId',
<li> {template: 'partials/phone-detail.html', controller: PhoneDetailCtrl});
Search: <input type="text" name="query"/> $route.otherwise({redirectTo: '/phones'});
</li>
<li> $route.onChange(function(){
Sort by: self.params = $route.current.params;
<select name="orderProp"> });
<option value="name">Alphabetical</option>
<option value="age">Newest</option> $route.parent(this);
</select> }
</li>
</ul> //PhoneCatCtrl.$inject = ['$route'];
...
<ul class="phones"> </pre>
<li ng:repeat="phone in phones.$filter(query).$orderBy(orderProp)">
<a href="#/phones/{{phone.id}}">{{phone.name}}</a> We created a new controller called `PhoneCatCtrl`. We declared its dependency on the `$route`
<a href="#/phones/{{phone.id}}" class="thumb"><img ng:src="{{phone.imageUrl}}"></a> service and used this service to declare that our application consists of two different views:
<p>{{phone.snippet}}</p>
</li> * The phone list view will be shown when the URL hash fragment is `/phone`. To construct this
</ul> view, angular will use the `phone-list.html` template and the `PhoneListCtrl` controller.
</pre>
* The phone details view will be show when the URL hash fragment matches '/phone/[phoneId]'. To
__`app/partials/phone-list.html`:__ construct this view, angular will use the `phone-detail.html` template and the `PhoneDetailCtrl`
<pre> controller.
TBD: detail view for {{params.phoneId}}
</pre> 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.
__`app/js/controller.js`:__ The statement `$route.otherwise({redirectTo: '/phones'});`, triggers a redirection to `/phones`
<pre> when none of our routes is matched.
/* App Controllers */
Thanks to the `$route.parent(this);` statement and `ng:controller="PhoneCatCtrl"` declaration in
function PhoneCatCtrl($route) { the `index.html` template, the `PhoneCatCtrl` controller has a special role in our app. It is the
var self = this; "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
$route.when('/phones', controller.
{template: 'partials/phone-list.html', controller: PhoneListCtrl});
$route.when('/phones/:phoneId',
{template: 'partials/phone-detail.html', controller: PhoneDetailCtrl}); Note the use of the `:phoneId` parameter in the second route declaration (`'/phones/:phoneId'`).
$route.otherwise({redirectTo: '/phones'}); 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
$route.onChange(function(){ `phoneId` parameter in the `phone-details.html` template.
self.params = $route.current.params;
});
$route.parent(this); ## Template
}
The `$route` service is usually used in conjunction with the {@link angular.widget.ng:view
//PhoneCatCtrl.$inject = ['$route']; 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.
function PhoneListCtrl($xhr) { __`app/index.html`:__
var self = this; <pre>
...
$xhr('GET', 'phones/phones.json', function(code, response) { <body ng:controller="PhoneCatCtrl">
self.phones = response;
}); <ng:view></ng:view>
self.orderProp = 'age'; <script src="lib/angular/angular.js" ng:autobind></script>
} <script src="js/controllers.js"></script>
</body>
//PhoneListCtrl.$inject = ['$xhr']; </html>
</pre>
function PhoneDetailCtrl() {} Note that we removed most of the code in the `index.html` template and replaced it with a single
</pre> line containing the `ng:view` tag. The code that we removed was placed into the `phone-list.html`
template:
## Discussion:
__`app/partials/phone-list.html`:__
* __The View.__ Our View template in `index.html` has been reduced down to this: <pre>
`<ng:view></ng:view>`. As described above, it is now a "layout template". We added the following <ul class="predicates">
two new View templates: <li>
Search: <input type="text" name="query"/>
* `app/partials/phone-list.html` for the phone list. The phone-list view was formerly our </li>
main view. We simply moved the code from `index.html` to here. <li>
Sort by:
* `app/partials/phone-detail.html` for the phone details (just a placeholder template for now). <select name="orderProp">
<option value="name">Alphabetical</option>
* __The Controller(s).__ We now have a new root controller (`PhoneCatCtrl`) and two <option value="age">Newest</option>
sub-controllers (`PhoneListCtrl` and `PhoneDetailCtrl`). These inherit the model properties and </select>
behavior from the root controller. </li>
</ul>
* __`$route.`__ The root controller's job now is to set up the `$route` configuration:
<ul class="phones">
* When the fragment part of the URL in the browser ends in "/phones", `$route` service <li ng:repeat="phone in phones.$filter(query).$orderBy(orderProp)">
grabs the `phone-list.html` template, compiles it, and links it with a new scope that is <a href="#/phones/{{phone.id}}">{{phone.name}}</a>
controlled by our `PhoneListCtrl` controller. <a href="#/phones/{{phone.id}}" class="thumb"><img ng:src="{{phone.imageUrl}}"></a>
<p>{{phone.snippet}}</p>
* When the URL ends in "/phones/:phoneId", `$route` compiles and links the </li>
`phone-detail.html` template as it did with `phone-list.html`. But note the use of the </ul>
`:phoneId` parameter declaration in the `path` argument of `$route.when()`: `$route` </pre>
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 We also added a placeholder template for the phone details view:
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. __`app/partials/phone-list.html`:__
<pre>
* Any other URL fragment gets redirected to `/phones`. TBD: detail view for {{params.phoneId}}
</pre>
* __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 ## Test
and accessible by these controllers.
To automatically verify that everything is wired properly, we wrote end to end tests that navigate
* __Tests.__ To automatically verify that everything is wired properly, we write end to end to various URLs and verify that the correct view was rendered.
tests that navigate to various URLs and verify that the correct view was rendered.
<pre>
<table id="tutorial_nav"> ...
<tr> it('should redirect index.html to index.html#/phones', function() {
<td id="previous_step">{@link tutorial.step_06 Previous}</td> browser().navigateTo('../../app/index.html');
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-7/app Live Demo expect(browser().location().hash()).toBe('/phones');
}</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> describe('Phone detail view', function() {
<td id="next_step">{@link tutorial.step_08 Next}</td>
</tr> beforeEach(function() {
</table> 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>

View file

@ -1,148 +1,200 @@
@workInProgress @ngdoc overview
@ngdoc overview @name Tutorial: Step 8
@name Tutorial: Step 8 @description
@description <table id="tutorial_nav">
<table id="tutorial_nav"> <tr>
<tr> <td id="previous_step">{@link tutorial.step_07 Previous}</td>
<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 id="step_result">{@link http://angular.github.com/angular-phonecat/step-8/app Live Demo }</td>
}</td> <td id="tut_home">{@link tutorial Tutorial Home}</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
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-7...step-8 Code Diff}</td>
Diff}</td> <td id="next_step">{@link tutorial.step_09 Next}</td>
<td id="next_step">{@link tutorial.step_09 Next}</td> </tr>
</tr> </table>
</table>
In this step, you will implement the phone details view, which is displayed when a user clicks on
In this step, we implement the Phone Details View template. Once again we will use {@link a phone in the phone list.
angular.services.$xhr $xhr} to fetch our data, and we'll flesh out the `phone-details.html` View
template. 1. Reset your workspace to Step 8 using:
__`app/partials/phone-details.html`:__ git checkout --force step-8
<pre>
<img ng:src="{{phone.images[0]}}" class="phone"/> or
<h1>{{phone.name}}</h1> ./goto_step.sh 8
<p>{{phone.description}}</p> 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
<ul class="phone-thumbs"> on the list, the phone details page with phone-specific information is displayed.
<li ng:repeat="img in phone.images">
<img ng:src="{{img}}"/>
</li> To implement the phone details view we will use {@link angular.services.$xhr $xhr} to fetch our
</ul> data, and we'll flesh out the `phone-details.html` view template.
<ul class="specs"> The most important changes are listed below. You can see the full diff on {@link
<li> https://github.com/angular/angular-phonecat/compare/step-7...step-8
<span>Availability and Networks</span> GitHub}:
<dl>
<dt>Availability</dt> ## Data
<dd ng:repeat="availability in phone.availability">{{availability}}</dd>
</dl> In addition to `phones.json`, the `app/phones/` directory also contains one json file for each
</li> phone:
...
</li> __`app/phones/nexus-s.json`:__ (sample snippet)
<span>Additional Features</span> <pre>
<dd>{{phone.additionalFeatures}}</dd> {
</li> "additionalFeatures": "Contour Display, Near Field Communications (NFC), Three-axis gyroscope,
</ul> Anti-fingerprint display coating, Internet Calling support (VoIP/SIP)",
</pre> "android": {
"os": "Android 2.3",
__`app/js/controller.js`:__ "ui": "Android"
<pre> },
function PhoneCatCtrl($route) (same as Step 7) ...
"images": [
function PhoneListCtrl($xhr) (same as Step 7) "img/phones/nexus-s.0.jpg",
"img/phones/nexus-s.1.jpg",
function PhoneDetailCtrl($xhr) { "img/phones/nexus-s.2.jpg",
var self = this; "img/phones/nexus-s.3.jpg"
],
$xhr('GET', 'phones/' + self.params.phoneId + '.json', function(code, response) { "storage": {
self.phone = response; "flash": "16384MB",
}); "ram": "512MB"
} }
}
//PhoneDetailCtrl.$inject = ['$xhr']; </pre>
</pre>
__`app/phones/nexus-s.json`:__ (sample snippet) Each of these files describes various properties of the phone using the same data structure. We'll
<pre> show this data in the phone detail view.
{
"additionalFeatures": "Contour Display, Near Field Communications (NFC), Three-axis gyroscope,
Anti-fingerprint display coating, Internet Calling support (VoIP/SIP)", ## Controller
"android": {
"os": "Android 2.3", We'll expand the `PhoneDetailCtrl` by using the `$xhr` service to fetch the json files. This works
"ui": "Android" the same way as the phone list controller.
},
... __`app/js/controller.js`:__
"images": [ <pre>
"img/phones/nexus-s.0.jpg", function PhoneDetailCtrl($xhr) {
"img/phones/nexus-s.1.jpg", var self = this;
"img/phones/nexus-s.2.jpg",
"img/phones/nexus-s.3.jpg" $xhr('GET', 'phones/' + self.params.phoneId + '.json', function(code, response) {
], self.phone = response;
"storage": { });
"flash": "16384MB", }
"ram": "512MB"
} //PhoneDetailCtrl.$inject = ['$xhr'];
} </pre>
</pre>
__`test/unit/controllerSpec.js`:__
<pre>
... ## Template
it('should fetch phone detail', function(){
scope.params = {phoneId:'xyz'}; The TBD placeholder line has been replaced with lists and bindings that comprise the phone
$browser.xhr.expectGET('phones/xyz.json').respond({name:'phone xyz'}); details. Note where we use the angular `{{ expression }}` markup and `ng:repeater`s to project
ctrl = scope.$new(PhoneDetailCtrl); phone data from our model into the view.
expect(ctrl.phone).toBeUndefined();
$browser.xhr.flush(); __`app/partials/phone-details.html`:__
<pre>
expect(ctrl.phone).toEqual({name:'phone xyz'}); <img ng:src="{{phone.images[0]}}" class="phone"/>
});
... <h1>{{phone.name}}</h1>
</pre>
<p>{{phone.description}}</p>
__`test/e2e/scenarios.js`:__
<pre> <ul class="phone-thumbs">
... <li ng:repeat="img in phone.images">
describe('Phone detail view', function() { <img ng:src="{{img}}"/>
</li>
beforeEach(function() { </ul>
browser().navigateTo('../../app/index.html#/phones/nexus-s');
}); <ul class="specs">
<li>
<span>Availability and Networks</span>
it('should display nexus-s page', function() { <dl>
expect(binding('phone.name')).toBe('Nexus S'); <dt>Availability</dt>
}); <dd ng:repeat="availability in phone.availability">{{availability}}</dd>
}); </dl>
... </li>
</pre> ...
</li>
## Discussion: <span>Additional Features</span>
<dd>{{phone.additionalFeatures}}</dd>
* Phone Details View Template. There is nothing fancy or new here, just note where we use the </li>
angular `{{ expression }}` markup and directives to project phone data from our model into the </ul>
view. </pre>
* 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 ## Test
is simply applying the previously learned concepts and angular APIs to create a large template
that displays a lot of data about a phone. We wrote a new unit test that is similar to the one we wrote for the `PhoneListCtrl` controller in
Step 5.
* 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. __`test/unit/controllerSpec.js`:__
<pre>
<table id="tutorial_nav"> ...
<tr> it('should fetch phone detail', function(){
<td id="previous_step">{@link tutorial.step_07 Previous}</td> scope.params = {phoneId:'xyz'};
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-8/app Live Demo $browser.xhr.expectGET('phones/xyz.json').respond({name:'phone xyz'});
}</td> ctrl = scope.$new(PhoneDetailCtrl);
<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 expect(ctrl.phone).toBeUndefined();
Diff}</td> $browser.xhr.flush();
<td id="next_step">{@link tutorial.step_09 Next}</td>
</tr> expect(ctrl.phone).toEqual({name:'phone xyz'});
</table> });
...
</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>

View file

@ -1,108 +1,127 @@
@workInProgress @ngdoc overview
@ngdoc overview @name Tutorial: Step 9
@name Tutorial: Step 9 @description
@description <table id="tutorial_nav">
<table id="tutorial_nav"> <tr>
<tr> <td id="previous_step">{@link tutorial.step_08 Previous}</td>
<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 id="step_result">{@link http://angular.github.com/angular-phonecat/step-9/app Live Demo }</td>
}</td> <td id="tut_home">{@link tutorial Tutorial Home}</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
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-8...step-9 Code Diff}</td>
Diff}</td> <td id="next_step">{@link tutorial.step_10 Next}</td>
<td id="next_step">{@link tutorial.step_10 Next}</td> </tr>
</tr> </table>
</table>
In this step you will learn how to create your own custom display filter.
In this step, we have determined that the built-in angular display filters ({@link
angular.filter.number number}, {@link angular.filter.currency currency}, {@link 1. Reset your workspace to Step 9 using:
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}. git checkout --force step-9
In the previous step, the details page displayed either "true" or "false" to indicate whether or
certain phone features were present or not. Our custom "checkmark" filter replaces those text
strings with glyphs: ✓ for "true", and ✘ for "false". ./goto_step.sh 9
Our filter code lives in `app/js/filters.js`: 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
__`app/index.html`:__ pages.
<pre>
... In the previous step, the details page displayed either "true" or "false" to indicate whether
<script src="lib/angular/angular.js" ng:autobind></script> certain phone features were present or not. We have used a custom filter to convert those text
<script src="js/controllers.js"></script> strings into glyphs: ✓ for "true", and ✘ for "false". Let's see, what the filter code looks like.
<script src="app/js/filters.js"></script>
... The most important changes are listed below. You can see the full diff on {@link
</pre> https://github.com/angular/angular-phonecat/compare/step-8...step-9
GitHub}:
In the phone details template, we employ our filter for angular expressions whose values are
"true" or "false"; `{{ [phone_feature] | checkmark }}`:
## Custom Filter
__`app/partials/phone-detail.html`:__
<pre> In order to create a new filter, simply register your custom filter function with the `{@link
<img ng:src="{{phone.images[0].large}}" class="phone"/> angular.filter angular.filter}` API.
<h1>{{phone.name}}</h1>
<p>{{phone.description}}</p> __`app/js/filters.js`:__
... <pre>
<ul class="specs"> angular.filter('checkmark', function(input) {
... return input ? '\u2713' : '\u2718';
<li> });
<span>Connectivity</span> </pre>
<dl>
<dt>Network Support</dt> The name of our filter is "checkmark". The `input` evaluates to either "true" or "false", and we
<dd>{{phone.connectivity.cell}}</dd> return one of two unicode characters we have chosen to represent true or false (`\u2713` and
<dt>WiFi</dt> `\u2718`).
<dd>{{phone.connectivity.wifi}}</dd>
<dt>Bluetooth</dt>
<dd>{{phone.connectivity.bluetooth}}</dd> ## Template
<dt>Infrared</dt>
<dd>{{phone.connectivity.infrared | checkmark}}</dd> Since the filter code lives in the `app/js/filters.js` file, we need to include this file in our
<dt>GPS</dt> layout template.
<dd>{{phone.connectivity.gps | checkmark}}</dd>
</dl> __`app/index.html`:__
</li> <pre>
... ...
</ul> <script src="js/controllers.js"></script>
</pre> <script src="js/filters.js"></script>
...
__`app/js/filters.js`:__ (New) </pre>
<pre>
angular.filter('checkmark', function(input) { The syntax for using filters in angular templates is as follows:
return input ? '\u2713' : '\u2718';
}); {{ expression | filter }}
</pre>
Let's employ the filter in the phone details template:
__`test/unit/filtersSpec.js`:__ (New)
<pre>
describe('checkmark filter', function() {
__`app/partials/phone-detail.html`:__
it('should convert boolean values to unicode checkmark or cross', function() { <pre>
expect(angular.filter.checkmark(true)).toBe('\u2713'); ...
expect(angular.filter.checkmark(false)).toBe('\u2718'); <dl>
}); <dt>Infrared</dt>
}) <dd>{{phone.connectivity.infrared | checkmark}}</dd>
</pre> <dt>GPS</dt>
<dd>{{phone.connectivity.gps | checkmark}}</dd>
## Discussion: </dl>
...
* This example shows how easy it is to roll your own filters for displaying data. As explained in </pre>
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.
## Test
* 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 Filters, like any other component, should be tested and these tests are very easy to write.
`\u2718`).
__`test/unit/filtersSpec.js`:__
* We created a new unit test to verify that our custom filter converts boolean values to unicode <pre>
characters. describe('checkmark filter', function() {
<table id="tutorial_nav"> it('should convert boolean values to unicode checkmark or cross', function() {
<tr> expect(angular.filter.checkmark(true)).toBe('\u2713');
<td id="previous_step">{@link tutorial.step_08 Previous}</td> expect(angular.filter.checkmark(false)).toBe('\u2718');
<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> </pre>
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-8...step-9 Code
Diff}</td> To run the unit tests, execute the `./scripts/test.sh` script and you should see the following
<td id="next_step">{@link tutorial.step_10 Next}</td> output.
</tr>
</table> 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>

View file

@ -1,4 +1,3 @@
@workInProgress
@ngdoc overview @ngdoc overview
@name Tutorial: Step 10 @name Tutorial: Step 10
@description @description
@ -14,25 +13,29 @@ Code Diff}</td>
</tr> </tr>
</table> </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 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 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. clicking on the desired thumbnail image. Let's have a look how we can do this with angular.
__`app/partials/phone-detail.html`:__ The most important changes are listed below. You can see the full diff on {@link
<pre> https://github.com/angular/angular-phonecat/compare/step-9...step-10
<img ng:src="{{mainImageUrl}}" class="phone"/> GitHub}:
<h1>{{phone.name}}</h1>
<p>{{phone.description}}</p> ## Controller
<ul class="phone-thumbs">
<li ng:repeat="img in phone.images">
<img ng:src="{{img}}" ng:click="setImage(img)">
</li>
</ul>
...
</pre>
__`app/js/controllers.js`:__ __`app/js/controllers.js`:__
<pre> <pre>
@ -53,9 +56,43 @@ function PhoneDetailCtrl($xhr) {
//PhoneDetailCtrl.$inject = ['$xhr']; //PhoneDetailCtrl.$inject = ['$xhr'];
</pre> </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`:__ __`test/e2e/scenarios.js`:__
<pre> <pre>
/* jasmine-like end2end tests go here */
... ...
describe('Phone detail view', function() { 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() { 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'); expect(element('img.phone').attr('src')).toBe('img/phones/nexus-s.0.jpg');
}); });
@ -84,18 +117,14 @@ __`test/e2e/scenarios.js`:__
}); });
</pre> </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"> <table id="tutorial_nav">
<tr> <tr>

View file

@ -1,178 +1,248 @@
@workInProgress @ngdoc overview
@ngdoc overview @name Tutorial: Step 11
@name Tutorial: Step 11 @description
@description <table id="tutorial_nav">
<table id="tutorial_nav"> <tr>
<tr> <td id="previous_step">{@link tutorial.step_10 Previous}</td>
<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 id="step_result">{@link http://angular.github.com/angular-phonecat/step-11/app Live Demo }</td>
}</td> <td id="tut_home">{@link tutorial Tutorial Home}</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
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-10...step-11 Code Diff}</td>
Code Diff}</td> <td id="next_step">Next</td>
<td id="next_step">Next</td> </tr>
</tr> </table>
</table>
In this step, you will improve the way our app fetches data.
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. 1. Reset your workspace to Step 11 using:
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. git checkout --force step-11
__`app/index.html`.__ or
<pre>
... ./goto_step.sh 11
<script src="js/services.js"></script>
... 2. Refresh your browser or check the app out on {@link
</pre> http://angular.github.com/angular-phonecat/step-11/app our server}.
__`app/js/services.js`.__ (New) The last improvement we will make to our app is to define a custom service that represents a
<pre> {@link http://en.wikipedia.org/wiki/Representational_State_Transfer RESTful} client. Using this
angular.service('Phone', function($resource){ client we can make xhr requests for data in an easier way, without having to deal with the
return $resource('phones/:phoneId.json', {}, { lower-level {@link angular.service.$xhr $xhr} API, HTTP methods and URLs.
query: {method:'GET', params:{phoneId:'phones'}, isArray:true}
}); 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
</pre> GitHub}:
__`app/js/controllers.js`.__
<pre> ## Template
...
The custom service is defined in `app/js/services.js` so we need to include this file in our
function PhoneListCtrl(Phone_) { layout template:
this.orderProp = 'age';
this.phones = Phone_.query(); __`app/index.html`.__
} <pre>
//PhoneListCtrl.$inject = ['Phone']; ...
<script src="js/services.js"></script>
...
function PhoneDetailCtrl(Phone_) { </pre>
this.phone = Phone_.get({phoneId:this.params.phoneId});
} ## Service
//PhoneDetailCtrl.$inject = ['Phone'];
</pre> __`app/js/services.js`.__
<pre>
__`test/unit/controllersSpec.js`:__ angular.service('Phone', function($resource){
<pre> return $resource('phones/:phoneId.json', {}, {
/* jasmine specs for controllers go here */ query: {method:'GET', params:{phoneId:'phones'}, isArray:true}
describe('PhoneCat controllers', function() { });
});
beforeEach(function(){ </pre>
this.addMatchers({
toEqualData: function(expected) { We used the {@link angular.service} API to register a custom service. We passed in the name of the
return angular.equals(this.actual, expected); 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
describe('PhoneListCtrl', function(){ http://en.wikipedia.org/wiki/Representational_State_Transfer RESTful} client with just a few lines
var scope, $browser, ctrl; of code. This client can then be used in our application, instead of the lower-level `$xhr`
service.
beforeEach(function() {
scope = angular.scope();
$browser = scope.$service('$browser'); ## Controller
$browser.xhr.expectGET('phones/phones.json').respond([{name: 'Nexus S'}, We simplified our sub-controllers (`PhoneListCtrl` and `PhoneDetailCtrl`) by factoring out the
{name: 'Motorola DROID'}]); lower-level `$xhr` service, replacing it with a new service called `Phone`. Angular's {@link
ctrl = scope.$new(PhoneListCtrl); 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.
it('should create "phones" model with 2 phones fetched from xhr', function() {
expect(ctrl.phones).toEqual([]); __`app/js/controllers.js`.__
$browser.xhr.flush(); <pre>
...
expect(ctrl.phones).toEqualData([{name: 'Nexus S'},
{name: 'Motorola DROID'}]); function PhoneListCtrl(Phone_) {
}); this.orderProp = 'age';
this.phones = Phone_.query();
it('should set the default value of orderProp model', function() { }
expect(ctrl.orderProp).toBe('age'); //PhoneListCtrl.$inject = ['Phone'];
});
});
function PhoneDetailCtrl(Phone_) {
var self = this;
describe('PhoneDetailCtrl', function(){
var scope, $browser, ctrl; self.phone = Phone_.get({phoneId: self.params.phoneId}, function(phone) {
self.mainImageUrl = phone.images[0];
beforeEach(function() { });
scope = angular.scope();
$browser = scope.$service('$browser'); ...
}); }
//PhoneDetailCtrl.$inject = ['Phone'];
beforeEach(function() { </pre>
scope = angular.scope();
$browser = scope.$service('$browser'); Notice how in `PhoneListCtrl` we replaced:
});
$xhr('GET', 'phones/phones.json', function(code, response) {
it('should fetch phone detail', function(){ self.phones = response;
scope.params = {phoneId:'xyz'}; });
$browser.xhr.expectGET('phones/xyz.json').respond({name:'phone xyz'});
ctrl = scope.$new(PhoneDetailCtrl); with:
expect(ctrl.phone).toEqualData({}); this.phones = Phone_.query();
$browser.xhr.flush();
This is a simple statement that we want to query for all phones.
expect(ctrl.phone).toEqualData({name:'phone xyz'});
}); 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
</pre> 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.
## Discussion:
Sometimes, relying on the future object and data-binding alone is not sufficient to do everything
* We simplified our sub-controllers (`PhoneListCtrl` and `PhoneDetailCtrl`) by factoring out the we require, so in these cases, we can add a callback to process the server response. The
lower-level `$xhr` service, replacing it with a new service called `Phone`. Angular's {@link `PhoneDetailCtrl` controller illustrates this by setting the `mainImageUrl` in a callback.
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 ## Test
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 We have modified our unit tests to verify that our new service is issuing HTTP requests and
object, which will be filled with data when the xhr response returns. Because of the processing them as expected. The tests also check that our controllers are interacting with the
data-binding in angular, we can use this future and bind it to our template. Then, when the service correctly.
data arrives, the view will automatically update. See? Angular tries hard to make simple
stuff simple. 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
* Once again we make use of `$route's` params, this time to construct the URL passed as a values would not match the responses exactly. To solve the problem, we use a newly-defined
parameter to `$resource` in our `services.js` script. `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
* Last, but certainly not least, we expanded and modified our unit test to verify that our new into account and ignores methods.
service is returning data as we expect it to.
In our assertions we use a newly-defined `toEqualData` {@link __`test/unit/controllersSpec.js`:__
http://pivotal.github.com/jasmine/jsdoc/symbols/jasmine.Matchers.html Jasmine matcher}, which <pre>
compares only object properties and ignores methods. This is necessary, because the `$resource` describe('PhoneCat controllers', function() {
client will augment the response object with handy methods for updating and deleting the
resource (we don't use these in our tutorial though). beforeEach(function(){
this.addMatchers({
There you have it! We have created a web app in a relatively short amount of time. toEqualData: function(expected) {
return angular.equals(this.actual, expected);
## Closing Notes: }
});
* For more details and examples of the angular concepts we touched on in this tutorial, see the });
{@link guide Developer Guide}.
describe('PhoneListCtrl', function(){
* For several more examples of sample code, see the {@link cookbook Cookbook}. var scope, $browser, ctrl;
* When you are ready to start developing a project using angular, be sure to begin with the {@link beforeEach(function() {
https://github.com/angular/angular-seed angular seed app}. scope = angular.scope();
$browser = scope.$service('$browser');
* 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 $browser.xhr.expectGET('phones/phones.json').respond([{name: 'Nexus S'},
web apps of your own, and perhaps you might even be interested in {@link contribute contributing} {name: 'Motorola DROID'}]);
to angular. ctrl = scope.$new(PhoneListCtrl);
});
<table id="tutorial_nav">
<tr> it('should create "phones" model with 2 phones fetched from xhr', function() {
<td id="previous_step">{@link tutorial.step_10 Previous}</td> expect(ctrl.phones).toEqual([]);
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-11/app Live Demo $browser.xhr.flush();
}</td>
<td id="tut_home">{@link tutorial Tutorial Home}</td> expect(ctrl.phones).toEqualData([{name: 'Nexus S'},
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-10...step-11 {name: 'Motorola DROID'}]);
Code Diff}</td> });
<td id="next_step">Next</td>
</tr> it('should set the default value of orderProp model', function() {
</table> 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>