aboutsummaryrefslogtreecommitdiffstats
path: root/docs/content/tutorial/step_05.ngdoc
blob: 1aac51f7e4bbef0f2c67afad3079df4707b8d4c4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
@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 api/angular.service services} called {@link
api/angular.service.$xhr $xhr}. We will use angular's {@link guide/dev_guide.di dependency
injection (DI)} to provide the service to the `PhoneListCtrl` controller.


<doc:tutorial-instructions step="5"></doc:tutorial-instructions>


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.

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/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 api/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/dev_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>

`$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.)

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!

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) {...}

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 api/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.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.

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');
    });
  });
});
</pre>

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:

         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.


<ul doc:tutorial-nav="5"></ul>