In this step, we will improve the way our APP gets data.
Please reset the working directory:
git checkout -f step-11
The last improvement to our application is to define a custom service that represents the RESTful client. With this client, we can send XHR requests in an easier way without caring about the underlying $http service (API, HTTP methods, and URLs).
The most important differences between Step 9 and Step 10 are listed below. You can see the complete difference in GitHub.
template
Customized services are defined in app/js/services, so we need to introduce this file in the layout template. In addition, we also need to load the angularjs-resource.js file, which contains the ngResource module and the $resource service in it. We will use them later:
app/index.html
... <script src="js/services.js"></script> <script src="lib/angular/angular-resource.js"></script>...
Serve
app/js/services.js
angular.module('phonecatServices', ['ngResource']). factory('Phone', function($resource){ return $resource('phones/:phoneId.json', {}, { query: {method:'GET', params:{phoneId:'phones'}, isArray:true} }); });We use the module API to register a custom service through a factory method. We pass in the service name Phone and factory functions. Factory functions and controller constructors are similar, and they both declare dependency services through function parameters. The Phone service declares that it depends on the $resource service.
The $resource service allows you to create a RESTful client in just a few lines of code. Our application uses this client to replace the underlying $http service.
app/js/app.js
...angular.module('phonecat', ['phonecatFilters', 'phonecatServices'])...We need to add phonecatServices to the phonecat dependency array.
Controller
By refactoring the underlying $http service and putting it in a new service Phone, we can greatly simplify the subcontrollers (PhoneListCtrl and PhoneDetailCtrl). AngularJS's $resource is more suitable for interacting with RESTful data sources than $http. And now it is easier for us to understand what the controller code is doing.
app/js/controllers.js
...function PhoneListCtrl($scope, Phone) { $scope.phones = Phone.query(); $scope.orderProp = 'age';}//PhoneListCtrl.$inject = ['$scope', 'Phone'];function PhoneDetailCtrl($scope, $routeParams, Phone) { $scope.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) { $scope.mainImageUrl = phone.images[0]; }); $scope.setImage = function(imageUrl) { $scope.mainImageUrl = imageUrl; }}//PhoneDetailCtrl.$inject = ['$scope', '$routeParams', 'Phone'];Note that in PhoneListCtrl we put:
$http.get('phones/phones.json').success(function(data) { $scope.phones = data;});Change to:
$scope.phones = Phone.query();
We use this simple statement to query all mobile phones.
Another thing that needs to be noted is that in the above code, when calling the Phone service method is that we do not pass any callback functions. Although this seems to be returned synchronously, it is not at all. What is returned synchronously is a "future" - an object, which will be filled with data when XHR returns accordingly. Given AngularJS's data binding, we can use future and bind it to our template. Our view will then automatically update when the data arrives.
Sometimes, relying solely on future objects and data binding is not enough to meet our needs, so in these cases, we need to add a callback function to handle the server's response. The PhoneDetailCtrl controller is an explanation by setting mainImageUrl in a callback function.
test
Modify our unit tests to verify that our new service initiates HTTP requests and process them as expected. The test also checks whether our controllers work correctly with the service.
The $resource service enhances the object obtained by adding updates and deleting resources. If we intend to use the toEqual matcher, our test will fail because the test value will not be exactly equivalent to the response. To solve this problem, we need to use a recently defined toEqualDataJasmine matcher. When the toEqualData matcher compares two objects, it only considers the object's properties and ignores all methods.
test/unit/controllersSpec.js:
describe('PhoneCat controllers', function() { beforeEach(function(){ this.addMatchers({ toEqualData: function(expected) { return angular.equals(this.actual, expected); } }); }); 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).toEqual([]); $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()); }); }); });Execute ./scripts/test.sh to run the test, and you should see the following output:
Chrome: Runner reset......Total 4 tests (Passed: 4; Fails: 0; Errors: 0) (3.00 ms) Chrome 19.0.1084.36 Mac OS: Run 4 tests (Passed: 4; Fails: 0; Errors 0) (3.00 ms)
Summarize
Completed! You have created a web application in a pretty short time. In the final chapter, we will mention what we should do next.
The above is the information sorting out AngularJS RES and customized services. We will continue to add relevant information in the future. I hope it can help everyone learn AngularJS!