angular/angular.js

View on GitHub
docs/content/tutorial/step_07.ngdoc

Summary

Maintainability
Test Coverage
@ngdoc tutorial
@name 7 - XHR & Dependency Injection
@step 7
@description

<ul doc-tutorial-nav="7"></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 AngularJS's built-in {@link guide/services services} called
{@link ng.$http $http}. We will use AngularJS's {@link guide/di dependency injection (DI)} to
provide the service to the `phoneList` component's controller.

* There is now a list of 20 phones, loaded from the server.


<div doc-tutorial-reset="7"></div>


## Data

The `app/phones/phones.json` file in our project is a dataset that contains a larger list of phones,
stored in JSON format.

Following is a sample of the file:

```json
[
  {
    "age": 13,
    "id": "motorola-defy-with-motoblur",
    "name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
    "snippet": "Are you ready for everything life throws your way?"
    ...
  },
  ...
]
```


## Component Controller

We will use AngularJS's {@link ng.$http $http} service in our controller for making an HTTP request to
our web server to fetch the data in the `app/phones/phones.json` file. `$http` is just one of
several built-in {@link guide/services AngularJS services} that handle common operations in web
applications. AngularJS injects these services for you, right where you need them.

Services are managed by AngularJS's {@link guide/di DI subsystem}. Dependency injection helps to make
your web applications both well-structured (e.g. separate entities for presentation, data, and
control) and loosely coupled (dependencies between entities are not resolved by the entities
themselves, but by the DI subsystem). As a result, applications are easier to test as well.

<br />
**`app/phone-list/phone-list.component.js:`**

```js
  angular.
    module('phoneList').
    component('phoneList', {
      templateUrl: 'phone-list/phone-list.template.html',
      controller: function PhoneListController($http) {
        var self = this;
        self.orderProp = 'age';

        $http.get('phones/phones.json').then(function(response) {
          self.phones = response.data;
        });
      }
    });
```

`$http` makes an HTTP GET request to our web server, asking for `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 will use JSON files
in this tutorial.)

The `$http` service returns a {@link ng.$q promise object}, which has a `then()` method. We call
this method to handle the asynchronous response and assign the phone data to the controller, as a
property called `phones`. Notice that AngularJS detected the JSON response and parsed it for us into
the `data` property of the `response` object passed to our callback!

Since we are making the assignment of the `phones` property in a callback function, where the `this`
value is not defined, we also introduce a local variable called `self` that points back to the
controller instance.

To use a service in AngularJS, you simply declare the names of the dependencies you need as arguments
to the controller's constructor function, as follows:

```js
function PhoneListController($http) {...}
```

AngularJS'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/tutorial_05.png">


### `$`-prefix Naming Convention

You can create your own services, and in fact we will do exactly that a few steps down the road. As
a naming convention, AngularJS's built-in services, Scope methods and a few other AngularJS APIs have a
`$` prefix in front of the name.

The `$` prefix is there to namespace AngularJS-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 AngularJS infers the controller's dependencies from the names of arguments to the controller's
constructor function, if you were to [minify][minification] the JavaScript code for the
`PhoneListController` controller, all of its function arguments would be minified as well, and the
dependency injector would not be able to identify services correctly.

We can overcome this problem by annotating the function with the names of the dependencies, provided
as strings, which will not get minified. There are two ways to provide these injection annotations:

* Create an `$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 our example, we would write:

  ```js
      function PhoneListController($http) {...}
      PhoneListController.$inject = ['$http'];
      ...
      .component('phoneList', {..., controller: PhoneListController});
  ```

* Use an inline annotation where, instead of just providing the function, you provide an array.
  This array contains a list of the service names, followed by the function itself as the last item
  of the array.

  ```js
      function PhoneListController($http) {...}
      ...
      .component('phoneList', {..., controller: ['$http', PhoneListController]});
  ```

Both of these methods work with any function that can be injected by AngularJS, 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, when
registering the controller:

```js
.component('phoneList', {..., controller: ['$http', function PhoneListController($http) {...}]});
```

From this point onwards, we are going to use the inline method in the tutorial. With that in mind,
let's add the annotations to our `PhoneListController`:

<br />
**`app/phone-list/phone-list.component.js`**

```js
  angular.
    module('phoneList').
    component('phoneList', {
      templateUrl: 'phone-list/phone-list.template.html',
      controller: ['$http',
        function PhoneListController($http) {
          var self = this;
          self.orderProp = 'age';

          $http.get('phones/phones.json').then(function(response) {
            self.phones = response.data;
          });
        }
      ]
    });
```


## Testing

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, AngularJS provides a mock `$http`
service that we can use in unit tests.  We configure "fake" responses to server requests by calling
methods on a service called `$httpBackend`:

<br />
**`app/phone-list/phone-list.component.spec.js`:**

```js
describe('phoneList', function() {

  beforeEach(module('phoneList'));

  describe('controller', function() {
    var $httpBackend, ctrl;

    // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_).
    // This allows us to inject a service and assign it to a variable with the same name
    // as the service while avoiding a name conflict.
    beforeEach(inject(function($componentController, _$httpBackend_) {
      $httpBackend = _$httpBackend_;
      $httpBackend.expectGET('phones/phones.json')
                  .respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);

      ctrl = $componentController('phoneList');
    }));

    ...

  });

});
```

<div class="alert alert-info">
  **Note:** Because we loaded Jasmine and `angular-mocks.js` in our test environment, we got two
  helper methods {@link angular.mock.module module} and {@link angular.mock.inject inject} that we
  can use to access and configure the injector.
</div>

We created the controller in the test environment, as follows:

* We used the `inject()` helper method to inject instances of
  {@link ngMock.$componentController $componentController} and {@link ng.$httpBackend $httpBackend}
  services into 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 the injected `$componentController` function passing the name of the `phoneList`
  component (whose controller we wanted to instantiate) as a parameter.

Because our code now uses the `$http` service to fetch the phone list data in our controller, before
we create the `PhoneListController`, we need to tell the testing harness to expect an incoming
request from the controller. To do this we:

* Inject the `$httpBackend` service into the `beforeEach()` function. This is a
  {@link ngMock.$httpBackend mock version} of the service that in a production environment
  facilitates all XHR and JSONP requests. The mock version of this service allows us to write tests
  without having to deal with native APIs and the global state associated with them — both of which
  make testing a nightmare. It also overcomes the asynchronous nature of these calls, which would
  slow down unit tests.

* 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` property doesn't exist on the controller
before the response is received:

```js
    it('should create a `phones` property with 2 phones fetched with `$http`', function() {
      expect(ctrl.phones).toBeUndefined();

      $httpBackend.flush();
      expect(ctrl.phones).toEqual([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
    });
```

* 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. See
  {@link ngMock.$httpBackend#flushing-http-requests Flushing HTTP requests} in the mock
  `$httpBackend` documentation for a full explanation of why this is necessary.

* We make the assertions, verifying that the `phones` property now exists on the controller.

Finally, we verify that the default value of `orderProp` is set correctly:

```js
    it('should set a default value for the `orderProp` property', function() {
      expect(ctrl.orderProp).toBe('age');
    });
```

You should now see the following output in the Karma tab:

```
Chrome 49.0: Executed 2 of 2 SUCCESS (0.133 secs / 0.097 secs)
```


## Experiments

<div></div>

* At the bottom of `phone-list.template.html`, add a
  `<pre>{{$ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp | json}}</pre>` binding to see
  the list of phones displayed in JSON format.

* In the `PhoneListController` 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:

  ```js
  self.phones = response.data.slice(0, 5);
  ```


## Summary

Now that you have learned how easy it is to use AngularJS services (thanks to AngularJS's dependency
injection), go to {@link step_08 step 8}, where you will add some thumbnail images of phones and
some links.


<ul doc-tutorial-nav="7"></ul>


[minification]: https://en.wikipedia.org/wiki/Minification_(programming)