Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

AngularJS REST and custom services


May 08, 2021 AngularJS


Table of contents


REST and custom services

In this step, you will change the way we get data.

  • We defined a custom service that represents a RESTful client. W ith this client, we can make an easier way to request data from the server without having to deal with the underlying? http API, HTTP method, and URL.

Reset the workspace to step 11

git checkout -f step-11

Refresh your browser or check this step online: Step 8 Live Demo

The most important differences between steps 10 and 11 are listed below. You can see the full difference on GitHub.

Dependence

Angular provides ngResource module, which is distributed separately from the core Angler framework.

We are using Bower to install client dependencies. This step updates bower.json profile to include new dependencies:

{
  "name": "angular-seed",
  "description": "A starter project for AngularJS",
  "version": "0.0.0",
  "homepage": "https://github.com/angular/angular-seed",
  "license": "MIT",
  "private": true,
  "dependencies": {
    "angular": "1.4.x",
    "angular-mocks": "1.4.x",
    "jquery": "~2.1.1",
    "bootstrap": "~3.1.1",
    "angular-route": "1.4.x",
    "angular-resource": "1.4.x"
  }
}

New "angular-resource": "1.4.x" tells bower to install a version of the angular-sourced component that is compatible with v1.4x. W e must ask bower to download and install this dependency. We can do this by running the following instructions:

npm install
Warning: If Angular has released a new version since you last ran 'npm install', you may be having trouble with 'bower install' because the version of the .js you installed conflicts with it. If you want to pass it, you'll need to delete your 'app/bower_components' folder before running 'npm install'.
Note: If you already have bower installed globally, you can run 'bower install', but for the project we have preconfigured, 'npm install' runs bower for us.

Template

Our custom source service will be defined app/js/services.js so we need to include this file in our layout template. In addition, we need to load angular-resouces.js file, which contains the ngResource module:

app/index.html.

...
  <script src="/attachments/image/wk/angularjs/angular-resource.js"></script>
  <script src="/attachments/image/wk/angularjs/services.js"></script>
...

Service

We created our own service to provide access to mobile phone data on the server:

app/js/services.js.

var phonecatServices = angular.module('phonecatServices', ['ngResource']);

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

We use the module API to register custom services using factory functions. T he name "Phone" and factory function of our incoming service. T he structure of the factory function is similar to that of the controller, and both can declare dependencies to be injected through function parameters. The Phone $resource a dependency on the service.

$resource makes it easier to create a RESTful client with just a few lines of code. This client can be used in our application instead of the underlying $http service.

app/js/app.js.

...
angular.module('phonecatApp', ['ngRoute', 'phonecatControllers','phonecatFilters', 'phonecatServices']).
...

We need to phonecatServices module dependency to the number of needs column of the phonecatApp module.

Controller

By refactoring the underlying $http service, we simplified our PhoneListCtrl PhoneDetailCtrl Phone A ngular's $resource $http to use than other sources and interacts with data sources that are available externally as REST sources. Now it's easier to understand what the code in the controller does.

app/js/controllers.js.

var phonecatControllers = angular.module('phonecatControllers', []);

...

phonecatControllers.controller('PhoneListCtrl', ['$scope', 'Phone', function($scope, Phone) {
  $scope.phones = Phone.query();
  $scope.orderProp = 'age';
}]);

phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', 'Phone', function($scope, $routeParams, Phone) {
  $scope.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) {
    $scope.mainImageUrl = phone.images[0];
  });

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

Notice what PhoneList

$http.get('phones/phones.json').success(function(data) {
  $scope.phones = data;
});

Into:

$scope.phones = Phone.query();

We use this simple statement to query all phones.

One important thing to note is that in the code above, we didn't pass any callback functions when referring to the methods of the phone service. A lthough it looks like the result is returned synchronously, it is not at all. S ynchronization returns a "future" -- an object that fills in data when the XHR response returns. B ecause of the data binding in Angular, we can use this future and bind it to our template. The view is then automatically updated when the data arrives.

Sometimes, future objects and data binding alone are not enough to meet all our needs, in which case we can add a callback function to handle the server response. PhoneDetailCtrl controller demonstrates this by setting mainImageUrl callback function.

Test

Because we now use the ngResource module, it is necessary to update karma configuration documents for the source so that the new tests can pass.

test/karma.conf.js:

    files : [
      'app/bower_components/angular/angular.js',
      'app/bower_components/angular-route/angular-route.js',
      'app/bower_components/angular-resource/angular-resource.js',
      'app/bower_components/angular-mocks/angular-mocks.js',
      'app/js/**/*.js',
      'test/unit/**/*.js'
    ],

We have modified our unit tests to verify that our new service will initiate HTTP requests and process them as expected. The test also checks that our controller interacts with the service correctly.

$resource service counsellors with response objects with methods for updating and deleting sources. I f we intend to use toEqual matcher, our test will fail because the test values cannot strictly match the response. T o solve this problem, we used a newly defined toEqualData (Jasmine matcher) ( jasmine matcher) . When toEqualData compares two objects, it considers the object property properties and ignores the object method.

test/unit/controllersSpec.js:

describe('PhoneCat controllers', function() {

  beforeEach(function(){
    this.addMatchers({
      toEqualData: function(expected) {
        return angular.equals(this.actual, expected);
      }
    });
  });

  beforeEach(module('phonecatApp'));
  beforeEach(module('phonecatServices'));

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

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

    it('should create "phones" model with 2 phones fetched from xhr', function() {
      expect(scope.phones).toEqualData([]);
      $httpBackend.flush();

      expect(scope.phones).toEqualData(
          [{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
    });

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

  describe('PhoneDetailCtrl', function(){
    var scope, $httpBackend, ctrl,
        xyzPhoneData = function() {
          return {
            name: 'phone xyz',
            images: ['image/url1.png', 'image/url2.png']
          }
        };

    beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) {
      $httpBackend = _$httpBackend_;
      $httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData());

      $routeParams.phoneId = 'xyz';
      scope = $rootScope.$new();
      ctrl = $controller('PhoneDetailCtrl', {$scope: scope});
    }));

    it('should fetch phone detail', function() {
      expect(scope.phone).toEqualData({});
      $httpBackend.flush();

      expect(scope.phone).toEqualData(xyzPhoneData());
    });
  });
});

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

Chrome 22.0: Executed 5 of 5 SUCCESS (0.038 secs / 0.01 secs)

Summarize

Now that we've seen how to build a custom service, as rest clients, we're ready to go to Step 12 App Animation (the last step) to learn how to improve your application with animation.