mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-16 23:30:23 +00:00
docs(tutorial): update all the remaining steps
I made some diagrams and portions of the text that are stil stale invisible. We'll fix these in the next relese.
This commit is contained in:
parent
2b87c814ab
commit
075c089b5c
12 changed files with 474 additions and 357 deletions
|
|
@ -149,16 +149,17 @@ The code contains some key Angular elements that we will need going forward.
|
|||
__`app/index.html`:__
|
||||
<pre>
|
||||
<!doctype html>
|
||||
<html ng-app>
|
||||
<html lang="en" ng-app>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>my angular app</title>
|
||||
<title>My HTML File</title>
|
||||
<link rel="stylesheet" href="css/app.css">
|
||||
<link rel="stylesheet" href="css/bootstrap.css">
|
||||
<script src="lib/angular/angular.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Nothing here {{'yet' + '!'}}
|
||||
<p>Nothing here {{'yet' + '!'}}</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -245,8 +246,9 @@ scripts and a simple example app, all pre-configured for developing a typical we
|
|||
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`
|
||||
* Added phone images to `app/img/phones/`
|
||||
* Added phone data files (JSON) to `app/phones/`
|
||||
* Added [Bootstrap](http://twitter.github.com/bootstrap/) files to `app/css/` and `app/img/`
|
||||
|
||||
|
||||
|
||||
|
|
@ -265,9 +267,9 @@ Now let's go to {@link step_01 step 1} and add some content to the web app.
|
|||
|
||||
<ul doc:tutorial-nav="0"></ul>
|
||||
|
||||
Move elsewhere:
|
||||
|
||||
<div style="display: none">
|
||||
Note: During the bootstrap the injector and the root scope will then be associated with the
|
||||
element on which the `ngApp` directive was declared, so when debugging the app you can retrieve
|
||||
them from browser console via `angular.element(rootElement).scope()` and
|
||||
`angular.element(rootElement).injector()`.
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -75,12 +75,14 @@ the `PhoneListCtrl` __controller__:
|
|||
__`app/js/controllers.js`:__
|
||||
<pre>
|
||||
function PhoneListCtrl($scope) {
|
||||
$scope.phones = [{"name": "Nexus S",
|
||||
"snippet": "Fast just got faster with Nexus S."},
|
||||
{"name": "Motorola XOOM™ with Wi-Fi",
|
||||
"snippet": "The Next, Next Generation tablet."},
|
||||
{"name": "MOTOROLA XOOM™",
|
||||
"snippet": "The Next, Next Generation tablet."}];
|
||||
$scope.phones = [
|
||||
{"name": "Nexus S",
|
||||
"snippet": "Fast just got faster with Nexus S."},
|
||||
{"name": "Motorola XOOM™ with Wi-Fi",
|
||||
"snippet": "The Next, Next Generation tablet."},
|
||||
{"name": "MOTOROLA XOOM™",
|
||||
"snippet": "The Next, Next Generation tablet."}
|
||||
];
|
||||
}
|
||||
</pre>
|
||||
|
||||
|
|
@ -120,7 +122,7 @@ __`test/unit/controllersSpec.js`:__
|
|||
<pre>
|
||||
describe('PhoneCat controllers', function() {
|
||||
|
||||
describe('PhoneListCtrl', function() {
|
||||
describe('PhoneListCtrl', function(){
|
||||
|
||||
it('should create "phones" model with 3 phones', function() {
|
||||
var scope = {},
|
||||
|
|
@ -163,7 +165,7 @@ execute the tests and report the results in the terminal.
|
|||
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)
|
||||
Chrome 19.0.1084.36 Mac OS: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (2.00 ms)
|
||||
|
||||
Yay! The test passed! Or not...
|
||||
|
||||
|
|
|
|||
|
|
@ -32,14 +32,27 @@ We made no changes to the controller.
|
|||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
Fulltext Search: <input ng-model="query">
|
||||
<div class="container-fluid">
|
||||
<div class="row-fluid">
|
||||
<div class="span2">
|
||||
<!--Sidebar content-->
|
||||
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in phones | filter:query">
|
||||
{{phone.name}}
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
Search: <input ng-model="query">
|
||||
|
||||
</div>
|
||||
<div class="span10">
|
||||
<!--Body content-->
|
||||
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in phones | filter:query">
|
||||
{{phone.name}}
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
...
|
||||
</pre>
|
||||
|
||||
|
|
@ -54,7 +67,7 @@ list. This new code demonstrates the following:
|
|||
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
|
||||
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.
|
||||
|
||||
|
|
@ -86,6 +99,7 @@ describe('PhoneCat App', 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -25,25 +25,21 @@ The most important differences between Steps 3 and 4 are listed below. You can s
|
|||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
<ul class="controls">
|
||||
<li>
|
||||
Search: <input ng-model="query">
|
||||
</li>
|
||||
<li>
|
||||
Sort by:
|
||||
<select ng-model="orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="age">Newest</option>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
Search: <input ng-model="query">
|
||||
Sort by:
|
||||
<select ng-model="orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="age">Newest</option>
|
||||
</select>
|
||||
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp">
|
||||
{{phone.name}}
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp">
|
||||
{{phone.name}}
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
</pre>
|
||||
|
||||
|
|
@ -72,18 +68,18 @@ necessary!
|
|||
|
||||
__`app/js/controller.js`:__
|
||||
<pre>
|
||||
/* App Controllers */
|
||||
|
||||
function PhoneListCtrl($scope) {
|
||||
$scope.phones = [{"name": "Nexus S",
|
||||
"snippet": "Fast just got faster with Nexus S.",
|
||||
"age": 0},
|
||||
{"name": "Motorola XOOM™ with Wi-Fi",
|
||||
"snippet": "The Next, Next Generation tablet.",
|
||||
"age": 1},
|
||||
{"name": "MOTOROLA XOOM™",
|
||||
"snippet": "The Next, Next Generation tablet.",
|
||||
"age": 2}];
|
||||
$scope.phones = [
|
||||
{"name": "Nexus S",
|
||||
"snippet": "Fast just got faster with Nexus S.",
|
||||
"age": 0},
|
||||
{"name": "Motorola XOOM™ with Wi-Fi",
|
||||
"snippet": "The Next, Next Generation tablet.",
|
||||
"age": 1},
|
||||
{"name": "MOTOROLA XOOM™",
|
||||
"snippet": "The Next, Next Generation tablet.",
|
||||
"age": 2}
|
||||
];
|
||||
|
||||
$scope.orderProp = 'age';
|
||||
}
|
||||
|
|
@ -114,11 +110,11 @@ __`test/unit/controllerSpec.js`:__
|
|||
<pre>
|
||||
describe('PhoneCat controllers', function() {
|
||||
|
||||
describe('PhoneListCtrl', function() {
|
||||
var scope, $browser, ctrl;
|
||||
describe('PhoneListCtrl', function(){
|
||||
var scope, ctrl;
|
||||
|
||||
beforeEach(function() {
|
||||
scope = {};
|
||||
scope = {},
|
||||
ctrl = new PhoneListCtrl(scope);
|
||||
});
|
||||
|
||||
|
|
@ -147,7 +143,7 @@ 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)
|
||||
Chrome 19.0.1084.36 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.
|
||||
|
|
@ -157,15 +153,14 @@ __`test/e2e/scenarios.js`:__
|
|||
...
|
||||
it('should be possible to control phone order via the drop down select box',
|
||||
function() {
|
||||
|
||||
// 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('phone.name')).
|
||||
toEqual(["Motorola XOOM\u2122 with Wi-Fi",
|
||||
"MOTOROLA XOOM\u2122"]);
|
||||
|
||||
select('orderProp').option('alphabetical');
|
||||
select('orderProp').option('Alphabetical');
|
||||
|
||||
expect(repeater('.phones li', 'Phone List').column('phone.name')).
|
||||
toEqual(["MOTOROLA XOOM\u2122",
|
||||
|
|
@ -183,12 +178,9 @@ Angular's server}.
|
|||
|
||||
# Experiments
|
||||
|
||||
<div style="display:none">
|
||||
!!!!! TODO(i): we need to fix select/option to support unknown option !!!!!
|
||||
* 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".
|
||||
</div>
|
||||
you'll see that Angular will temporarily add a new "unknown" option to the drop-down list and the
|
||||
ordering will default to unordered/natural order.
|
||||
|
||||
* Add an `{{orderProp}}` binding into the `index.html` template to display its current value as
|
||||
text.
|
||||
|
|
|
|||
|
|
@ -2,14 +2,12 @@
|
|||
@name Tutorial: 5 - XHRs & Dependency Injection
|
||||
@description
|
||||
|
||||
<h2 style="color: red">This page has not been updated for AngularJS 1.0 yet</h2>
|
||||
|
||||
<ul doc:tutorial-nav="5"></ul>
|
||||
|
||||
|
||||
Enough of building an app with three phones in a hard-coded dataset! Let's fetch a larger dataset
|
||||
from our server using one of angular's built-in {@link api/angular.module.ng services} called {@link
|
||||
api/angular.module.ng.$xhr $xhr}. We will use angular's {@link guide/dev_guide.di dependency
|
||||
api/angular.module.ng.$http $http}. We will use angular's {@link guide/dev_guide.di dependency
|
||||
injection (DI)} to provide the service to the `PhoneListCtrl` controller.
|
||||
|
||||
|
||||
|
|
@ -44,8 +42,8 @@ Following is a sample of the file:
|
|||
|
||||
## Controller
|
||||
|
||||
We'll use angular's {@link api/angular.module.ng.$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
|
||||
We'll use angular's {@link api/angular.module.ng.$http $http} service in our controller to make an HTTP
|
||||
request to your web server to fetch the data in the `app/phones/phones.json` file. `$http` is just
|
||||
one of several built-in {@link api/angular.module.ng angular services} that handle common operations
|
||||
in web apps. Angular injects these services for you where you need them.
|
||||
|
||||
|
|
@ -56,38 +54,41 @@ 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;
|
||||
function PhoneListCtrl($scope, $http) {
|
||||
$http.get('phones/phones.json').success(function(data) {
|
||||
$scope.phones = data;
|
||||
});
|
||||
|
||||
self.orderProp = 'age';
|
||||
$scope.orderProp = 'age';
|
||||
}
|
||||
|
||||
//PhoneListCtrl.$inject = ['$xhr'];
|
||||
//PhoneListCtrl.$inject = ['$scope', '$http'];
|
||||
</pre>
|
||||
|
||||
`$xhr` makes an HTTP GET request to our web server, asking for `phone/phones.json` (the url is
|
||||
`$http` 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.)
|
||||
|
||||
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!
|
||||
The `$http` service returns a {@link api/angular.module.ng.$q promise object} with a `success`
|
||||
method. We call this method to handle the asynchronous response and assign the phone data to the
|
||||
scope controlled by this controller, as a model called `phones`. Notice that angular detected the
|
||||
json response and parsed it for us!
|
||||
|
||||
To use a service in angular, you simply declare the names of the 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 dependencies you need as arguments
|
||||
to the controller's constructor function, as follows:
|
||||
|
||||
function PhoneListCtrl($xhr) {...}
|
||||
function PhoneListCtrl($scope, $http) {...}
|
||||
|
||||
Angular's dependency injector provides services to your controller when the controller is being
|
||||
constructed. The dependency injector also takes care of creating any transitive dependencies the
|
||||
service may have (services often depend upon other services).
|
||||
|
||||
Note that the names of arguments are significant, because the injector uses these to look up the
|
||||
dependencies.
|
||||
|
||||
|
||||
<img src="img/tutorial/xhr_service_final.png">
|
||||
|
||||
|
||||
|
|
@ -109,7 +110,16 @@ To overcome issues caused by minification, just assign an array with service ide
|
|||
into the `$inject` property of the controller function, just like the last line in the snippet
|
||||
(commented out) suggests:
|
||||
|
||||
PhoneListCtrl.$inject = ['$xhr'];
|
||||
PhoneListCtrl.$inject = ['$scope', '$http'];
|
||||
|
||||
There is also one more way to specify this dependency list and avoid minification issues — using the
|
||||
bracket notation which wraps the function to be injected into an array of strings (representing the
|
||||
dependency names) followed by the function to be injected:
|
||||
|
||||
var PhoneListCtrl = ['$scope', '$http', function($scope, $http) { /* constructor body */ }];
|
||||
|
||||
Both of these methods work with any function that can be injected by Angular, so it's up to your
|
||||
project's style guide to decide which one you use.
|
||||
|
||||
|
||||
## Test
|
||||
|
|
@ -118,63 +128,73 @@ __`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
|
||||
constructor with some kind of fake `$http` 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;
|
||||
describe('PhoneListCtrl', function(){
|
||||
var scope, ctrl, $httpBackend;
|
||||
|
||||
beforeEach(function() {
|
||||
scope = angular.module.ng.$rootScope.Scope();
|
||||
$browser = scope.$service('$browser');
|
||||
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/phones.json').
|
||||
respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
|
||||
|
||||
$browser.xhr.expectGET('phones/phones.json')
|
||||
.respond([{name: 'Nexus S'},
|
||||
{name: 'Motorola DROID'}]);
|
||||
ctrl = scope.$new(PhoneListCtrl);
|
||||
});
|
||||
});
|
||||
scope = $rootScope.$new();
|
||||
ctrl = $controller(PhoneListCtrl, {$scope: scope});
|
||||
}));
|
||||
</pre>
|
||||
|
||||
Note: Because we loaded Jasmine and `angular-mocks.js` in our test environment, we got two helper
|
||||
methods {@link api/angular.mock.module module} and {@link api/angular.mock.inject inject} that we'll
|
||||
use to access and configure the injector.
|
||||
|
||||
We created the controller in the test environment, as follows:
|
||||
|
||||
* We created a root scope object by calling `angular.module.ng.$rootScope.Scope()`
|
||||
* We used the `inject` helper method to inject instances of
|
||||
{@link api/angular.module.ng.$rootScope $rootScope},
|
||||
{@link api/angular.module.ng.$controller $controller} and
|
||||
{@link api/angular.module.ng.$httpBackend $httpBackend} services into the Jasmine's `beforeEach`
|
||||
function. These instances come from an injector which is recreated from scratch for every single
|
||||
test. This guarantees that each test starts from a well known starting point and each test is
|
||||
isolated from the work done in other tests.
|
||||
|
||||
* We called `scope.$new(PhoneListCtrl)` to get angular to create the child scope associated with
|
||||
the `PhoneListCtrl` controller
|
||||
* We created a new scope for our controller by calling `$rootScope.$new()`
|
||||
|
||||
Because our code now uses the `$xhr` service to fetch the phone list data in our controller, before
|
||||
* We called `scope.$new(PhoneListCtrl)` to get Angular to create the child scope associated with
|
||||
the `PhoneListCtrl` controller.
|
||||
|
||||
Because our code now uses the `$http` service to fetch the phone list data in our controller, before
|
||||
we create the `PhoneListCtrl` child scope, we need to tell the testing harness to expect an
|
||||
incoming request from the controller. To do this we:
|
||||
|
||||
* Use the {@link api/angular.module.ng.$rootScope.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.
|
||||
* Request `$httpBackend` service to be injected into our `beforeEach` function. This is a mock
|
||||
mock version of the service that in production environment facilitates all XHR and JSONP requests.
|
||||
The mock version of this service allows you to write tests without having to deal with
|
||||
native APIs and the global state associated with them — both of which make testing a nightmare.
|
||||
|
||||
* Use the `$browser.xhr.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.
|
||||
* Use the `$httpBackend.expectGET` method to train the `$httpBackend` service to expect an incoming
|
||||
HTTP request and tell it what to respond with. Note that the responses are not returned until we call
|
||||
the `$httpBackend.flush` method.
|
||||
|
||||
Now, we will make assertions to verify that the `phones` model doesn't exist on 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(scope.phones).toBeUndefined();
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(ctrl.phones).toEqual([{name: 'Nexus S'},
|
||||
expect(scope.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 flush the request queue in the browser by calling `$httpBackend.flush()`. This causes the
|
||||
promise returned by the `$http` service to be resolved with the trained response.
|
||||
|
||||
* We make the assertions, verifying that the phone model now exists on the scope.
|
||||
|
||||
|
|
@ -182,7 +202,7 @@ 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');
|
||||
expect(scope.orderProp).toBe('age');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -194,24 +214,24 @@ 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)
|
||||
Chrome 19.0.1084.36 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.
|
||||
* At the bottom of `index.html`, add a `{{phones | json}}` 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:
|
||||
* In the `PhoneListCtrl` controller, pre-process the http response by limiting the number of phones
|
||||
to the first 5 in the list. Use the following code in the $http callback:
|
||||
|
||||
self.phones = response.splice(0, 5);
|
||||
$scope.phones = data.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 {@link step_06 step 6}, where you will add some
|
||||
Now that you have learned how easy it is to use angular services (thanks to Angular's dependency
|
||||
injection), go to {@link step_06 step 6}, where you will add some
|
||||
thumbnail images of phones and some links.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
@name Tutorial: 6 - Templating Links & Images
|
||||
@description
|
||||
|
||||
<h2 style="color: red">This page has not been updated for AngularJS 1.0 yet</h2>
|
||||
|
||||
<ul doc:tutorial-nav="6"></ul>
|
||||
|
||||
|
||||
|
|
@ -47,26 +45,27 @@ __`app/phones/phones.json`__ (sample snippet):
|
|||
__`app/index.html`:__
|
||||
<pre>
|
||||
...
|
||||
<ul class="phones">
|
||||
<li ng:repeat="phone in phones.$filter(query).$orderBy(orderProp)">
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<a href="#/phones/{{phone.id}}" class="thumb"><img ng:src="{{phone.imageUrl}}"></a>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp" class="thumbnail">
|
||||
<a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
</pre>
|
||||
|
||||
To dynamically generate links that will in the future lead to phone detail pages, we used the
|
||||
now-familiar {@link guide/dev_guide.compiler.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.
|
||||
now-familiar double-curly brace binding in the `href` attribute values. In step 2, we added the
|
||||
`{{phone.name}}` binding as the element content. In this step the `{{phone.id}}` binding is used in
|
||||
the element attribute.
|
||||
|
||||
We also added phone images next to each record using an image tag with the {@link
|
||||
api/angular.directive.ng:src ng:src} directive. That directive prevents the browser from treating
|
||||
the angular `{{ expression }}` markup literally, which it would have done if we had only specified
|
||||
an attribute binding in a regular `src` attribute (`<img src="{{phone.imageUrl}}">`). Using
|
||||
`ng:src` prevents the browser from making an http request to an invalid location.
|
||||
api/angular.module.ng.$compileProvider.directive.ngSrc ngSrc} directive. That directive prevents the
|
||||
browser from treating the angular `{{ expression }}` markup literally, and initiating a request to
|
||||
invalid url `http://localhost:8000/app/{{phone.imageUrl}}`, which it would have done if we had only
|
||||
specified an attribute binding in a regular `src` attribute (`<img src="{{phone.imageUrl}}">`).
|
||||
Using `ngSrc` (`ng-src`) prevents the browser from making an http request to an invalid location.
|
||||
|
||||
|
||||
## Test
|
||||
|
|
@ -77,7 +76,7 @@ __`test/e2e/scenarios.js`__:
|
|||
it('should render phone specific links', function() {
|
||||
input('query').enter('nexus');
|
||||
element('.phones li a').click();
|
||||
expect(browser().location().hash()).toBe('/phones/nexus-s');
|
||||
expect(browser().location().url()).toBe('/phones/nexus-s');
|
||||
});
|
||||
...
|
||||
</pre>
|
||||
|
|
@ -92,10 +91,10 @@ angular's server}.
|
|||
|
||||
# Experiments
|
||||
|
||||
* Replace the `ng:src` directive with a plain old `<src>` attribute. Using tools such as Firebug,
|
||||
* 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}}`).
|
||||
`/app/{{phone.imageUrl}}`).
|
||||
|
||||
|
||||
# Summary
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
@name Tutorial: 7 - Routing & Multiple Views
|
||||
@description
|
||||
|
||||
<h2 style="color: red">This page has not been updated for AngularJS 1.0 yet</h2>
|
||||
|
||||
<ul doc:tutorial-nav="7"></ul>
|
||||
|
||||
|
||||
|
|
@ -14,8 +12,9 @@ multiple views by adding routing.
|
|||
<doc:tutorial-instructions step="7"></doc:tutorial-instructions>
|
||||
|
||||
|
||||
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.
|
||||
Note that when you now navigate to `app/index.html`, 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
|
||||
|
|
@ -36,42 +35,61 @@ template into what we call a "layout template". This is a template that is commo
|
|||
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 api/angular.module.ng.$route $route}
|
||||
service. This service makes it easy to wire together controllers, view templates, and the current
|
||||
Application routes in angular are declared via the
|
||||
{@link api/angular.module.ng.$routeProvider $routeProvider}, which is the provider of the
|
||||
{@link api/angular.module.ng.$route $route service}. This service makes it easy to wire together
|
||||
controllers, view templates, and the current
|
||||
URL location in the browser. Using this feature we can implement {@link
|
||||
http://en.wikipedia.org/wiki/Deep_linking deep linking}, which lets us utilize the browser's
|
||||
history (back and forward navigation) and bookmarks.
|
||||
|
||||
|
||||
## Controllers
|
||||
### A Note About DI, Injector and Providers
|
||||
|
||||
__`app/js/controller.js`:__
|
||||
As you noticed the dependency injection is the core feature of AngularJS, so it's important for you
|
||||
to understand a thing or two about how it works.
|
||||
|
||||
When the application bootstraps, Angular creates an injector that will be used for all DI stuff in
|
||||
this app. The injector itself doesn't know anything about what `$http` or `$route` services do, in
|
||||
fact it doesn't even know about the existence of these services unless it is configured with proper
|
||||
module definitions. The sole responsibilities of the injector are to load specified module
|
||||
definition(s), register all service providers defined in these modules and when asked inject
|
||||
a specified function with dependencies (services) that it lazily instantiates via their providers.
|
||||
|
||||
Providers are objects that provide (create) instances of services and expose configuration apis
|
||||
that can be used to control the creation and runtime behavior of a service. In case of the `$route`
|
||||
service, the `$routeProvider` exposes apis that allow you to define routes for your application.
|
||||
|
||||
Angular modules solve the problem of removing global state from the application and provide a way
|
||||
of configuring the injector. As opposed to AMD or require.js modules, Angular modules don't try to
|
||||
solve the problem of script load ordering or lazy script fetching. These goals are orthogonal and
|
||||
both module systems can live side by side and fulfil their goals.
|
||||
|
||||
## The App Module
|
||||
|
||||
__`app/js/app.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'];
|
||||
...
|
||||
angular.module('phonecat', []).
|
||||
config(['$routeProvider', function($routeProvider) {
|
||||
$routeProvider.
|
||||
when('/phones', {template: 'partials/phone-list.html', controller: PhoneListCtrl}).
|
||||
when('/phones/:phoneId', {template: 'partials/phone-detail.html', controller: PhoneDetailCtrl}).
|
||||
otherwise({redirectTo: '/phones'});
|
||||
}]);
|
||||
</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:
|
||||
In order to configure our application with routes, we need to create a module for our application.
|
||||
We call this module `phonecatApp` and using the `config` api we request the `$routeProvider` to be
|
||||
injected into our config function and use `$routeProvider.when` api to define our routes.
|
||||
|
||||
Note that during the injector configuration phase, the providers can be injected as well, but they
|
||||
will not be available for injection once the injector is created and starts creating service
|
||||
instances.
|
||||
|
||||
Our application routes were defined as follows:
|
||||
|
||||
* The phone list view will be shown when the URL hash fragment is `/phones`. To construct this
|
||||
view, angular will use the `phone-list.html` template and the `PhoneListCtrl` controller.
|
||||
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
|
||||
|
|
@ -83,78 +101,107 @@ empty `PhoneDetailCtrl` controller to the `app/js/controllers.js` file for the p
|
|||
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 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. 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.
|
||||
URL. All variables defined with the `:` notation are extracted into the
|
||||
{@link api/angular.module.ng.$routeParams $routeParams} object.
|
||||
|
||||
The `params` alias created in the {@link api/angular.module.ng.$route `$route.onChange`} callback
|
||||
allows us to use the `phoneId` property of this map in the `phone-details.html` template.
|
||||
|
||||
In order for our application to bootstrap with our newly created module we'll also need to specify
|
||||
the module name as the value of the {@link api/angular.module.ng.$compileProvider.directive.ngApp ngApp}
|
||||
directive:
|
||||
|
||||
__`app/index.html`:__
|
||||
<pre>
|
||||
<!doctype html>
|
||||
<html ng-app="phonecat">
|
||||
...
|
||||
</pre>
|
||||
|
||||
|
||||
## Controllers
|
||||
|
||||
__`app/js/controller.js`:__
|
||||
<pre>
|
||||
...
|
||||
function PhoneDetailCtrl($scope, $routeParams) {
|
||||
$scope.phoneId = $routeParams.phoneId;
|
||||
}
|
||||
|
||||
//PhoneDetailCtrl.$inject = ['$scope', '$routeParams'];
|
||||
</pre>
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
The `$route` service is usually used in conjunction with the {@link api/angular.widget.ng:view
|
||||
ng:view} widget. The role of the `ng:view` widget is to include the view template for the current
|
||||
The `$route` service is usually used in conjunction with the {@link api/angular.module.ng.$compileProvider.directive.ngView
|
||||
ngView} directive. The role of the `ngView` directive 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>
|
||||
<html ng:app>
|
||||
<html ng-app="phonecat">
|
||||
<head>
|
||||
...
|
||||
<body ng:controller="PhoneCatCtrl">
|
||||
|
||||
<ng:view></ng:view>
|
||||
|
||||
<script src="lib/angular/angular.js"></script>
|
||||
<script src="js/controllers.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div ng-view></div>
|
||||
|
||||
</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:
|
||||
line containing a div with `ng-view` attribute. The code that we removed was placed into the
|
||||
`phone-list.html` template:
|
||||
|
||||
__`app/partials/phone-list.html`:__
|
||||
<pre>
|
||||
<ul class="predicates">
|
||||
<li>
|
||||
Search: <input type="text" ng:model="query"/>
|
||||
</li>
|
||||
<li>
|
||||
Sort by:
|
||||
<select ng:model="orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="age">Newest</option>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="container-fluid">
|
||||
<div class="row-fluid">
|
||||
<div class="span2">
|
||||
<!--Sidebar content-->
|
||||
|
||||
<ul class="phones">
|
||||
<li ng:repeat="phone in phones.$filter(query).$orderBy(orderProp)">
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<a href="#/phones/{{phone.id}}" class="thumb"><img ng:src="{{phone.imageUrl}}"></a>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
Search: <input ng-model="query">
|
||||
Sort by:
|
||||
<select ng-model="orderProp">
|
||||
<option value="name">Alphabetical</option>
|
||||
<option value="age">Newest</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
<div class="span10">
|
||||
<!--Body content-->
|
||||
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp" class="thumbnail">
|
||||
<a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</pre>
|
||||
|
||||
<div style="display:none">
|
||||
TODO!
|
||||
<img src="img/tutorial/tutorial_07_final.png">
|
||||
</div>
|
||||
|
||||
We also added a placeholder template for the phone details view:
|
||||
|
||||
__`app/partials/phone-detail.html`:__
|
||||
<pre>
|
||||
TBD: detail view for {{params.phoneId}}
|
||||
TBD: detail view for {{phoneId}}
|
||||
</pre>
|
||||
|
||||
Note how we are using `params` model defined in the `PhoneCatCtrl` controller.
|
||||
Note how we are using `phoneId` model defined in the `PhoneDetailCtrl` controller.
|
||||
|
||||
|
||||
## Test
|
||||
|
|
@ -165,21 +212,21 @@ to various URLs and verify that the correct view was rendered.
|
|||
<pre>
|
||||
...
|
||||
it('should redirect index.html to index.html#/phones', function() {
|
||||
browser().navigateTo('../../app/index.html');
|
||||
expect(browser().location().hash()).toBe('/phones');
|
||||
browser().navigateTo('../../app/index.html');
|
||||
expect(browser().location().url()).toBe('/phones');
|
||||
});
|
||||
...
|
||||
|
||||
describe('Phone detail view', function() {
|
||||
|
||||
beforeEach(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');
|
||||
});
|
||||
it('should display placeholder page with phoneId', function() {
|
||||
expect(binding('phoneId')).toBe('nexus-s');
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
|
|
@ -194,14 +241,16 @@ angular's server}.
|
|||
|
||||
* 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.
|
||||
scope managed by `PhoneListCtrl`, which is associated with the `<div ng-view>` element. If you add
|
||||
the same binding into the `phone-list.html` template, the binding will work as expected.
|
||||
|
||||
<div style="display: none">
|
||||
* In `PhoneCatCtrl`, create a new model called "`hero`" with `this.hero = 'Zoro'`. In
|
||||
`PhoneListCtrl` let's shadow it with `this.hero = 'Batman'`, 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.
|
||||
</div>
|
||||
|
||||
# Summary
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
@name Tutorial: 8 - More Templating
|
||||
@description
|
||||
|
||||
<h2 style="color: red">This page has not been updated for AngularJS 1.0 yet</h2>
|
||||
|
||||
<ul doc:tutorial-nav="8"></ul>
|
||||
|
||||
|
||||
|
|
@ -17,8 +15,8 @@ phone in the phone list.
|
|||
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 api/angular.module.ng.$xhr $xhr} to fetch our
|
||||
data, and we'll flesh out the `phone-details.html` view template.
|
||||
To implement the phone details view we will use {@link api/angular.module.ng.$http $http} 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
|
||||
|
|
@ -58,44 +56,42 @@ 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
|
||||
We'll expand the `PhoneDetailCtrl` by using the `$http` service to fetch the json files. This works
|
||||
the same way as the phone list controller.
|
||||
|
||||
__`app/js/controller.js`:__
|
||||
<pre>
|
||||
function PhoneDetailCtrl($xhr) {
|
||||
var self = this;
|
||||
|
||||
$xhr('GET', 'phones/' + self.params.phoneId + '.json', function(code, response) {
|
||||
self.phone = response;
|
||||
function PhoneDetailCtrl($scope, $routeParams, $http) {
|
||||
$http.get('phones/' + $routeParams.phoneId + '.json').success(function(data) {
|
||||
$scope.phone = data;
|
||||
});
|
||||
}
|
||||
|
||||
//PhoneDetailCtrl.$inject = ['$xhr'];
|
||||
//PhoneDetailCtrl.$inject = ['$scope', '$routeParams', '$http'];
|
||||
</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 `$routeParams.phoneId` extracted from the current
|
||||
route by the `$route` service.
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
The TBD placeholder line has been replaced with lists and bindings that comprise the phone details.
|
||||
Note where we use the angular `{{expression}}` markup and `ng:repeater`s to project phone data from
|
||||
Note where we use the angular `{{expression}}` markup and `ngRepeater`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[0]}}" class="phone">
|
||||
|
||||
<h1>{{phone.name}}</h1>
|
||||
|
||||
<p>{{phone.description}}</p>
|
||||
|
||||
<ul class="phone-thumbs">
|
||||
<li ng:repeat="img in phone.images">
|
||||
<img ng:src="{{img}}"/>
|
||||
<li ng-repeat="img in phone.images">
|
||||
<img ng-src="{{img}}">
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
|
@ -104,7 +100,7 @@ __`app/partials/phone-details.html`:__
|
|||
<span>Availability and Networks</span>
|
||||
<dl>
|
||||
<dt>Availability</dt>
|
||||
<dd ng:repeat="availability in phone.availability">{{availability}}</dd>
|
||||
<dd ng-repeat="availability in phone.availability">{{availability}}</dd>
|
||||
</dl>
|
||||
</li>
|
||||
...
|
||||
|
|
@ -115,7 +111,10 @@ __`app/partials/phone-details.html`:__
|
|||
</ul>
|
||||
</pre>
|
||||
|
||||
<div style="display: none">
|
||||
TODO!
|
||||
<img src="img/tutorial/tutorial_08-09_final.png">
|
||||
</div>
|
||||
|
||||
## Test
|
||||
|
||||
|
|
@ -125,16 +124,26 @@ step 5.
|
|||
__`test/unit/controllerSpec.js`:__
|
||||
<pre>
|
||||
...
|
||||
describe('PhoneDetailCtrl', function(){
|
||||
var scope, $httpBackend, ctrl;
|
||||
|
||||
beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/xyz.json').respond({name:'phone xyz'});
|
||||
|
||||
$routeParams.phoneId = 'xyz';
|
||||
scope = $rootScope.$new();
|
||||
ctrl = $controller(PhoneDetailCtrl, {$scope: scope});
|
||||
}));
|
||||
|
||||
|
||||
it('should fetch phone detail', function() {
|
||||
scope.params = {phoneId:'xyz'};
|
||||
$browser.xhr.expectGET('phones/xyz.json').respond({name:'phone xyz'});
|
||||
ctrl = scope.$new(PhoneDetailCtrl);
|
||||
expect(scope.phone).toBeUndefined();
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(ctrl.phone).toBeUndefined();
|
||||
$browser.xhr.flush();
|
||||
|
||||
expect(ctrl.phone).toEqual({name:'phone xyz'});
|
||||
expect(scope.phone).toEqual({name:'phone xyz'});
|
||||
});
|
||||
});
|
||||
...
|
||||
</pre>
|
||||
|
||||
|
|
@ -144,7 +153,7 @@ 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)
|
||||
Chrome 19.0.1084.36 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
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
@name Tutorial: 9 - Filters
|
||||
@description
|
||||
|
||||
<h2 style="color: red">This page has not been updated for AngularJS 1.0 yet</h2>
|
||||
|
||||
<ul doc:tutorial-nav="9"></ul>
|
||||
|
||||
|
||||
|
|
@ -26,13 +24,15 @@ GitHub}:
|
|||
|
||||
## Custom Filter
|
||||
|
||||
In order to create a new filter, simply register your custom filter function with the {@link
|
||||
api/angular.module.ng.$filter `angular.module.ng.$filter`} API.
|
||||
In order to create a new filter, you are going to create a `phonecatFilters` module and register
|
||||
your custom filter with this module:
|
||||
|
||||
__`app/js/filters.js`:__
|
||||
<pre>
|
||||
angular.module.ng.$filter('checkmark', function(input) {
|
||||
return input ? '\u2713' : '\u2718';
|
||||
angular.module('phonecatFilters', []).filter('checkmark', function() {
|
||||
return function(input) {
|
||||
return input ? '\u2713' : '\u2718';
|
||||
};
|
||||
});
|
||||
</pre>
|
||||
|
||||
|
|
@ -40,6 +40,16 @@ The name of our filter is "checkmark". The `input` evaluates to either `true` or
|
|||
return one of two unicode characters we have chosen to represent true or false (`\u2713` and
|
||||
`\u2718`).
|
||||
|
||||
Now that our filter is ready, we need to register the `phonecatFilters` module as a dependency for
|
||||
our main `phonecat` module.
|
||||
|
||||
__`app/js/app.js`:__
|
||||
<pre>
|
||||
...
|
||||
angular.module('phonecat', ['phonecatFilters']).
|
||||
...
|
||||
</pre>
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
|
|
@ -81,22 +91,32 @@ Filters, like any other component, should be tested and these tests are very eas
|
|||
|
||||
__`test/unit/filtersSpec.js`:__
|
||||
<pre>
|
||||
describe('checkmark filter', function() {
|
||||
describe('filter', function() {
|
||||
|
||||
it('should convert boolean values to unicode checkmark or cross', function() {
|
||||
expect(angular.module.ng.$filter.checkmark(true)).toBe('\u2713');
|
||||
expect(angular.module.ng.$filter.checkmark(false)).toBe('\u2718');
|
||||
beforeEach(module('phonecatFilters'));
|
||||
|
||||
|
||||
describe('checkmark', function() {
|
||||
|
||||
it('should convert boolean values to unicode checkmark or cross',
|
||||
inject(function(checkmarkFilter) {
|
||||
expect(checkmarkFilter(true)).toBe('\u2713');
|
||||
expect(checkmarkFilter(false)).toBe('\u2718');
|
||||
}));
|
||||
});
|
||||
})
|
||||
});
|
||||
</pre>
|
||||
|
||||
Note that you need to configure our test injector with the `phonecatFilters` module before any of
|
||||
our filter tests execute.
|
||||
|
||||
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)
|
||||
Chrome 19.0.1084.36 Mac OS: Run 4 tests (Passed: 4; Fails: 0; Errors 0) (3.00 ms)
|
||||
|
||||
|
||||
# Experiments
|
||||
|
|
@ -111,7 +131,7 @@ following bindings to `index.html`:
|
|||
* We can also create a model with an input element, and combine it with a filtered binding. Add
|
||||
the following to index.html:
|
||||
|
||||
<input ng:model="userInput"> Uppercased: {{ userInput | uppercase }}
|
||||
<input ng-model="userInput"> Uppercased: {{ userInput | uppercase }}
|
||||
|
||||
|
||||
# Summary
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
@name Tutorial: 10 - Event Handlers
|
||||
@description
|
||||
|
||||
<h2 style="color: red">This page has not been updated for AngularJS 1.0 yet</h2>
|
||||
|
||||
<ul doc:tutorial-nav="10"></ul>
|
||||
|
||||
|
||||
|
|
@ -27,52 +25,53 @@ GitHub}:
|
|||
__`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];
|
||||
function PhoneDetailCtrl($scope, $routeParams, $http) {
|
||||
$http.get('phones/' + $routeParams.phoneId + '.json').success(function(data) {
|
||||
$scope.phone = data;
|
||||
$scope.mainImageUrl = data.images[0];
|
||||
});
|
||||
|
||||
self.setImage = function(imageUrl) {
|
||||
self.mainImageUrl = imageUrl;
|
||||
$scope.setImage = function(imageUrl) {
|
||||
$scope.mainImageUrl = imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
//PhoneDetailCtrl.$inject = ['$xhr'];
|
||||
//PhoneDetailCtrl.$inject = ['$scope', '$routeParams', '$http'];
|
||||
</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`.
|
||||
We also created a `setImage` event handler function that will change the value of `mainImageUrl`.
|
||||
|
||||
|
||||
## Template
|
||||
|
||||
__`app/partials/phone-detail.html`:__
|
||||
<pre>
|
||||
<img ng:src="{{mainImageUrl}}" class="phone"/>
|
||||
<img ng-src="{{mainImageUrl}}" class="phone">
|
||||
|
||||
...
|
||||
|
||||
<ul class="phone-thumbs">
|
||||
<li ng:repeat="img in phone.images">
|
||||
<img ng:src="{{img}}" ng:click="setImage(img)">
|
||||
<li ng-repeat="img in phone.images">
|
||||
<img ng-src="{{img}}" ng-click="setImage(img)">
|
||||
</li>
|
||||
</ul>
|
||||
...
|
||||
</pre>
|
||||
|
||||
We bound the `ng:src` attribute of the large image to the `mainImageUrl` property.
|
||||
We bound the `ngSrc` directive of the large image to the `mainImageUrl` property.
|
||||
|
||||
We also registered an {@link api/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.
|
||||
We also registered an {@link api/angular.module.ng.$compileProvider.directive.ngClick `ngClick`}
|
||||
handler with thumbnail images. When a user clicks on one of the thumbnail images, the handler will
|
||||
use the `setImage` event handler function to change the value of the `mainImageUrl` property to the
|
||||
url of the thumbnail image.
|
||||
|
||||
<div style="display: none">
|
||||
TODO!
|
||||
<img src="img/tutorial/tutorial_10-11_final.png">
|
||||
</div>
|
||||
|
||||
## Test
|
||||
|
||||
|
|
@ -85,13 +84,10 @@ __`test/e2e/scenarios.js`:__
|
|||
...
|
||||
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');
|
||||
expect(element('img.phone').attr('src')).toBe('img/phones/nexus-s.0.jpg');
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -113,24 +109,27 @@ angular's server}.
|
|||
|
||||
# Experiments
|
||||
|
||||
* Let's add a new controller method to `PhoneCatCtrl`:
|
||||
* Let's add a new controller method to `PhoneDetailCtrl`:
|
||||
|
||||
this.hello = function(name) {
|
||||
$scope.hello = function(name) {
|
||||
alert('Hello ' + (name || 'world') + '!');
|
||||
}
|
||||
|
||||
and add:
|
||||
|
||||
<button ng:click="hello('Elmo')">Hello</button>
|
||||
<button ng-click="hello('Elmo')">Hello</button>
|
||||
|
||||
to the `index.html` template.
|
||||
to the `phone-details.html` template.
|
||||
|
||||
<div style="display: none">
|
||||
TODO!
|
||||
The controller methods are inherited between controllers/scopes, so you can use the same snippet
|
||||
in the `phone-list.html` template as well.
|
||||
|
||||
* Move the `hello` method from `PhoneCatCtrl` to `PhoneListCtrl` and you'll see that the button
|
||||
declared in `index.html` will stop working, while the one declared in the `phone-list.html`
|
||||
template remains operational.
|
||||
</div>
|
||||
|
||||
|
||||
# Summary
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
@name Tutorial: 11 - REST and Custom Services
|
||||
@description
|
||||
|
||||
<h2 style="color: red">This page has not been updated for AngularJS 1.0 yet</h2>
|
||||
|
||||
<ul doc:tutorial-nav="11"></ul>
|
||||
|
||||
|
||||
|
|
@ -16,7 +14,7 @@ In this step, you will improve the way our app fetches data.
|
|||
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
|
||||
api/angular.module.ng.$xhr $xhr} API, HTTP methods and URLs.
|
||||
api/angular.module.ng.$http $http} 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
|
||||
|
|
@ -26,12 +24,14 @@ GitHub}:
|
|||
## Template
|
||||
|
||||
The custom service is defined in `app/js/services.js` so we need to include this file in our layout
|
||||
template:
|
||||
template. Additionally, we also need to load the `angular-resource.js` file, which contains the
|
||||
`ngResource` module and in it the `$resource` service, that we'll soon use:
|
||||
|
||||
__`app/index.html`.__
|
||||
<pre>
|
||||
...
|
||||
<script src="js/services.js"></script>
|
||||
<script src="lib/angular/angular-resource.js"></script>
|
||||
...
|
||||
</pre>
|
||||
|
||||
|
|
@ -39,64 +39,68 @@ __`app/index.html`.__
|
|||
|
||||
__`app/js/services.js`.__
|
||||
<pre>
|
||||
angular.module.ng('Phone', function($resource) {
|
||||
angular.module('phonecatServices', ['ngResource']).
|
||||
factory('Phone', function($resource){
|
||||
return $resource('phones/:phoneId.json', {}, {
|
||||
query: {method: 'GET', params: {phoneId: 'phones'}, isArray: true}
|
||||
query: {method:'GET', params:{phoneId:'phones'}, isArray:true}
|
||||
});
|
||||
});
|
||||
});
|
||||
</pre>
|
||||
|
||||
We used the {@link api/angular.module.ng} 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.
|
||||
We used the module API to register a custom service using a factory function. We passed in the name
|
||||
of the service - 'Phone' - and the factory function. The factory function is similar to a
|
||||
controller's constructor in that both can declare dependencies via function arguments. The Phone
|
||||
service declared a dependency on the `$resource` service.
|
||||
|
||||
The {@link api/angular.module.ng.$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 {@link
|
||||
api/angular.module.ng.$xhr $xhr} service.
|
||||
The {@link api/angular.module.ngResource.$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 {@link
|
||||
api/angular.module.ng.$http $http} service.
|
||||
|
||||
|
||||
## Controller
|
||||
|
||||
We simplified our sub-controllers (`PhoneListCtrl` and `PhoneDetailCtrl`) by factoring out the
|
||||
lower-level {@link api/angular.module.ng.$xhr $xhr} service, replacing it with a new service called
|
||||
`Phone`. Angular's {@link api/angular.module.ng.$resource `$resource`} service is easier to use than
|
||||
{@link api/angular.module.ng.$xhr $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.
|
||||
lower-level {@link api/angular.module.ng.$http $http} service, replacing it with a new service called
|
||||
`Phone`. Angular's {@link api/angular.module.ngResource.$resource `$resource`} service is easier to
|
||||
use than `$http for interacting with data sources exposed as RESTful resources. It is also easier
|
||||
now to understand what the code in our controllers is doing.
|
||||
|
||||
__`app/js/controllers.js`.__
|
||||
<pre>
|
||||
...
|
||||
|
||||
function PhoneListCtrl(Phone) {
|
||||
this.orderProp = 'age';
|
||||
this.phones = Phone.query();
|
||||
function PhoneListCtrl($scope, Phone) {
|
||||
$scope.phones = Phone.query();
|
||||
$scope.orderProp = 'age';
|
||||
}
|
||||
//PhoneListCtrl.$inject = ['Phone'];
|
||||
|
||||
//PhoneListCtrl.$inject = ['$scope', 'Phone'];
|
||||
|
||||
|
||||
function PhoneDetailCtrl(Phone) {
|
||||
var self = this;
|
||||
|
||||
self.phone = Phone.get({phoneId: self.params.phoneId}, function(phone) {
|
||||
self.mainImageUrl = phone.images[0];
|
||||
function PhoneDetailCtrl($scope, $routeParams, Phone) {
|
||||
$scope.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) {
|
||||
$scope.mainImageUrl = phone.images[0];
|
||||
});
|
||||
|
||||
...
|
||||
$scope.setImage = function(imageUrl) {
|
||||
$scope.mainImageUrl = imageUrl;
|
||||
}
|
||||
}
|
||||
//PhoneDetailCtrl.$inject = ['Phone'];
|
||||
|
||||
//PhoneDetailCtrl.$inject = ['$scope', '$routeParams', 'Phone'];
|
||||
</pre>
|
||||
|
||||
Notice how in `PhoneListCtrl` we replaced:
|
||||
|
||||
$xhr('GET', 'phones/phones.json', function(code, response) {
|
||||
self.phones = response;
|
||||
$http.get('phones/phones.json').success(function(data) {
|
||||
$scope.phones = data;
|
||||
});
|
||||
|
||||
with:
|
||||
|
||||
this.phones = Phone.query();
|
||||
$scope.phones = Phone.query();
|
||||
|
||||
This is a simple statement that we want to query for all phones.
|
||||
|
||||
|
|
@ -118,10 +122,10 @@ We have modified our unit tests to verify that our new service is issuing HTTP r
|
|||
processing them as expected. The tests also check that our controllers are interacting with the
|
||||
service correctly.
|
||||
|
||||
The {@link api/angular.module.ng.$resource $resource} service augments the response object with
|
||||
methods for updating and deleting the resource. If we were to use the standard `toEqual` matcher,
|
||||
our tests would fail because the test values would not match the responses exactly. To solve the
|
||||
problem, we use a newly-defined `toEqualData` {@link
|
||||
The {@link api/angular.module.ngResource.$resource $resource} service augments the response object
|
||||
with methods for updating and deleting the resource. If we were to use the standard `toEqual`
|
||||
matcher, our tests would fail because the test values would not match the responses exactly. To
|
||||
solve the problem, we use a newly-defined `toEqualData` {@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.
|
||||
|
|
@ -131,7 +135,7 @@ __`test/unit/controllersSpec.js`:__
|
|||
<pre>
|
||||
describe('PhoneCat controllers', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(function(){
|
||||
this.addMatchers({
|
||||
toEqualData: function(expected) {
|
||||
return angular.equals(this.actual, expected);
|
||||
|
|
@ -139,54 +143,63 @@ describe('PhoneCat controllers', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('PhoneListCtrl', function() {
|
||||
var scope, $browser, ctrl;
|
||||
|
||||
beforeEach(function() {
|
||||
scope = angular.module.ng.$rootScope.Scope();
|
||||
$browser = scope.$service('$browser');
|
||||
beforeEach(module('phonecatServices'));
|
||||
|
||||
|
||||
describe('PhoneListCtrl', function(){
|
||||
var scope, ctrl, $httpBackend;
|
||||
|
||||
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/phones.json').
|
||||
respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
|
||||
|
||||
scope = $rootScope.$new();
|
||||
ctrl = $controller(PhoneListCtrl, {$scope: scope});
|
||||
}));
|
||||
|
||||
$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(scope.phones).toEqual([]);
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(ctrl.phones).toEqualData([{name: 'Nexus S'},
|
||||
{name: 'Motorola DROID'}]);
|
||||
expect(scope.phones).toEqualData(
|
||||
[{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
|
||||
});
|
||||
|
||||
|
||||
it('should set the default value of orderProp model', function() {
|
||||
expect(ctrl.orderProp).toBe('age');
|
||||
expect(scope.orderProp).toBe('age');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('PhoneDetailCtrl', function() {
|
||||
var scope, $browser, ctrl;
|
||||
describe('PhoneDetailCtrl', function(){
|
||||
var scope, $httpBackend, ctrl,
|
||||
xyzPhoneData = function() {
|
||||
return {
|
||||
name: 'phone xyz',
|
||||
images: ['image/url1.png', 'image/url2.png']
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
scope = angular.module.ng.$rootScope.Scope();
|
||||
$browser = scope.$service('$browser');
|
||||
});
|
||||
|
||||
beforeEach(function() {
|
||||
scope = angular.module.ng.$rootScope.Scope();
|
||||
$browser = scope.$service('$browser');
|
||||
});
|
||||
beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData());
|
||||
|
||||
$routeParams.phoneId = 'xyz';
|
||||
scope = $rootScope.$new();
|
||||
ctrl = $controller(PhoneDetailCtrl, {$scope: scope});
|
||||
}));
|
||||
|
||||
|
||||
it('should fetch phone detail', function() {
|
||||
scope.params = {phoneId:'xyz'};
|
||||
$browser.xhr.expectGET('phones/xyz.json').respond({name:'phone xyz'});
|
||||
ctrl = scope.$new(PhoneDetailCtrl);
|
||||
expect(scope.phone).toEqualData({});
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(ctrl.phone).toEqualData({});
|
||||
$browser.xhr.flush();
|
||||
|
||||
expect(ctrl.phone).toEqualData({name:'phone xyz'});
|
||||
expect(scope.phone).toEqualData(xyzPhoneData());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -198,7 +211,7 @@ 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)
|
||||
Chrome 19.0.1084.36 Mac OS: Run 4 tests (Passed: 4; Fails: 0; Errors 0) (3.00 ms)
|
||||
|
||||
|
||||
# Summary
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
@name Tutorial: The End
|
||||
@description
|
||||
|
||||
<h2 style="color: red">This page has not been updated for AngularJS 1.0 yet</h2>
|
||||
|
||||
Our application is now complete. Feel free to experiment with the code further, and jump back to
|
||||
previous steps using the `git checkout` or `goto_step.sh` commands.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue