add new batch of tutorial docs and images

This commit is contained in:
Igor Minar 2011-05-10 17:45:42 -07:00
parent 3776e08db0
commit 3751f172b3
22 changed files with 1088 additions and 365 deletions

View file

@ -2,105 +2,133 @@
@name Tutorial
@description
A great way to get introduced to angular is to work through the {@link tutorial.step_00 angular
tutorial}, which walks you through the construction of an angular web app. The app you will build
in the tutorial is loosely based on the {@link http://www.google.com/phone/ Google phone gallery
app}. The {@link http://angular.github.com/angular-phonecat/step-11/app/#/phones end result of our
effort} is visually simpler, but demonstrates many of the angular features without distractions in
the form of CSS code.
The starting point for our tutorial is the {@link https://github.com/angular/angular-seed
angular-seed project}.
A great way to get introduced to angular is to work through this tutorial, which walks you through
the construction of an angular web app. The app you will build is a catalog that displays a list of
Android devices, lets you filter the list to see only devices that interest you, and then view
details for any device.
The angular-seed project includes a simple example app, the latest angular libraries, test
libraries, and scripts. It provides all of these in an environment that is pre-configured for
developing a typical web app. For this tutorial, we modified the angular-seed as follows:
* Removed the example app
* Added phone images to `app/img/phones`
* Added phone data files (JSON) to `app/phones`
<img src="img/tutorial/catalog_screen.png">
As you work through this tutorial, you will learn how angular makes browsers smarter — without the
use of extensions or plugins.
* You will see examples of how to use client-side data binding and dependency injection to build
dynamic views of data that change immediately in response to user actions.
* You will see how Angular creates listeners on your data without the need for DOM manipulation.
* You will learn a better, easier way to test your web apps.
* You will learn how to use Angular services to make common web tasks, such as getting data into
your app, easier.
And all of this works in any browser without modifications!
**Note**: Using the angular seed app isn't required for building angular apps, but doing so helps
you get started quickly and makes the development and testing process much easier.
When you finish the tutorial you will be able to:
* Create a simple dynamic application that works in any browser
* Create a dynamic application that works in any browser
* Define the differences between angular and common JavaScript frameworks
* Understand how data binding works in angular
* Use the angular-seed project to quickly boot-strap your own projects
* Create and run tests
* Identify resources for learning more about angular
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.
Git or the snapshots described below. Windows users will be able read the tutorial but won't be
able to run the tests or experiment with the code.
You can go through the whole tutorial in a couple of hours or you may want to spend a pleasant day
really digging into it. In any case, we promise that your time will be well spent!
really digging into it. If you're looking for a shorter introduction to angular, check out {@link
http://docs.angularjs.org/#!started started}.
<a name="PreReqs"></a>
# Prerequisites
To run the tutorial app and tests on your machine you will need the following:
* A Mac or Linux machine (required by the tutorial scripts, not angular)
* An http server running on your system. If you don't already have one installed, you can install
`node.js` ({@link https://github.com/joyent/node/wiki/Installation node.js install guide}) or
another http sever (such as Apache, etc.).
* Java. This is required for running tests with JsTestDriver.
* A Mac or Linux machine (required for running the tutorial scripts)
* An http server running on your system. Mac and Linux machines typically have Apache preinstalled.
If you don't already have an http server installed, you can install `node.js` ({@link
https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager node.js install guide})
or another http sever.
* {@link http://java.com Java}.
* A web browser.
* A text editor.
# Working with the code
There are two ways that you can you follow this tutorial and hack on the code:
## Using Git
The following instructions are for developers who are comfortable with the Git versioning system:
The following instructions are for Git users. If you're not a Git user, skip down to the "Using
Snapshots" section.
1. Check to be sure you have all of the <a href="#PreReqs">prerequisites</a> on your system.
2. Clone the angular-phonecat repository located at {@link
https://github.com/angular/angular-phonecat GitHub} by running the following command in a terminal:
git clone git://github.com/angular/angular-phonecat.git
This will create a directory called `angular-phonecat` in the current directory.
3. Change your current directory to `angular-phonecat`.
This will create a directory called `angular-phonecat` in the current directory.
3. Change your current directory to `angular-phonecat`.
cd angular-phonecat
cd angular-phonecat
The tutorial instructions assume you are running all commands from this directory.
## Using Snapshots
Snapshots are the sets of files that reflect the state of the tutorial app at each step. These
files include the HTML, CSS, and JavaScript for the app, plus Jasmine JavaScript files and Java
libraries for the test stack. These will let you run the tutorial app and tests, without requiring
knowledge of Git. You can download and install the snapshot files as follows:
1. Check to be sure you have all of the <a href="#PreReqs">prerequisites</a> on your system.
2. {@link TODO Download the zip archive} with all files and unzip them into `[tutorial-dir]`
directory.
2. {@link http://code.angularjs.org/angular-phonecat-snapshots.zip Download the zip archive} with
all files and unzip them into `[tutorial-dir]` directory.
3. Change directories to `[tutorial-dir]/sandbox`.
cd [tutorial-dir]/sandbox
# Tutorial Navigation
To see the app running on the angular server, click the "Live Demo" link at the top or bottom of
any tutorial page. To view the code differences between tutorial steps, click the Code Diff link
at top or bottom of each tutorial page. In the Code Diff, additions are highlighted in green;
deletions are highlighted in red.
Let's get going and proceed to {@link tutorial/step_00 step 0}.
Let's get going with {@link tutorial/step_00 step 0}.

View file

@ -1,7 +1,8 @@
@ngdoc overview
@ngdoc overview
@name Tutorial: Step 0
@description
<table id="tutorial_nav">
<tr>
<td id="previous_step">{@link tutorial Previous}</td>
@ -12,39 +13,54 @@
</tr>
</table>
You are now ready to build the phone cat application. In this step, you will become familiar with
the most important source code files, learn how to start the development servers bundled with
angular-seed, and run the application in the browser.
1. Do one of the following:
* Git users: In the `angular-phonecat` directory, run this command:
git checkout step-0
git checkout -f step-0
* Snapshot users: In the `[tutorial-dir]/sandbox` directory, run this command:
./goto_step.sh 0
This resets your workspace to Step 0 of the tutorial app.
This resets your workspace to Step 0 of the tutorial app.
You must repeat this for every future step in the tutorial and change the number to the number of
the step you are on. Either command will cause any changes you made within your working directory
to be lost.
2. To see the app running in a browser, do one of the following:
* __For node.js users:__
1. In a _separate_ terminal tab or window, run `./scripts/web-server.js` to start the app
server.
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. Navigate in your browser to
http://localhost:[*port-number*]/[*context-path*]/app/index.html.
http://localhost:[*port-number*]/[*context-path*]/app/index.html.
You can now see the app in the browser. It's not very exciting, but that's OK.
The code that created this app is shown below. You will see that it creates a static HTML page
that displays "Nothing here yet!"; the code does, however, have everything we need to proceed.
This bit of code serves as a prototype template, consisting of basic HTML tags with a pair of
angular-specific attributes.
You can now see the page in your browser. It's not very exciting, but that's OK.
The static HTML page that displays "Nothing here yet!" was constructed with the HTML code shown
below. The code contains some key angular elements that we will need going forward.
__`app/index.html`:__
<pre>
@ -57,29 +73,84 @@ __`app/index.html`:__
</head>
<body>
Nothing here yet!
<script src="lib/angular/angular.js" ng:autobind></script>
</body>
</html>
</pre>
## What is the code doing?
* __... `xmlns:ng="http://angularjs.org"` ...__ This `xmlns` declaration for the `ng` namespace
must be specified in all angular applications if you use XHTML, or if you are targeting IE
versions older than 9 (regardless of whether you are using XHTML or HTML).
* __`<script src="lib/angular/angular.js"` ...__ This downloads the `angular.js` script and
registers a callback that will be executed by the browser when the containing HTML page is fully
downloaded. When the callback is executed, angular looks for the {@link
angular.directive.ng:autobind ng:autobind} attribute. If `ng:autobind` is found, it signals
angular to bootstrap, compile, and manage the whole html page.
* xmlns declaration
<html xmlns:ng="http://angularjs.org">
This `xmlns` declaration for the `ng` namespace must be specified in all angular applications in
order to make angular work with XHTML and IE versions older than 9 (regardless of whether you are
using XHTML or HTML).
* angular script tag
<script src="lib/angular/angular.js" ng:autobind>
This single line of code is all that is needed to bootstrap an angular application.
The code downloads the `angular.js` script and registers a callback that will be executed by the
browser when the containing HTML page is fully downloaded. When the callback is executed, angular
looks for the {@link angular.directive.ng:autobind ng:autobind} attribute. If angular finds
`ng:autobind`, it creates a root scope for the application and associates it with the `<html>`
element of the template:
<img src="img/tutorial/tutorial_00_final.png"/>
As you will see shortly, everything in angular is evaluated within a scope. We'll learn more
about this in the next steps.
## What are all these files in my working directory?
Most of the files in your working directory come from the {@link
https://github.com/angular/angular-seed angular-seed project} which is typically used to bootstrap
new angular projects. The seed project includes the latest angular libraries, test libraries,
scripts and a simple example app, all pre-configured for developing a typical web app.
For the purposes of this tutorial, we modified the angular-seed with the following changes:
* Removed the example app
* Added phone images to `app/img/phones`
* Added phone data files (JSON) to `app/phones`
# Summary
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>

View file

@ -1,11 +1,11 @@
@ngdoc overview
@ngdoc overview
@name Tutorial: Step 1
@description
<table id="tutorial_nav">
<tr>
<td id="previous_step">{@link tutorial.step_00 Previous}</td>
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-1/app Live
Demo}</td>
Demo}</td>
<td id="tut_home">{@link tutorial Tutorial Home}</td>
<td id="code_diff">
{@link https://github.com/angular/angular-phonecat/compare/step-0...step-1 Code Diff}</td>
@ -13,38 +13,53 @@
</tr>
</table>
In this step you will add some basic information about two cell phones to our app.
1. Do one of the following to reset your workspace to step 1; be aware that this will throw away
any changes you might have made to the tutorial files:
In order to illustrate how angular enhances standard HTML, you will create a purely *static* HTML
page and then examine how we can turn this HTML code into a template that angular will use to
dynamically display the same result with any set of data.
* Git users run:
git checkout --force step-1
In this step you will add some basic information about two cell phones to an HTML page.
1. Reset the workspace to step 1.
* Git users run:
git checkout -f step-1
* Snapshot users run:
* Snapshot users run:
./goto_step.sh 1
2. Refresh your browser or check the app out on {@link
http://angular.github.com/angular-phonecat/step-1/app anglar's server}. The page now contains a
list with information about two phones.
http://angular.github.com/angular-phonecat/step-1/app anglar's server}.
The page now contains a list with information about two phones.
The most important changes are listed below. You can see the full diff on {@link
https://github.com/angular/angular-phonecat/compare/step-0...step-1 GitHub}:
__`app/index.html`:__
<pre>
...
<ul>
<li>
<span>Nexus S<span>
<span>Nexus S</span>
<p>
Fast just got faster with Nexus S.
</p>
</li>
<li>
<span>Motorola XOOM™ with Wi-Fi<span>
<span>Motorola XOOM™ with Wi-Fi</span>
<p>
The Next, Next Generation tablet.
</p>
@ -53,26 +68,38 @@ __`app/index.html`:__
...
</pre>
# Experiments
* Try adding more static html to `index.html`. For example:
<p>Total number of phones: 3</p>
* Try adding more static HTML to `index.html`. For example:
<pre>
<p>Total number of phones: 2</p>
</pre>
# Summary
This addition to your app uses static HTML to display the list. Now, let's go to step 2 to learn
how to use angular to dynamically generate the same list.
<table id="tutorial_nav">
<tr>
<td id="previous_step">{@link tutorial.step_00 Previous}</td>
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-1/app Live
Demo}</td>
Demo}</td>
<td id="tut_home">{@link tutorial Tutorial Home}</td>
<td id="code_diff">
{@link https://github.com/angular/angular-phonecat/compare/step-0...step-1 Code Diff}</td>
<td id="next_step">{@link tutorial.step_02 Next}</td>
</tr>
</table>

View file

@ -1,11 +1,11 @@
@ngdoc overview
@ngdoc overview
@name Tutorial: Step 2
@description
<table id="tutorial_nav">
<tr>
<td id="previous_step">{@link tutorial.step_01 Previous}</td>
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-2/app Live
Demo}</td>
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>
@ -13,39 +13,54 @@ Diff}</td>
</tr>
</table>
Now it's time to make this web page dynamic with angular. We'll also add a test that verifies the
code for the controller we are going to add.
code for the controller we are going to add.
There are many ways to structure the code for an application. With angular, we encourage the use
of {@link http://en.wikipedia.org/wiki/ModelViewController the MVC design pattern} to decouple
the code and separate concerns. With that in mind, let's use a little angular and JavaScript to
add Model, View, and Controller components to our app.
1. Reset your workspace to step 2 using:
There are many ways to structure the code for an application. With angular, we encourage the use of
{@link http://en.wikipedia.org/wiki/ModelViewController the MVC design pattern} to decouple the
code and separate concerns. With that in mind, let's use a little angular and JavaScript to add
model, view, and controller components to our app.
1. Reset your workspace to step 2.
git checkout -f step-2
git checkout --force step-2
or
./goto_step.sh 2
2. Refresh your browser or check the app out on {@link
http://angular.github.com/angular-phonecat/step-2/app angular's server}. The app now contains a
list with 3 phones.
http://angular.github.com/angular-phonecat/step-2/app angular's server}.
The app now contains a list with 3 phones.
The most important changes are listed below. You can see the full diff on {@link
https://github.com/angular/angular-phonecat/compare/step-1...step-2 GitHub}:
## Template for the View
The __View__ component is constructed by angular from this template:
The __view__ component is constructed by angular from this template:
__`app/index.html`:__
<pre>
...
<body ng:controller="PhoneListCtrl">
<ul>
<li ng:repeat="phone in phones">
{{phone.name}}
@ -53,33 +68,44 @@ __`app/index.html`:__
</li>
</ul>
<script src="lib/angular/angular.js" ng:autobind></script>
<script src="js/controllers.js"></script>
</body>
</html>
</pre>
We replaced the hard-coded phone list with the {@link angular.widget.@ng:repeat ng:repeat widget}
and two {@link guide.expression angular expressions} enclosed in curly braces: `{{phone.name}}`
and `{{phone.snippet}}`:
and two {@link guide.expression angular expressions} enclosed in curly braces: `{{phone.name}}` and
`{{phone.snippet}}`:
* The `ng:repeat="phone in phones"` statement in the `<li>` tag is an angular repeater. It
tells angular to create a `<li>` element for each phone in the phones list, using the first
`<li>` tag as the template.
tells angular to create a `<li>` element for each phone in the phones list, using the first `<li>`
tag as the template.
<img src="img/tutorial/tutorial_02_final.png">
* The curly braces around `phone.name` and `phone.snippet` are an example of {@link
angular.markup angular markup}. The curly markup is shorthand for the angular directive {@link
angular.directive.ng:bind ng:bind}. `ng:bind` directives indicates to angular that these are
template binding points. Binding points are locations in the template where angular creates
data-binding between the View and the Model. In angular, the View is a projection of the Model
through the HTML template. This means that whenever the model changes, angular refreshes the
appropriate binding points, which updates the view.
angular.markup angular markup}. The curly markup is shorthand for the angular directive {@link
angular.directive.ng:bind ng:bind}. The `ng:bind` directives indicate to angular that these are
template binding points. Binding points are locations in the template where angular creates
data-binding between the view and the model. In angular, the view is a projection of the model
through the HTML template. This means that whenever the model changes, angular refreshes the
appropriate binding points, which updates the view.
## Model and Controller
The data __Model__ (a short list of phones in object literal notation) is instantiated within the
__Controller__ function (`PhoneListCtrl`):
The data __model__ (a simple array of phones in object literal notation) is instantiated within
the __controller__ function (`PhoneListCtrl`):
__`app/js/controllers.js`:__
<pre>
@ -93,36 +119,52 @@ function PhoneListCtrl() {
}
</pre>
Although the controller is not yet doing very much controlling, it is playing a crucial role. By
providing context for our data model, the controller allows us to establish data-binding between
the model and the view. Note in the following how we connected the dots between our presentation,
data, and logic components:
data, and logic components:
* The name of our controller function (in the JavaScript file `controllers.js`) matches the
{@link angular.directive.ng:controller ng:controller} directive in the `<body>` tag
(`PhoneListCtrl`).
{@link angular.directive.ng:controller ng:controller} directive in the `<body>` tag
(`PhoneListCtrl`).
* We instantiated our data within the scope of our controller function, and our template
binding points are located within the block bounded by the `<body
ng:controller="PhoneListCtrl">` tag.
binding points are located within the block bounded by the `<body ng:controller="PhoneListCtrl">`
tag.
Angular scopes are a crucial concept in angular; you can think of scopes as the glue that makes
the template, model and controller all work together. Angular uses scopes, along with the
information contained in the template, data model, and controller, to keep the model and view
separated but in sync. Any changes to the model are reflected in the view; any changes that occur
in the view are reflected in the model. To learn more about angular scopes, see the {@link
angular.scopes angular scope documentation}.
Angular uses scopes, along with the information contained in the template, data model, and
controller to keep the Model and View separated but in sync: any changes to the model are
reflected in the view; any changes that occur in the view are reflected in the model.
As for our data model, we created a simple array of phone records, specified in object literal
notation.
## Tests
The "Angular way" makes it easy for us to test as we develop; the unit test for your newly created
controller looks as follows:
__`test/unit/controllersSpec.js`:__
<pre>
describe('PhoneCat controllers', function() {
describe('PhoneListCtrl', function(){
it('should create "phones" model with 3 phones', function() {
var ctrl = new PhoneListCtrl();
expect(ctrl.phones.length).toBe(3);
@ -131,78 +173,117 @@ describe('PhoneCat controllers', function() {
});
</pre>
Ease of testing is another cornerstone of angular's design philosophy. All we are doing here is
showing how easy it is to create a unit test. The test verifies that we have 3 records in the
phones array.
Angular developers prefer the syntax of Jasmine's Behavior-driven Development (BDD) framework when
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}.
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}.
The 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`
2. Open a new browser tab or window and navigate to {@link http://localhost:9876}.
3. Choose "Capture this browser in strict mode".
At this point, you can leave this tab open and forget about it. JsTestDriver will use it to execute
the tests and report the results in the terminal.
4. 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!
Yay! The test passed! Or not...
Note: If you see errors after you run the test, close the browser tab and go back to the terminal
and kill the script, then repeat the procedure above.
# Experiments
* Add another binding to `index.html`. For example:
* Add another binding to `index.html`. For example:
<p>Total number of phones: {{phones.length}}</p>
* Create a new model property in the controller and bind to it from the template. For example:
this.hello = "Hello, World!"
this.hello = "Hello, World!"
Refresh your browser to make sure it says, "Hello, World!"
* Create a repeater that constructs a simple table:
<table>
<tr><th>row number</th></tr>
<tr ng:repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i}}</td></tr>
</table>
Now, make the list 1-based by incrementing `i` by one in the binding:
<table>
<tr><th>row number</th></tr>
<tr ng:repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i+1}}</td></tr>
</table>
* Make the unit test fail by changing the `toBe(3)` statement to `toBe(4)`, and rerun the
`./scripts/test.sh` script.
# Summary
You now have a dynamic app that features separate model, view, and controller components, and
you're testing as you go. 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>
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,11 +1,10 @@
@ngdoc overview
@ngdoc overview
@name Tutorial: Step 3
@description
<table id="tutorial_nav">
<tr>
<td id="previous_step">{@link tutorial.step_02 Previous}</td>
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-3/app Live
Demo}</td>
<td id="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>
@ -13,40 +12,57 @@ Diff}</td>
</tr>
</table>
We did a lot of work in laying a foundation for the app in the last step, so now we'll do
something simple, and add full text search (yes, it will be simple!). We will also write an
end-to-end test, because a good end-to-end test is a good friend. It stays with your app, keeps an
eye on it, and quickly detects regressions.
1. Reset your workspace to Step 3 using:
We did a lot of work in laying a foundation for the app in the last step, so now we'll do something
simple, and add full text search (yes, it will be simple!). We will also write an end-to-end test,
because a good end-to-end test is a good friend. It stays with your app, keeps an eye on it, and
quickly detects regressions.
1. Reset your workspace to step 3.
git checkout -f step-3
git checkout --force step-3
or
./goto_step.sh 3
2. Refresh your browser or check the app out on {@link
http://angular.github.com/angular-phonecat/step-3/app angular's server}. The app now has a search
box. The phone list on the page changes depending on what a user types into the search box.
http://angular.github.com/angular-phonecat/step-3/app angular's server}.
The app now has a search box. The phone list on the page changes depending on what a user types
into the search box.
The most important changes are listed below. You can see the full diff on {@link
https://github.com/angular/angular-phonecat/compare/step-2...step-3
GitHub}:
## Controller
We made no changes to the controller.
## Template
__`app/index.html`:__
<pre>
...
Fulltext Search: <input name="query"/>
<ul class="phones">
<li ng:repeat="phone in phones.$filter(query)">
{{phone.name}}
@ -56,53 +72,70 @@ __`app/index.html`:__
...
</pre>
We added a standard HTML `<input>` tag and use angular's {@link angular.Array.filter $filter}
function to process the input for the `ng:repeater`.
function to process the input for the `ng:repeater`.
This lets a user enter search criteria and immediately see the effects of their search on the
phone list. This new code demonstrates the following:
* Data-binding. This is one of the core features in angular. When the page loads, angular binds
the name of the input box to a variable of the same name in the data model and keeps the two in
sync.
This lets a user enter search criteria and immediately see the effects of their search on the phone
list. This new code demonstrates the following:
* Data-binding. This is one of the core features in angular. When the page loads, angular binds the
name of the input box to a variable of the same name in the data model and keeps the two in sync.
In this code, the data that a user types into the input box (named __`query`__) is immediately
available as a filter input in the list repeater (`phone in phones.$filter(`__`query`__`)`).
When changes to the data model cause the repeater's input to change, the repeater efficiently
updates the DOM to reflect the current state of the model.
available as a filter input in the list repeater (`phone in phones.$filter(`__`query`__`)`). When
changes to the data model cause the repeater's input to change, the repeater efficiently updates
the DOM to reflect the current state of the model.
<img src="img/tutorial/tutorial_03_final.png">
* Use of `$filter`. The {@link angular.Array.filter $filter} method, uses the `query` value, to
create a new array that contains only those records that match the `query`.
`ng:repeat` automatically updates the view in response to the changing number of phones returned
by the `$filter`. The process is completely transparent to the developer.
by the `$filter`. The process is completely transparent to the developer.
## Test
In step 2, we learned how to write and run unit tests. Unit tests are perfect for testing
controllers and other components of our application written in JavaScript, but they can't easily
test DOM manipulation or the wiring of our application. For these, an end-to-end test is a much
better choice.
The search feature was fully implemented via templates and data-binding, so we'll write our first
end-to-end test, to verify that the feature works.
__`test/e2e/scenarios.js`:__
<pre>
describe('PhoneCat App', function() {
describe('Phone list view', function() {
beforeEach(function() {
browser().navigateTo('../../app/index.html');
});
it('should filter the phone list as user types into the search box', function() {
expect(repeater('.phones li').count()).toBe(3);
input('query').enter('nexus');
expect(repeater('.phones li').count()).toBe(1);
input('query').enter('motorola');
expect(repeater('.phones li').count()).toBe(2);
});
@ -110,64 +143,109 @@ describe('PhoneCat App', function() {
});
</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: {@link http://localhost:8000/test/e2e/runner.html}
* users with other http servers:
`http://localhost:[*port-number*]/[*context-path*]/test/e2e/runner.html`
* casual reader: {@link 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.
# Experiments
* Display the current value of the `query` model by adding a `{{query}}` binding into the
`index.html` template, and see how it changes when you type in the input box.
* Change `index.html` to reflect the current search query in the html title by replacing the title
tag in the head section with:
<title>Google Phone Gallery: {{query}}</title>`
* Let's see how we can get the current value of the `query` model to appear in the HTML page title.
You might think you could just add the {{query}} to the title tag element as follows:
<title>Google Phone Gallery: {{query}}</title>
However, when you reload the page, you won't see the expected result. This is because the "query"
model lives in the scope defined by the body element:
If you reload the page, you won't see the expected result. This is because the "query" model
lives in the scope defined by:
<body ng:controller="PhoneListCtrl">
In order to be able to bind to the query mode from `<title>`, we need to move the
`ng:controller` declaration to an element that is a common parent for both the body and title
elements. In our case that's the html element:
If you want to bind to the query model from the `<title>` element, you must __move__ the
`ng:controller` declaration to the HTML element because it is the common parent of both the body
and title elements:
<html ng:controller="PhoneListCtrl">
* Make the end-to-end test fail by changing the first `toBe(3)` statement to `toBe(4)`, and
refresh the end-to-end test runner tab in the browser to rerun the test.
* Add a `wait();` statement into the end-to-end test and rerun it. You'll see the runner pausing,
Be sure to *remove* the `ng:controller` declaration from the body element.
* Add the following end-to-end test into the `describe` block within `test/e2e/scenarios.js`:
<pre>
it('should display the current filter value within an element with id "status"', function() {
expect(element('#status').text()).toMatch(/Current filter: \s*$/);
input('query').enter('nexus');
expect(element('#status').text()).toMatch(/Current filter: nexus\s*$/);
//alternative version of the last assertion that tests just the value of the binding
using('#status').expect(binding('query')).toBe('nexus');
});
</pre>
Refresh the browser tab with end-to-end test runner to see the test fail. Now add a `div` or `p`
element with `id` `"status"` and content with the `query` binding into the `index.html` template to
make the test pass.
* Add a `wait();` statement into an end-to-end test and rerun it. You'll see the runner pausing,
giving you the opportunity to explore the state of your application displayed in the browser. The
app is live! Change the search query to prove it. This is great for troubleshooting end-to-end
tests.
# Summary
With full text search under our belt and a test to verify it, let's go to step 4 to learn how to
add sorting capability to the phone app.
add sorting capability to the phone 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="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,4 +1,4 @@
@ngdoc overview
@ngdoc overview
@name Tutorial: Step 4
@description
<table id="tutorial_nav">
@ -12,35 +12,48 @@ Diff}</td>
</tr>
</table>
In this step, you will add a feature to let your users control the order of the items in the phone
list. The dynamic ordering is implemented by creating a new model property, wiring it together
with the repeater, and letting the data binding magic do the rest of the work.
list. The dynamic ordering is implemented by creating a new model property, wiring it together with
the repeater, and letting the data binding magic do the rest of the work.
1. Reset your workspace to Step 4 using:
git checkout --force step-4
git checkout -f step-4
or
./goto_step.sh 4
2. Refresh your browser or check the app out on {@link
http://angular.github.com/angular-phonecat/step-4/app angular's server}. You should see that in
http://angular.github.com/angular-phonecat/step-4/app angular's server}.
You should see that in
addition to the search box, the app displays a drop down menu that allows users to control the
order in which the phones are listed.
The most important changes are listed below. You can see the full diff on {@link
https://github.com/angular/angular-phonecat/compare/step-3...step-4
GitHub}:
## Template
__`app/index.html`:__
<pre>
...
<ul class="predicates">
<ul class="controls">
<li>
Search: <input type="text" name="query"/>
</li>
@ -53,6 +66,7 @@ __`app/index.html`:__
</li>
</ul>
<ul class="phones">
<li ng:repeat="phone in phones.$filter(query).$orderBy(orderProp)">
{{phone.name}}
@ -62,31 +76,44 @@ __`app/index.html`:__
...
</pre>
In the `index.html` template we made the following changes:
* First, we added a `<select>` html element named `orderProp`, so that our users can pick from the
two provided sorting options.
<img src="img/tutorial/tutorial_04-06_final.png">
* We then chained the `$filter` method with {@link angular.Array.orderBy `$orderBy`} method to
further process the input into the repeater. `$orderBy` is a utility method similar to {@link
angular.Array.filter `$filter`}, but instead of filtering an array, it reorders it.
Angular creates a two way data-binding between the select element and the `orderProp` model.
`orderProp` is then used as the input for the `$orderBy` method.
As we discussed in the section about data-binding and the repeater in step 3, whenever the model
changes (for example because a user changes the order with the select drop down menu), angular's
data-binding will cause the view to automatically update. No bloated DOM manipulation code is
necessary!
necessary!
## Controller
__`app/js/controller.js`:__
<pre>
/* App Controllers */
function PhoneListCtrl() {
this.phones = [{"name": "Nexus S",
"snippet": "Fast just got faster with Nexus S.",
@ -98,48 +125,63 @@ function PhoneListCtrl() {
"snippet": "The Next, Next Generation tablet.",
"age": 2}];
this.orderProp = 'age';
}
</pre>
* We modified the `phones` model - the array of phones - and added an `age` property to each phone
record. This property is used to order phones by age.
* We added a line to the controller that sets the default value of `orderProp` to `age`. If we had
not set the default value here, angular would have used the value of the first `<option>` element
(`'name'`) when it initialized the data model.
This is a good time to talk about two-way data-binding. Notice that when the app is loaded in
the browser, "Newest" is selected in the drop down menu. This is because we set `orderProp` to
`'age'` in the controller. So the binding works in the direction from our model to the UI. Now
if you select "Alphabetically" in the drop down menu, the model will be updated as well and the
phones will be reordered. That is the data-binding doing its job in the opposite direction —
from the UI to the model.
This is a good time to talk about two-way data-binding. Notice that when the app is loaded in the
browser, "Newest" is selected in the drop down menu. This is because we set `orderProp` to `'age'`
in the controller. So the binding works in the direction from our model to the UI. Now if you
select "Alphabetically" in the drop down menu, the model will be updated as well and the phones
will be reordered. That is the data-binding doing its job in the opposite direction — from the UI
to the model.
## Test
The changes we made should be verified with both a unit test and an end-to-end test. Let's look at
the unit test first.
__`test/unit/controllerSpec.js`:__
<pre>
describe('PhoneCat controllers', function() {
describe('PhoneListCtrl', function(){
var scope, $browser, ctrl;
beforeEach(function() {
ctrl = new PhoneListCtrl();
});
it('should create "phones" model with 3 phones', function() {
expect(ctrl.phones.length).toBe(3);
});
it('should set the default value of orderProp model', function() {
expect(ctrl.orderProp).toBe('age');
});
@ -148,35 +190,48 @@ describe('PhoneCat controllers', function() {
</pre>
The unit test now verifies that the default ordering property is set.
We used Jasmine's API to extract the controller construction into a `beforeEach` block, which is
shared by all tests in the nearest `describe` block.
To run the unit tests, once again execute the `./scripts/test.sh` script and you should see the
following output.
Chrome: Runner reset.
..
Total 2 tests (Passed: 2; Fails: 0; Errors: 0) (3.00 ms)
Chrome 11.0.696.57 Mac OS: Run 2 tests (Passed: 2; Fails: 0; Errors 0) (3.00 ms)
Let's turn our attention to the end-to-end test.
__`test/e2e/scenarios.js`:__
<pre>
...
it('should be possible to control phone order via the drop down select box', function() {
input('query').enter('tablet'); //let's narrow the dataset to make the test assertions
shorter
//let's narrow the dataset to make the test assertions shorter
input('query').enter('tablet');
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"]);
@ -184,28 +239,37 @@ __`test/e2e/scenarios.js`:__
...
</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}.
# Experiments
* In the `PhoneListCtrl` controller, remove the statement that sets the `orderProp` value and
you'll see that the ordering as well as the current selection in the dropdown menu will default to
"Alphabetical".
* Add an `{{orderProp}}` binding into the `index.html` template to display its current value as
text.
# Summary
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>
@ -216,3 +280,6 @@ Diff}</td>
<td id="next_step">{@link tutorial.step_05 Next}</td>
</tr>
</table>

View file

@ -1,4 +1,4 @@
@ngdoc overview
@ngdoc overview
@name Tutorial: Step 5
@description
<table id="tutorial_nav">
@ -13,33 +13,46 @@ Diff}</td>
</tr>
</table>
Enough of building an app with three phones in a hard-coded dataset! Let's fetch a larger dataset
from our server using one of angular's built-in {@link angular.service services} called {@link
angular.service.$xhr $xhr}. We will use angular's dependency injection to provide the service to
the `PhoneListCtrl` controller.
angular.service.$xhr $xhr}. We will use angular's {@link guide.di dependency injection (DI)} to
provide the service to the `PhoneListCtrl` controller.
1. Reset your workspace to Step 5 using:
git checkout --force step-5
1. Reset your workspace to step 5.
git checkout -f step-5
or
./goto_step.sh 5
2. Refresh your browser or check the app out on {@link
http://angular.github.com/angular-phonecat/step-5/app angular's server}. You should now see a list
of 20 phones.
http://angular.github.com/angular-phonecat/step-5/app angular's server}.
You should now see a list of 20 phones.
The most important changes are listed below. You can see the full diff on {@link
https://github.com/angular/angular-phonecat/compare/step-4...step-5
GitHub}:
## Data
The `app/phones/phone.json` file in your project is a dataset that contains a larger list of
phones stored in the JSON format.
The `app/phones/phone.json` file in your project is a dataset that contains a larger list of phones
stored in the JSON format.
Following is a sample of the file:
<pre>
@ -56,101 +69,186 @@ Following is a sample of the file:
</pre>
## Controller
In this step, the view template will remain the same but the model and controller will change.
We'll use angular's {@link angular.service.$xhr} service to make an HTTP request to your web
server to fetch the data in the `phones.json` file.
We'll use angular's {@link angular.service.$xhr $xhr} service in our controller to make an HTTP
request to your web server to fetch the data in the `app/phones/phones.json` file. `$xhr` is just
one of several built-in {@link angular.service angular services} that handle common operations in
web apps. Angular injects these services for you where you need them.
Services are managed by angular's {@link guide.di DI subsystem}. Dependency injection helps to make
your web apps both well-structured (e.g., separate components for presentation, data, and control)
and loosely coupled (dependencies between components are not resolved by the components themselves,
but by the DI subsystem).
__`app/js/controllers.js:`__
<pre>
function PhoneListCtrl($xhr) {
var self = this;
$xhr('GET', 'phones/phones.json', function(code, response) {
self.phones = response;
});
self.orderProp = 'age';
}
//PhoneListCtrl.$inject = ['$xhr'];
</pre>
We removed the hard-coded dataset from the controller and instead are using the `$xhr` service to
access the data stored in `app/phones/phones.json`. The `$xhr` service makes a HTTP GET request to
our web server, asking for `phone/phones.json` (the url is relative to our `index.html` file). The
server responds by providing the data in the json file.
Keep in mind that the response might just as well have been dynamically generated by a backend
server. To the browser and our app they both look the same. For the sake of simplicity we used a
json file in this tutorial.
`$xhr` makes an HTTP GET request to our web server, asking for `phone/phones.json` (the url is
relative to our `index.html` file). The server responds by providing the data in the json file.
(The response might just as well have been dynamically generated by a backend server. To the
browser and our app they both look the same. For the sake of simplicity we used a json file in this
tutorial.)
Notice that the `$xhr` service takes a callback as the last parameter. This callback is used to
process the response. In our case, we just assign the response to the current scope controlled by
the controller, as a model called `phones`. Have you realized that we didn't even have to parse
the response? Angular took care of that for us.
We already mentioned that the `$xhr` function we just used is an angular service. {@link
angular.service Angular services} are substitutable objects managed by angular's {@link guide.di
DI subsystem}.
The `$xhr` service takes a callback as the last argument. This callback is used to process the
response. We assign the response to the scope controlled by the controller, as a model called
`phones`. Notice that angular detected the json response and parsed it for us!
Dependency injection helps to make your web apps well structured, loosely coupled, and much easier
to test. What's important to understand is how the controllers get access to these services
through dependency injection.
The dependency injection pattern is based on declaring the dependencies we require and letting the
system provide them to us. To do this in angular, you simply provide the names of the services you
need as arguments to the controller's constructor function, as follows:
To use a service in angular, you simply declare the names of the services you need as arguments to
the controller's constructor function, as follows:
function PhoneListCtrl($xhr) {
The name of the argument is significant, because angular recognizes the identity of a service by
the argument name. Once angular knows what services are being requested, it provides them to the
controller when the controller is being constructed. The dependency injector also takes care of
creating any transitive dependencies the service may have (services often depend upon other
services).
As we mentioned earlier, angular infers the controller's dependencies from the names of arguments
of the controller's constructor function. If you were to minify the JavaScript code for this
controller, all of these function arguments would be minified as well, and the dependency injector
would not being able to identify services correctly.
Angular's dependency injector provides services to your controller when the controller is being
constructed. The dependency injector also takes care of creating any transitive dependencies the
service may have (services often depend upon other services).
<img src="img/tutorial/xhr_service_final.png">
### '$' Prefix Naming Convention
You can create your own services, and in fact we will do exactly that in step 11. As a naming
convention, angular's built-in services, Scope methods and a few other angular APIs have a '$'
prefix in front of the name. Don't use a '$' prefix when naming your services and models, in order
to avoid any possible naming collisions.
### A Note on Minification
Since angular infers the controller's dependencies from the names of arguments to the controller's
constructor function, if you were to {@link http://en.wikipedia.org/wiki/Minification_(programming)
minify} the JavaScript code for `PhoneListCtrl` controller, all of its function arguments would be
minified as well, and the dependency injector would not being able to identify services correctly.
To overcome issues caused by minification, just assign an array with service identifier strings
into the `$inject` property of the controller function, just like the last line in the snippet
(commented out) suggests:
PhoneListCtrl.$inject = ['$xhr'];
## Test
__`test/unit/controllersSpec.js`:__
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, as follows:
<pre>
describe('PhoneCat controllers', function() {
describe('PhoneListCtrl', function(){
var scope, $browser, ctrl;
beforeEach(function() {
scope = angular.scope();
$browser = scope.$service('$browser');
$browser.xhr.expectGET('phones/phones.json').respond([{name: 'Nexus S'},
{name: 'Motorola DROID'}]);
ctrl = scope.$new(PhoneListCtrl);
});
});
</pre>
We created the controller in the test environment, as follows:
* We created a root scope object by calling `angular.scope()`
* We called `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.
* 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.
Now, we will make assertions to verify that the `phones` model doesn't exist on the scope, before
the response is received:
<pre>
it('should create "phones" model with 2 phones fetched from xhr', function() {
expect(ctrl.phones).toBeUndefined();
$browser.xhr.flush();
expect(ctrl.phones).toEqual([{name: 'Nexus S'},
{name: 'Motorola DROID'}]);
});
</pre>
* 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.
* We make the assertions, verifying that the phone model now exists on the scope.
Finally, we verify that the default value of `orderProp` is set correctly:
<pre>
it('should set the default value of orderProp model', function() {
expect(ctrl.orderProp).toBe('age');
});
@ -159,74 +257,50 @@ describe('PhoneCat controllers', function() {
</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)
# Experiments
* At the bottom of `index.html`, add a `{{phones}}` binding to see the list of phones displayed in
json format.
* In the `PhoneListCtrl` controller, pre-process the xhr response by limiting the number of phones
to the first 5 in the list. Use the following code in the xhr callback:
to the first 5 in the list. Use the following code in the xhr callback:
self.phones = response.splice(0, 5);
# Summary
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="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,4 +1,4 @@
@ngdoc overview
@ngdoc overview
@name Tutorial: Step 6
@description
<table id="tutorial_nav">
@ -13,49 +13,64 @@ Diff}</td>
</tr>
</table>
In this step, you will add thumbnail images for the phones in the phone list, and links that, for
now, will go nowhere. In subsequent steps you will use the links to display additional information
about the phones in the catalog.
about the phones in the catalog.
1. Reset your workspace to Step 6 using:
git checkout --force step-6
1. Reset your workspace to step 6.
git checkout -f step-6
or
./goto_step.sh 6
2. Refresh your browser or check the app out on {@link
http://angular.github.com/angular-phonecat/step-6/app angular's server}. You should now see links
and images of the phones in the list.
http://angular.github.com/angular-phonecat/step-6/app angular's server}.
You should now see links and images of the phones in the list.
The most important changes are listed below. You can see the full diff on {@link
https://github.com/angular/angular-phonecat/compare/step-5...step-6
GitHub}:
## Data
Note that the `phones.json` file contains unique ids and image urls for each of the phones. The
urls point to the `app/img/phones/` directory.
__`app/phones/phones.json`__ (sample snippet):
<pre>
[
{
...
"id": "motorola-defy-with-motoblur",
"imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg",
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
"id": "motorola-defy-with-motoblur",
"imageUrl": "img/phones/motorola-defy-with-motoblur.0.jpg",
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
...
},
},
...
]
</pre>
## Template
__`app/index.html`:__
<pre>
...
@ -69,11 +84,13 @@ __`app/index.html`:__
...
</pre>
To dynamically generate links that will in the future lead to phone detail pages, we used the
now-familiar {@link angular.markup double-curly brace markup} in the `href` attribute values. In
step 2, we added the `{{phone.name}}` binding as the element content. In this step the
'{{phone.id}}' binding is used in the element attribute.
We also added phone images next to each record using an image tag with the {@link
angular.directive.ng:src ng:src} directive. That directive prevents the browser from treating the
angular `{{ exppression }}` markup literally, which it would have done if we had only specified an
@ -81,8 +98,11 @@ attribute binding in a regular `src` attribute (`<img src="{{phone.imageUrl}}">`
prevents the browser from making an http request to an invalid location.
## Test
__`test/e2e/scenarios.js`__:
<pre>
...
@ -94,28 +114,37 @@ __`test/e2e/scenarios.js`__:
...
</pre>
We added a new end-to-end test to verify that the app is generating correct links to the phone
views that we will implement in the upcoming steps.
You can now refresh the browser tab with the end-to-end test runner to see the tests run, or you
can see them running on {@link
http://angular.github.com/angular-phonecat/step-6/test/e2e/runner.html
angular's server}.
# Experiments
* Replace the `ng:src` directive with a plain old `<src>` attribute, and using tools such as
Firebug, or Chrome's Web Inspector, or by inspecting the webserver access logs, confirm that the
app is indeed making an extraneous request to `/app/%7B%7Bphone.imageUrl%7D%7D` (or
* Replace the `ng:src` directive with a plain old `<src>` attribute. Using tools such as Firebug,
or Chrome's Web Inspector, or inspecting the webserver access logs, confirm that the app is indeed
making an extraneous request to `/app/%7B%7Bphone.imageUrl%7D%7D` (or
`/app/index.html/{{phone.imageUrl}}`).
# Summary
Now that you have added phone images and links, go to step 7 to learn about angular layout
templates and how angular makes it easy to create applications that have multiple views.
<table id="tutorial_nav">
<tr>
<td id="previous_step">{@link tutorial.step_05 Previous}</td>
@ -127,3 +156,4 @@ Diff}</td>
<td id="next_step">{@link tutorial.step_07 Next}</td>
</tr>
</table>

View file

@ -1,4 +1,4 @@
@ngdoc overview
@ngdoc overview
@name Tutorial: Step 7
@description
<table id="tutorial_nav">
@ -13,39 +13,55 @@ Diff}</td>
</tr>
</table>
In this step, you will learn how to create a layout template and how to build an app that has
multiple views by adding routing.
multiple views by adding routing.
1. Reset your workspace to Step 7 using:
git checkout --force step-7
1. Reset your workspace to step 7.
git checkout -f step-7
or
./goto_step.sh 7
2. Refresh your browser, but be sure that there is nothing in the url after `app/index.html`, or
check the app out on {@link http://angular.github.com/angular-phonecat/step-7/app angular's
server}. Note that you are redirected to `app/index.html#/phones` and the same phone list appears
server}.
Note that you are redirected to `app/index.html#/phones` and the same phone list appears
in the browser. When you click on a phone link the stub of a phone detail page is displayed.
The most important changes are listed below. You can see the full diff on {@link
https://github.com/angular/angular-phonecat/compare/step-6...step-7
GitHub}:
## What's going on here?
Our app is slowly growing and becoming more complex. Before step 7, the app provided our users
with a single view (the list of all phones), and all of the template code was located in the
`index.html` file. The next step in building the app is the addition of a view that will show
detailed information about each of the devices in our list.
## Multiple Views, Routing and Layout Template
Our app is slowly growing and becoming more complex. Before step 7, the app provided our users with
a single view (the list of all phones), and all of the template code was located in the
`index.html` file. The next step in building the app is to add a view that will show detailed
information about each of the devices in our list.
To add the detailed view, we could expand the `index.html` file to contain template code for both
views, but that would get messy very quickly. Instead, we are going to turn the `index.html`
template into what we call a "layout template". This is a template that is common for all views in
our application. Other "partial templates" are then included into this layout template depending
on the current "route" — the view that is currently displayed to the user.
our application. Other "partial templates" are then included into this layout template depending on
the current "route" — the view that is currently displayed to the user.
Application routes in angular are declared via the {@link angular.service.$route $route} service.
This service makes it easy to wire together controllers, view templates, and the current URL
@ -54,87 +70,106 @@ http://en.wikipedia.org/wiki/Deep_linking deep linking}, which lets us utilize t
history (back and forward navigation) and bookmarks.
## Controllers
__`app/js/controller.js`:__
<pre>
function PhoneCatCtrl($route) {
var self = this;
$route.when('/phones',
{template: 'partials/phone-list.html', controller: PhoneListCtrl});
$route.when('/phones/:phoneId',
{template: 'partials/phone-detail.html', controller: PhoneDetailCtrl});
$route.otherwise({redirectTo: '/phones'});
$route.onChange(function(){
self.params = $route.current.params;
});
$route.parent(this);
}
//PhoneCatCtrl.$inject = ['$route'];
...
</pre>
We created a new controller called `PhoneCatCtrl`. We declared its dependency on the `$route`
service and used this service to declare that our application consists of two different views:
service and used this service to declare that our application consists of two different views:
* The phone list view will be shown when the URL hash fragment is `/phone`. To construct this
view, angular will use the `phone-list.html` template and the `PhoneListCtrl` controller.
* The phone details view will be shown when the URL hash fragment matches '/phone/[phoneId]'. To
construct this view, angular will use the `phone-detail.html` template and the `PhoneDetailCtrl`
controller.
* The phone list view will be shown when the URL hash fragment is `/phone`. To construct this view,
angular will use the `phone-list.html` template and the `PhoneListCtrl` controller.
* The phone details view will be shown when the URL hash fragment matches '/phone/:phoneId', where
`:phoneId` is a variable part of the URL. To construct the phone details view, angular will use the
`phone-detail.html` template and the `PhoneDetailCtrl` controller.
We reused the `PhoneListCtrl` controller that we constructed in previous steps and we added a new,
empty `PhoneDetailCtrl` controller to the `app/js/controllers.js` file for the phone details view.
We reused the `PhoneListCtrl` controller for the first view and we added an empty
`PhoneDetailCtrl` controller to the `app/js/controllers.js` file for the second one.
The statement `$route.otherwise({redirectTo: '/phones'})` triggers a redirection to `/phones` when
the browser address doesn't match either of our routes.
Thanks to the `$route.parent(this);` statement and `ng:controller="PhoneCatCtrl"` declaration in
the `index.html` template, the `PhoneCatCtrl` controller has a special role in our app. It is the
"root" controller or the parent controller for the other two sub-controllers (`PhoneListCtrl` and
"root" controller and the parent controller for the other two sub-controllers (`PhoneListCtrl` and
`PhoneDetailCtrl`). The sub-controllers inherit the model properties and behavior from the root
controller.
Note the use of the `:phoneId` parameter in the second route declaration (`'/phones/:phoneId'`).
When the current URL matches this route, the `$route` service extracts the `phoneId` string from
the current URL and provides it to our controller via the `$route.current.params` map. We will use
the `phoneId` parameter in the `phone-details.html` template thanks to the alias created in the
{@link angular.service.$route `$route.onChange`} callback.
In this `onChange` callback, we aliased url parameters extracted from the current route to the
`params` property in the root scope. This model property is inherited by child scopes created for
our routes and accessible by their controllers and templates, just like the `phone-list.html`
template demonstrates.
Note the use of the `:phoneId` parameter in the second route declaration. The `$route` service uses
the route declaration — `'/phones/:phoneId'` — as a template that is matched against the current
URL. All variables defined with the `:` notation are extracted into the `$route.current.params` map.
The `params` alias created in the {@link angular.service.$route `$route.onChange`} callback allows
us to use the `phoneId` property of this map in the `phone-details.html` template.
## Template
The `$route` service is usually used in conjunction with the {@link angular.widget.ng:view
ng:view} widget. The role of the `ng:view` widget is to include the view template for the current
route into the layout template, which makes it a perfect fit for our `index.html` template.
The `$route` service is usually used in conjunction with the {@link angular.widget.ng:view ng:view}
widget. The role of the `ng:view` widget is to include the view template for the current route into
the layout template, which makes it a perfect fit for our `index.html` template.
__`app/index.html`:__
<pre>
...
<body ng:controller="PhoneCatCtrl">
<ng:view></ng:view>
<script src="lib/angular/angular.js" ng:autobind></script>
<script src="js/controllers.js"></script>
</body>
</html>
</pre>
Note that we removed most of the code in the `index.html` template and replaced it with a single
line containing the `ng:view` tag. The code that we removed was placed into the `phone-list.html`
template:
__`app/partials/phone-list.html`:__
<pre>
<ul class="predicates">
@ -150,6 +185,7 @@ __`app/partials/phone-list.html`:__
</li>
</ul>
<ul class="phones">
<li ng:repeat="phone in phones.$filter(query).$orderBy(orderProp)">
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
@ -159,21 +195,31 @@ __`app/partials/phone-list.html`:__
</ul>
</pre>
<img src="img/tutorial/tutorial_07_final.png">
We also added a placeholder template for the phone details view:
__`app/partials/phone-list.html`:__
<pre>
TBD: detail view for {{params.phoneId}}
</pre>
Note how we are using `params` model defined in the `PhoneCanCtrl` controller.
## Test
To automatically verify that everything is wired properly, we wrote end-to-end tests that navigate
to various URLs and verify that the correct view was rendered.
<pre>
...
it('should redirect index.html to index.html#/phones', function() {
@ -182,13 +228,17 @@ to various URLs and verify that the correct view was rendered.
});
...
describe('Phone detail view', function() {
beforeEach(function() {
browser().navigateTo('../../app/index.html#/phones/nexus-s');
});
it('should display placeholder page with phoneId', function() {
expect(binding('params.phoneId')).toBe('nexus-s');
});
@ -196,30 +246,39 @@ to various URLs and verify that the correct view was rendered.
</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}.
# Experiments
* Try to add an `{{orderProp}}` binding to `index.html`, and you'll see that nothing happens even
when you are in the phone list view. This is because the `orderProp` model is visible only in the
scope managed by `PhoneListCtrl`, which is associated with the `<ng:view>` element. If you add the
same binding into the `phone-list.html` template, the binding will work as expected.
* In `PhoneCatCtrl`, create a new model called "`firstName`" with `this.hero = 'Zoro'`. In
* In `PhoneCatCtrl`, create a new model called "`hero`" with `this.hero = 'Zoro'`. In
`PhoneListCtrl` let's shadow it with `this.hero = 'Batman'`, and in `PhoneDetailCtrl` we'll use
`this.hero = "Captain Proton"`. Then add the `<p>hero = {{hero}}</p>` to all three of our
templates (`index.html`, `phone-list.html`, and `phone-detail.html`). Open the app and you'll see
scope inheritance and model property shadowing do some wonders.
`this.hero = "Captain Proton"`. Then add the `<p>hero = {{hero}}</p>` to all three of our templates
(`index.html`, `phone-list.html`, and `phone-detail.html`). Open the app and you'll see scope
inheritance and model property shadowing do some wonders.
# Summary
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>
@ -231,3 +290,6 @@ Diff}</td>
<td id="next_step">{@link tutorial.step_08 Next}</td>
</tr>
</table>

View file

@ -1,4 +1,4 @@
@ngdoc overview
@ngdoc overview
@name Tutorial: Step 8
@description
<table id="tutorial_nav">
@ -13,104 +13,135 @@ Diff}</td>
</tr>
</table>
In this step, you will implement the phone details view, which is displayed when a user clicks on
a phone in the phone list.
In this step, you will implement the phone details view, which is displayed when a user clicks on a
phone in the phone list.
1. Reset your workspace to Step 8 using:
git checkout --force step-8
git checkout -f step-8
or
./goto_step.sh 8
2. Refresh your browser or check the app out on {@link
http://angular.github.com/angular-phonecat/step-8/app angular's server}. Now when you click on a
http://angular.github.com/angular-phonecat/step-8/app angular's server}.
Now when you click on a
phone on the list, the phone details page with phone-specific information is displayed.
To implement the phone details view we will use {@link angular.services.$xhr $xhr} to fetch our
data, and we'll flesh out the `phone-details.html` view template.
The most important changes are listed below. You can see the full diff on {@link
https://github.com/angular/angular-phonecat/compare/step-7...step-8
GitHub}:
## Data
In addition to `phones.json`, the `app/phones/` directory also contains one json file for each
phone:
__`app/phones/nexus-s.json`:__ (sample snippet)
<pre>
{
"additionalFeatures": "Contour Display, Near Field Communications (NFC), Three-axis gyroscope,
Anti-fingerprint display coating, Internet Calling support (VoIP/SIP)",
Anti-fingerprint display coating, Internet Calling support (VoIP/SIP)",
"android": {
"os": "Android 2.3",
"os": "Android 2.3",
"ui": "Android"
},
},
...
"images": [
"img/phones/nexus-s.0.jpg",
"img/phones/nexus-s.1.jpg",
"img/phones/nexus-s.2.jpg",
"img/phones/nexus-s.0.jpg",
"img/phones/nexus-s.1.jpg",
"img/phones/nexus-s.2.jpg",
"img/phones/nexus-s.3.jpg"
],
],
"storage": {
"flash": "16384MB",
"flash": "16384MB",
"ram": "512MB"
}
}
</pre>
Each of these files describes various properties of the phone using the same data structure. We'll
show this data in the phone detail view.
## Controller
We'll expand the `PhoneDetailCtrl` by using the `$xhr` service to fetch the json files. This works
the same way as the phone list controller.
__`app/js/controller.js`:__
<pre>
function PhoneDetailCtrl($xhr) {
var self = this;
$xhr('GET', 'phones/' + self.params.phoneId + '.json', function(code, response) {
self.phone = response;
});
}
//PhoneDetailCtrl.$inject = ['$xhr'];
</pre>
To construct the URL for the HTTP request, we use `params.phoneId` extracted from the current
route in the `PhoneCatCtrl` controller.
To construct the URL for the HTTP request, we use `params.phoneId` extracted from the current route
in the `PhoneCatCtrl` controller.
## Template
The TBD placeholder line has been replaced with lists and bindings that comprise the phone
details. Note where we use the angular `{{expression}}` markup and `ng:repeater`s to project phone
data from our model into the view.
The TBD placeholder line has been replaced with lists and bindings that comprise the phone details.
Note where we use the angular `{{expression}}` markup and `ng:repeater`s to project phone data from
our model into the view.
__`app/partials/phone-details.html`:__
<pre>
<img ng:src="{{phone.images[0]}}" class="phone"/>
<img ng:src="{{phone.images}}" class="phone"/>
<h1>{{phone.name}}</h1>
<p>{{phone.description}}</p>
<ul class="phone-thumbs">
<li ng:repeat="img in phone.images">
<img ng:src="{{img}}"/>
</li>
</ul>
<ul class="specs">
<li>
<span>Availability and Networks</span>
@ -128,11 +159,16 @@ __`app/partials/phone-details.html`:__
</pre>
<img src="img/tutorial/tutorial_08-09_final.png">
## Test
We wrote a new unit test that is similar to the one we wrote for the `PhoneListCtrl` controller in
step 5.
__`test/unit/controllerSpec.js`:__
<pre>
...
@ -141,36 +177,46 @@ __`test/unit/controllerSpec.js`:__
$browser.xhr.expectGET('phones/xyz.json').respond({name:'phone xyz'});
ctrl = scope.$new(PhoneDetailCtrl);
expect(ctrl.phone).toBeUndefined();
$browser.xhr.flush();
expect(ctrl.phone).toEqual({name:'phone xyz'});
});
...
</pre>
To run the unit tests, execute the `./scripts/test.sh` script and you should see the following
output.
Chrome: Runner reset.
...
Total 3 tests (Passed: 3; Fails: 0; Errors: 0) (5.00 ms)
Chrome 11.0.696.57 Mac OS: Run 3 tests (Passed: 3; Fails: 0; Errors 0) (5.00 ms)
We also added a new end-to-end test that navigates to the Nexus S detail page and verifies that
the heading on the page is "Nexus S".
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');
});
@ -179,25 +225,32 @@ __`test/e2e/scenarios.js`:__
</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}.
# Experiments
* Stretching:
* Alternate chin-to-chest with look-at-ceiling. Repeat eight times.
* Now do ear-to-shoulder (left ear to left shoulder, right to right. Caution: do not try left
ear to right shoulder!). Repeat eight times.
* Finally, do chin-to-shoulder, left right left right. Repeat eight times.
* Using the {@link
https://docs.google.com/document/d/11L8htLKrh6c92foV71ytYpiKkeKpM4_a5-9c3HywfIc/edit?hl=en&pli=1#
end-to-end test runner API}, write a test that verifies that we display 4 thumbnail images on the
Nexus S details page.
# Summary
Now that 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>
@ -209,3 +262,6 @@ Diff}</td>
<td id="next_step">{@link tutorial.step_09 Next}</td>
</tr>
</table>

View file

@ -1,4 +1,4 @@
@ngdoc overview
@ngdoc overview
@name Tutorial: Step 9
@description
<table id="tutorial_nav">
@ -13,34 +13,47 @@ Diff}</td>
</tr>
</table>
In this step you will learn how to create your own custom display filter.
1. Reset your workspace to Step 9 using:
git checkout --force step-9
git checkout -f step-9
or
./goto_step.sh 9
2. Refresh your browser or check the app out on {@link
http://angular.github.com/angular-phonecat/step-9/app angular's server}. Navigate to one of the
detail pages.
http://angular.github.com/angular-phonecat/step-9/app angular's server}.
Navigate to one of the detail pages.
In the previous step, the details page displayed either "true" or "false" to indicate whether
certain phone features were present or not. We have used a custom filter to convert those text
strings into glyphs: ✓ for "true", and ✘ for "false". Let's see, what the filter code looks like.
The most important changes are listed below. You can see the full diff on {@link
https://github.com/angular/angular-phonecat/compare/step-8...step-9
GitHub}:
## Custom Filter
In order to create a new filter, simply register your custom filter function with the {@link
angular.filter `angular.filter`} API.
__`app/js/filters.js`:__
<pre>
angular.filter('checkmark', function(input) {
@ -48,17 +61,22 @@ angular.filter('checkmark', function(input) {
});
</pre>
The name of our filter is "checkmark". The `input` evaluates to either `true` or `false`, and we
return one of two unicode characters we have chosen to represent true or false (`\u2713` and
`\u2718`).
`\u2718`).
## Template
Since the filter code lives in the `app/js/filters.js` file, we need to include this file in our
layout template.
__`app/index.html`:__
__`app/index.html`:__
<pre>
...
<script src="js/controllers.js"></script>
@ -66,15 +84,20 @@ __`app/index.html`:__
...
</pre>
The syntax for using filters in angular templates is as follows:
{{ expression | filter }}
Let's employ the filter in the phone details template:
__`app/partials/phone-detail.html`:__
__`app/partials/phone-detail.html`:__
<pre>
...
<dl>
@ -87,14 +110,19 @@ __`app/partials/phone-detail.html`:__
</pre>
## Test
Filters, like any other component, should be tested and these tests are very easy to write.
__`test/unit/filtersSpec.js`:__
<pre>
describe('checkmark filter', function() {
it('should convert boolean values to unicode checkmark or cross', function() {
expect(angular.filter.checkmark(true)).toBe('\u2713');
expect(angular.filter.checkmark(false)).toBe('\u2718');
@ -102,17 +130,22 @@ describe('checkmark filter', function() {
})
</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)
# Experiments
* Let's experiment with some of the {@link angular.filter built-in angular filters} and add the
following bindings to `index.html`:
* `{{ "lower cap string" | uppercase }}`
@ -120,17 +153,23 @@ following bindings to `index.html`:
* `{{ 1304375948024 | date }}`
* `{{ 1304375948024 | date:"'MM/dd/yyyy @ h:mma" }}`
* We can also create a model with an input element, and combine it with a filtered binding. Add
the following to index.html:
<input name="userInput"> Uppercased: {{ userInput | uppercase }}
# Summary
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>

View file

@ -7,69 +7,90 @@
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-10/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-9...step-10
Code Diff}</td>
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-9...step-10 Code
Diff}</td>
<td id="next_step">{@link tutorial.step_11 Next}</td>
</tr>
</table>
In this step, you will add a clickable phone image swapper to the phone details page.
1. Reset your workspace to Step 10 using:
git checkout --force step-10
git checkout -f 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 angular's server}.
The phone details view displays one large image of the current phone and several smaller thumbnail
images. It would be great if we could replace the large image with any of the thumbnails just by
clicking on the desired thumbnail image. Let's have a look at how we can do this with angular.
The most important changes are listed below. You can see the full diff on {@link
https://github.com/angular/angular-phonecat/compare/step-9...step-10
GitHub}:
## Controller
__`app/js/controllers.js`:__
<pre>
...
function PhoneDetailCtrl($xhr) {
var self = this;
$xhr('GET', 'phones/' + self.params.phoneId + '.json', function(code, response) {
self.phone = response;
self.mainImageUrl = response.images[0];
self.mainImageUrl = response.images;
});
self.setImage = function(imageUrl) {
self.mainImageUrl = imageUrl;
}
}
//PhoneDetailCtrl.$inject = ['$xhr'];
</pre>
In the `PhoneDetailCtrl` controller, we created the `mainImageUrl` model property and set its
default value to the first phone image url.
We also created a `setImage` 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)">
@ -78,38 +99,52 @@ __`app/partials/phone-detail.html`:__
...
</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.
<img src="img/tutorial/tutorial_10-11_final.png">
## Test
To verify this new feature, we added two end-to-end tests. One verifies that the main image is set
to the first phone image by default. The second test clicks on several thumbnail images and
verifies that the main image changed appropriately.
__`test/e2e/scenarios.js`:__
<pre>
...
describe('Phone detail view', function() {
beforeEach(function() {
browser().navigateTo('../../app/index.html#/phones/nexus-s');
});
it('should display the first phone image as the main phone image', function() {
expect(element('img.phone').attr('src')).toBe('img/phones/nexus-s.0.jpg');
});
it('should swap main image if a thumbnail image is clicked on', function() {
element('.phone-thumbs li:nth-child(3) img').click();
expect(element('img.phone').attr('src')).toBe('img/phones/nexus-s.2.jpg');
element('.phone-thumbs li:nth-child(1) img').click();
expect(element('img.phone').attr('src')).toBe('img/phones/nexus-s.0.jpg');
});
@ -117,46 +152,59 @@ __`test/e2e/scenarios.js`:__
});
</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}.
# Experiments
* Let's add a new controller method to `PhoneCatCtrl`:
this.hello(name) = function(name) {
alert('Hello ' + (name || 'world') + '!');
}
and add:
<button ng:click="hello('Elmo')">Hello</button>
to the `index.html` template.
The controller methods are inherited between controllers/scopes, so you can use the same snippet
in the `phone-list.html` template as well.
in the `phone-list.html` template as well.
* Move the `hello` method from `PhoneCatCtrl` to `PhoneListCtrl` and you'll see that the button
declared in `index.html` will stop working, while the one declared in the `phone-list.html`
template remains operational.
# Summary
With the phone image swapper in place, we're ready for step 11 (the last step!) to learn an even
better way to fetch data.
<table id="tutorial_nav">
<tr>
<td id="previous_step">{@link tutorial.step_09 Previous}</td>
<td id="step_result">{@link http://angular.github.com/angular-phonecat/step-10/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-9...step-10
Code Diff}</td>
<td id="code_diff">{@link https://github.com/angular/angular-phonecat/compare/step-9...step-10 Code
Diff}</td>
<td id="next_step">{@link tutorial.step_11 Next}</td>
</tr>
</table>

View file

@ -1,4 +1,4 @@
@ngdoc overview
@ngdoc overview
@name Tutorial: Step 11
@description
<table id="tutorial_nav">
@ -13,44 +13,59 @@ Code Diff}</td>
</tr>
</table>
In this step, you will improve the way our app fetches data.
In this step, you will improve the way our app fetches data.
1. Reset your workspace to Step 11 using:
git checkout --force step-11
git checkout -f step-11
or
./goto_step.sh 11
2. Refresh your browser or check the app out on {@link
http://angular.github.com/angular-phonecat/step-11/app angular's server}.
The last improvement we will make to our app is to define a custom service that represents a
{@link http://en.wikipedia.org/wiki/Representational_State_Transfer RESTful} client. Using this
client we can make xhr requests for data in an easier way, without having to deal with the
lower-level {@link angular.service.$xhr $xhr} API, HTTP methods and URLs.
The last improvement we will make to our app is to define a custom service that represents a {@link
http://en.wikipedia.org/wiki/Representational_State_Transfer RESTful} client. Using this client we
can make xhr requests for data in an easier way, without having to deal with the lower-level {@link
angular.service.$xhr $xhr} API, HTTP methods and URLs.
The most important changes are listed below. You can see the full diff on {@link
https://github.com/angular/angular-phonecat/compare/step-10...step-11
GitHub}:
## Template
The custom service is defined in `app/js/services.js` so we need to include this file in our
layout template:
__`app/index.html`.__
## Template
The custom service is defined in `app/js/services.js` so we need to include this file in our layout
template:
__`app/index.html`.__
<pre>
...
<script src="js/services.js"></script>
...
</pre>
## Service
__`app/js/services.js`.__
<pre>
angular.service('Phone', function($resource){
@ -60,29 +75,35 @@ __`app/js/services.js`.__
});
</pre>
We used the {@link angular.service} API to register a custom service. We passed in the name of the
service - 'Phone' - and a factory function. The factory function is similar to a controller's
constructor in that both can declare dependencies via function arguments. The Phone service
declared a dependency on the `$resource` service.
The {@link angular.service.$resource `$resource`} service makes it easy to create a {@link
http://en.wikipedia.org/wiki/Representational_State_Transfer RESTful} client with just a few lines
of code. This client can then be used in our application, instead of the lower-level `$xhr`
service.
of code. This client can then be used in our application, instead of the lower-level `$xhr` service.
## Controller
We simplified our sub-controllers (`PhoneListCtrl` and `PhoneDetailCtrl`) by factoring out the
lower-level `$xhr` service, replacing it with a new service called `Phone`. Angular's {@link
angular.service.$resource `$resource`} service is easier to use than `$xhr` for interacting with
data sources exposed as RESTful resources. It is also easier now to understand what the code in
our controllers is doing.
data sources exposed as RESTful resources. It is also easier now to understand what the code in our
controllers is doing.
__`app/js/controllers.js`.__
<pre>
...
function PhoneListCtrl(Phone_) {
this.orderProp = 'age';
this.phones = Phone_.query();
@ -90,60 +111,78 @@ function PhoneListCtrl(Phone_) {
//PhoneListCtrl.$inject = ['Phone'];
function PhoneDetailCtrl(Phone_) {
var self = this;
self.phone = Phone_.get({phoneId: self.params.phoneId}, function(phone) {
self.mainImageUrl = phone.images[0];
self.mainImageUrl = phone.images;
});
...
}
//PhoneDetailCtrl.$inject = ['Phone'];
</pre>
Notice how in `PhoneListCtrl` we replaced:
$xhr('GET', 'phones/phones.json', function(code, response) {
self.phones = response;
});
with:
this.phones = Phone_.query();
This is a simple statement that we want to query for all phones.
An important thing to notice in the code above is that we don't pass any callback functions when
invoking methods of our Phone service. Although it looks as if the result were returned
synchronously, that is not the case at all. What is returned synchronously is a "future" — an
object, which will be filled with data when the xhr response returns. Because of the data-binding
in angular, we can use this future and bind it to our template. Then, when the data arrives, the
view will automatically update.
view will automatically update.
Sometimes, relying on the future object and data-binding alone is not sufficient to do everything
we require, so in these cases, we can add a callback to process the server response. The
`PhoneDetailCtrl` controller illustrates this by setting the `mainImageUrl` in a callback.
`PhoneDetailCtrl` controller illustrates this by setting the `mainImageUrl` in a callback.
## Test
We have modified our unit tests to verify that our new service is issuing HTTP requests and
processing them as expected. The tests also check that our controllers are interacting with the
service correctly.
The `$resource` client augments the response object with methods for updating and deleting the
resource. If we were to use the standard `toEqual` matcher, our tests would fail because the test
values would not match the responses exactly. To solve the problem, we use a newly-defined
`toEqualData` {@link http://pivotal.github.com/jasmine/jsdoc/symbols/jasmine.Matchers.html Jasmine
matcher}. When the `toEqualData` matcher compares two objects, it takes only object properties
into account and ignores methods.
matcher}. When the `toEqualData` matcher compares two objects, it takes only object properties into
account and ignores methods.
__`test/unit/controllersSpec.js`:__
<pre>
describe('PhoneCat controllers', function() {
beforeEach(function(){
this.addMatchers({
toEqualData: function(expected) {
@ -152,73 +191,93 @@ describe('PhoneCat controllers', function() {
});
});
describe('PhoneListCtrl', function(){
var scope, $browser, ctrl;
beforeEach(function() {
scope = angular.scope();
$browser = scope.$service('$browser');
$browser.xhr.expectGET('phones/phones.json').respond([{name: 'Nexus S'},
{name: 'Motorola DROID'}]);
ctrl = scope.$new(PhoneListCtrl);
});
it('should create "phones" model with 2 phones fetched from xhr', function() {
expect(ctrl.phones).toEqual([]);
$browser.xhr.flush();
expect(ctrl.phones).toEqualData([{name: 'Nexus S'},
{name: 'Motorola DROID'}]);
});
it('should set the default value of orderProp model', function() {
expect(ctrl.orderProp).toBe('age');
});
});
describe('PhoneDetailCtrl', function(){
var scope, $browser, ctrl;
beforeEach(function() {
scope = angular.scope();
$browser = scope.$service('$browser');
});
beforeEach(function() {
scope = angular.scope();
$browser = scope.$service('$browser');
});
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)
# Summary
There you have it! We have created a web app in a relatively short amount of time.
<table id="tutorial_nav">
<tr>
<td id="previous_step">{@link tutorial.step_10 Previous}</td>
@ -227,6 +286,9 @@ There you have it! We have created a web app in a relatively short amount of ti
<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>
<td id="next_step">{@link tutorial/the_end Next}</td>
</tr>
</table>

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB