add new batch of tutorial docs and images
|
|
@ -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}.
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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/Model–View–Controller the MVC design pattern} to decouple
|
||||
the code and separate concerns. With that in mind, let's use a little angular and JavaScript to
|
||||
add Model, View, and Controller components to our app.
|
||||
|
||||
1. Reset your workspace to step 2 using:
|
||||
There are many ways to structure the code for an application. With angular, we encourage the use of
|
||||
{@link http://en.wikipedia.org/wiki/Model–View–Controller the MVC design pattern} to decouple the
|
||||
code and separate concerns. With that in mind, let's use a little angular and JavaScript to add
|
||||
model, view, and controller components to our app.
|
||||
|
||||
|
||||
1. Reset your workspace to step 2.
|
||||
|
||||
|
||||
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>
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
BIN
docs/img/tutorial/catalog_screen.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
docs/img/tutorial/tutorial_00_final.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
docs/img/tutorial/tutorial_02_final.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
docs/img/tutorial/tutorial_03_final.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
docs/img/tutorial/tutorial_04-06_final.png
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
docs/img/tutorial/tutorial_07_final.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
docs/img/tutorial/tutorial_08-09_final.png
Normal file
|
After Width: | Height: | Size: 209 KiB |
BIN
docs/img/tutorial/tutorial_10-11_final.png
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
docs/img/tutorial/xhr_service_final.png
Normal file
|
After Width: | Height: | Size: 179 KiB |