May 08, 2021 AngularJS
In this step, you will change the way we get data.
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.
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
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>
...
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.
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.
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)
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.