Tutorial: 5 - XHRs & Dependency Injection

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 services called $xhr. We will use angular's dependency injection (DI) to provide the service to the PhoneListCtrl controller.

You should now see a list of 20 phones.

The most important changes are listed below. You can see the full diff on 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:

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

Controller

We'll use angular's $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 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 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:

function PhoneListCtrl($xhr) {
  var self = this;


  $xhr('GET', 'phones/phones.json', function(code, response) {
    self.phones = response;
  });


  self.orderProp = 'age';
}


//PhoneListCtrl.$inject = ['$xhr'];

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

'$' 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 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:

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);
    });
  });

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

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:

Now, we will make assertions to verify that the phones model doesn't exist on the scope, before the response is received:

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

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

    it('should set the default value of orderProp model', function() {
      expect(ctrl.orderProp).toBe('age');
    });
  });
});

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

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.