mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-16 23:30:23 +00:00
Test snippet was missing this necessary statement that was present in the sample code. Closes #5743
271 lines
11 KiB
Text
271 lines
11 KiB
Text
@ngdoc overview
|
|
@name Tutorial: 5 - XHRs & Dependency Injection
|
|
@description
|
|
|
|
<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 guide/dev_guide.services services} called {@link
|
|
api/ng.$http $http}. We will use Angular's {@link guide/di dependency
|
|
injection (DI)} to provide the service to the `PhoneListCtrl` controller.
|
|
|
|
|
|
<div doc-tutorial-reset="5"></div>
|
|
|
|
|
|
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/phones.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>
|
|
[
|
|
{
|
|
"age": 13,
|
|
"id": "motorola-defy-with-motoblur",
|
|
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
|
|
"snippet": "Are you ready for everything life throws your way?"
|
|
...
|
|
},
|
|
...
|
|
]
|
|
</pre>
|
|
|
|
|
|
## Controller
|
|
|
|
We'll use Angular's {@link api/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 guide/dev_guide.services 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>
|
|
var phonecatApp = angular.module('phonecatApp', []);
|
|
|
|
phonecatApp.controller('PhoneListCtrl', function ($scope, $http) {
|
|
$http.get('phones/phones.json').success(function(data) {
|
|
$scope.phones = data;
|
|
});
|
|
|
|
$scope.orderProp = 'age';
|
|
});
|
|
</pre>
|
|
|
|
`$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 `$http` service returns a {@link api/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 dependencies you need as arguments
|
|
to the controller's constructor function, as follows:
|
|
|
|
phonecatApp.controller('PhoneListCtrl', function ($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 class="diagram" 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.
|
|
|
|
The `$` prefix is there to namespace Angular-provided services.
|
|
To prevent collisions it's best to avoid naming your services and models anything that begins with a `$`.
|
|
|
|
If you inspect a Scope, you may also notice some properties that begin with `$$`. These
|
|
properties are considered private, and should not be accessed or modified.
|
|
|
|
|
|
### 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 be able to identify services correctly.
|
|
|
|
There are two ways to overcome issues caused by minification:
|
|
|
|
* You can create a `$inject` property on the controller function which holds an array of strings.
|
|
Each string in the array is the name of the service to inject for the corresponding parameter.
|
|
In the case of our example we would write:
|
|
<pre>
|
|
function PhoneListCtrl($scope, $http) {...}
|
|
PhoneListCtrl.$inject = ['$scope', '$http'];
|
|
phonecatApp.controller('PhoneListCtrl', PhoneListCtrl);
|
|
</pre>
|
|
* Use the inline 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:
|
|
<pre>
|
|
function PhoneListCtrl($scope, $http) {...}
|
|
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', PhoneListCtrl]);
|
|
</pre>
|
|
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.
|
|
|
|
When using the second method, it is common to provide the constructor function inline as an
|
|
anonymous function when registering the controller:
|
|
<pre>
|
|
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', function($scope, $http) {...}]);
|
|
</pre>
|
|
|
|
From this point onward, we're going to use the inline method in the tutorial. With that in mind,
|
|
let's add the annotations to our `PhoneListCtrl`:
|
|
|
|
__`app/js/controllers.js:`__
|
|
<pre>
|
|
var phonecatApp = angular.module('phonecatApp', []);
|
|
|
|
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http',
|
|
function ($scope, $http) {
|
|
$http.get('phones/phones.json').success(function(data) {
|
|
$scope.phones = data;
|
|
});
|
|
|
|
$scope.orderProp = 'age';
|
|
}]);
|
|
</pre>
|
|
|
|
## 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 `$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, ctrl, $httpBackend;
|
|
|
|
// Load our app module definition before each test.
|
|
beforeEach(module('phonecatApp'));
|
|
|
|
// The injector ignores leading and trailing underscores here (i.e. _$httpBackend_).
|
|
// This allows us to inject a service but then attach it to a variable
|
|
// with the same name as the service.
|
|
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});
|
|
}));
|
|
</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 used the `inject` helper method to inject instances of
|
|
{@link api/ng.$rootScope $rootScope},
|
|
{@link api/ng.$controller $controller} and
|
|
{@link api/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 created a new scope for our controller by calling `$rootScope.$new()`
|
|
|
|
* We called the injected `$controller` function passing the name of the `PhoneListCtrl` controller
|
|
and the created scope as parameters.
|
|
|
|
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:
|
|
|
|
* Request `$httpBackend` service to be injected into our `beforeEach` function. This is a mock
|
|
version of the service that in a 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 `$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 `scope` before
|
|
the response is received:
|
|
|
|
<pre>
|
|
it('should create "phones" model with 2 phones fetched from xhr', function() {
|
|
expect(scope.phones).toBeUndefined();
|
|
$httpBackend.flush();
|
|
|
|
expect(scope.phones).toEqual([{name: 'Nexus S'},
|
|
{name: 'Motorola DROID'}]);
|
|
});
|
|
</pre>
|
|
|
|
* 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.
|
|
|
|
Finally, we verify that the default value of `orderProp` is set correctly:
|
|
|
|
<pre>
|
|
it('should set the default value of orderProp model', function() {
|
|
expect(scope.orderProp).toBe('age');
|
|
});
|
|
</pre>
|
|
|
|
You should now see the following output in the Karma tab:
|
|
|
|
Chrome 22.0: Executed 2 of 2 SUCCESS (0.028 secs / 0.007 secs)
|
|
|
|
|
|
|
|
# Experiments
|
|
|
|
* 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 http response by limiting the number of phones
|
|
to the first 5 in the list. Use the following code in the `$http` callback:
|
|
|
|
$scope.phones = data.splice(0, 5);
|
|
|
|
|
|
# Summary
|
|
|
|
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.
|
|
|
|
|
|
<ul doc-tutorial-nav="5"></ul>
|