mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-05-01 06:12:31 +08:00
add new batch of tutorial docs and images
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user