Improve this Doc

In this step, we will change the way our application fetches data.

Dependencies

The RESTful functionality is provided by AngularJS in the ngResource module, which is distributed separately from the core AngularJS framework.

Since we are using npm to install client-side dependencies, this step updates the package.json configuration file to include the new dependency:


package.json:

{
  "name": "angular-phonecat",
  ...
  "dependencies": {
    "angular": "1.8.x",
    "angular-resource": "1.8.x",
    "angular-route": "1.8.x",
    "bootstrap": "3.3.x"
  },
  ...
}

The new dependency "angular-resource": "1.8.x" tells npm to install a version of the angular-resource module that is compatible with version 1.8.x of AngularJS. We must tell npm to download and install this dependency.

npm install

Service

We create our own service to provide access to the phone data on the server. We will put the service in its own module, under core, so we can explicitly declare its dependency on ngResource:


app/core/phone/phone.module.js:

angular.module('core.phone', ['ngResource']);


app/core/phone/phone.service.js:

angular.
  module('core.phone').
  factory('Phone', ['$resource',
    function($resource) {
      return $resource('phones/:phoneId.json', {}, {
        query: {
          method: 'GET',
          params: {phoneId: 'phones'},
          isArray: true
        }
      });
    }
  ]);

We used the module API to register a custom service using a factory function. We passed in the name of the service — 'Phone' — and the factory function. The factory function is similar to a controller's constructor in that both can declare dependencies to be injected via function arguments. The Phone service declares a dependency on the $resource service, provided by the ngResource module.

The $resource service makes it easy to create a RESTful client with just a few lines of code. This client can then be used in our application, instead of the lower-level $http service.


app/core/core.module.js:

angular.module('core', ['core.phone']);

We need to add the core.phone module as a dependency of the core module.

Template

Our custom resource service will be defined in app/core/phone/phone.service.js, so we need to include this file and the associated .module.js file in our layout template. Additionally, we also need to load the angular-resource.js file, which contains the ngResource module:


app/index.html:

<head>
  ...
  <script src="lib/angular-resource/angular-resource.js"></script>
  ...
  <script src="core/phone/phone.module.js"></script>
  <script src="core/phone/phone.service.js"></script>
  ...
</head>

Component Controllers

We can now simplify our component controllers (PhoneListController and PhoneDetailController) by factoring out the lower-level $http service, replacing it with the new Phone service. AngularJS's $resource service is easier to use than $http for interacting with data sources exposed as RESTful resources. It is also easier now to understand what the code in our controllers is doing.


app/phone-list/phone-list.module.js:

angular.module('phoneList', ['core.phone']);


app/phone-list/phone-list.component.js:

angular.
  module('phoneList').
  component('phoneList', {
    templateUrl: 'phone-list/phone-list.template.html',
    controller: ['Phone',
      function PhoneListController(Phone) {
        this.phones = Phone.query();
        this.orderProp = 'age';
      }
    ]
  });


app/phone-detail/phone-detail.module.js:

angular.module('phoneDetail', [
  'ngRoute',
  'core.phone'
]);


app/phone-detail/phone-detail.component.js:

angular.
  module('phoneDetail').
  component('phoneDetail', {
    templateUrl: 'phone-detail/phone-detail.template.html',
    controller: ['$routeParams', 'Phone',
      function PhoneDetailController($routeParams, Phone) {
        var self = this;
        self.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) {
          self.setImage(phone.images[0]);
        });

        self.setImage = function setImage(imageUrl) {
          self.mainImageUrl = imageUrl;
        };
      }
    ]
  });

Notice how in PhoneListController we replaced:

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

with just:

this.phones = Phone.query();

This is a simple and declarative statement that we want to query for all phones.

An important thing to notice in the code above is that we don't pass any callback functions, when invoking methods of our Phone service. Although it looks as if the results were returned synchronously, that is not the case at all. What is returned synchronously is a "future" — an object, which will be filled with data, when the XHR response is received. Because of the data-binding in AngularJS, we can use this future and bind it to our template. Then, when the data arrives, the view will be updated automatically.

Sometimes, relying on the future object and data-binding alone is not sufficient to do everything we require, so in these cases, we can add a callback to process the server response. The phoneDetail component's controller illustrates this by setting the mainImageUrl in a callback.

Testing

Because we are now using the ngResource module, it is necessary to update the Karma configuration file with angular-resource.


karma.conf.js:

files: [
  'lib/angular/angular.js',
  'lib/angular-resource/angular-resource.js',
  ...
],

We have added a unit test to verify that our new service is issuing HTTP requests and returns the expected "future" objects/arrays.

The $resource service augments the response object with extra methods — e.g. for updating and deleting the resource — and properties (some of which are only meant to be accessed by AngularJS). If we were to use Jasmine's standard .toEqual() matcher, our tests would fail, because the test values would not match the responses exactly.

To solve the problem, we instruct Jasmine to use a custom equality tester for comparing objects. We specify angular.equals as our equality tester, which ignores functions and $-prefixed properties, such as those added by the $resource service.
(Remember that AngularJS uses the $ prefix for its proprietary API.)


app/core/phone/phone.service.spec.js:

describe('Phone', function() {
  ...
  var phonesData = [...];

  // Add a custom equality tester before each test
  beforeEach(function() {
    jasmine.addCustomEqualityTester(angular.equals);
  });

  // Load the module that contains the `Phone` service before each test
  ...

  // Instantiate the service and "train" `$httpBackend` before each test
  ...

  // Verify that there are no outstanding expectations or requests after each test
  afterEach(function () {
    $httpBackend.verifyNoOutstandingExpectation();
    $httpBackend.verifyNoOutstandingRequest();
  });

  it('should fetch the phones data from `/phones/phones.json`', function() {
    var phones = Phone.query();

    expect(phones).toEqual([]);

    $httpBackend.flush();
    expect(phones).toEqual(phonesData);
  });

});

Here we are using $httpBackend's verifyNoOutstandingExpectation() and verifyNoOutstandingRequest() methods to verify that all expected requests have been sent and that no extra request is scheduled for later.

Note that we have also modified our component tests to use the custom matcher when appropriate.

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

Chrome 49.0: Executed 5 of 5 SUCCESS (0.123 secs / 0.104 secs)

Summary

Now that we have seen how to build a custom service as a RESTful client, we are ready for step 14 to learn how to enhance the user experience with animations.