When defining a service in Angular, there are two different modules provided for you. They are the factory module and the service module. They are fairly similar, but in the end I chose to use the factory module because 99% of the examples on the Angular site are done using it instead of the service module. For some more info on the differences on these two modules you can check out this SO post.
Factory
Expanding on our existing code that we worked on in part 2, we are going to update it so the code retrieves the data from a factory instead of just hard coding the values in the controller. Since I don't actually have a service for this, I am going to simulate the async service call with a window.settimeout. Adding a factory module that does this will give you the following code:var service = angular.module('solarSystemApp.service', []);var factory= service.factory('objectsService', function(){
var objectService = {
retrieveObjects: function(callback){
window.setTimeout(function callback(){
callback({Name: 'Sun', Type: 'Star',
Children: [{Name: 'Mercury', Type: 'Planet', Orbit: 88}, {Name: 'Venus', Type: 'Planet' , Orbit: 244 },
{Name: 'Earth', Type: 'Planet' , Orbit: 365}, {Name: 'Mars', Type: 'Planet' , Orbit: 687},
{Name: 'Jupiter', Type: 'Planet', Orbit: 4332}, {Name:'Saturn', Type: 'Planet', Orbit: 10833},
{Name: 'Uranus', Type: 'Planet', Orbit: 30800}, {Name: 'Neptune', Type: 'Planet', Orbit: 60190},
{Name: 'Pluto', Type: 'Dwarf Planet', Orbit: 90520}]}));
});
}, 1000);
}
};
return objectService
});
In the code we are defining a factory module called 'objectsService' on the 'solarSystemApp.service' module we had already defined earlier. The factory takes a function as a parameter that returns the object with the methods you want to call from your controller. The purpose of a service factory function is to generate a single object or function that represents the service to the rest of the application. That object or function will then be passed as a parameter to any other factory function which specifies a dependency on this service.
An important thing to note is that Angular factory functions are executed lazily. That is, they are only executed when needed to satisfy a dependency, and are then executed exactly once for each service. Everything which is dependent on this service gets a reference to the single instance generated by the service factory
So we changed the controller function to now take in two parameters. The first one is still the $scope object, and the second one is now newly created objectsService. The main body of our controller now calls the retrieveObjects method and gives it a callback where we set the values to the parentObject property on the $scope. The other thing to note in that block is we have a $scope.$apply() call wrapped around our block. Why do we have that? The simple reason is we need to have a way to tell Angular to update the UI bindings. Other libraries such as Knockout.js use observable objects that you need to wrap around all of your objects and arrays you want to have 2 way binding. Since you don't have to do that in Angluar, and can use basic JS objects you just need to wrap your code in an $apply() call to notify the UI. A lot of the built in async calls such as $http should automatically wrap the return blocks in an $apply statement, but I haven't used it yet.
The last statement actually does the injection, and passes the objects in based on the order of the array. There is also another way of implicitly injecting the classes without using $inject by naming your parameters the same as your modules. However if you plan on minifying your code, you shouldn't use this method and stick with $inject.
To implement promises we are going to change the code in the following to look like this:
Here is an updated JSFiddle with the entire sample above.
An important thing to note is that Angular factory functions are executed lazily. That is, they are only executed when needed to satisfy a dependency, and are then executed exactly once for each service. Everything which is dependent on this service gets a reference to the single instance generated by the service factory
Injection
Dependency injection is useful for many reasons. In this example we are going to use injection to inject our factory service to the controller so we can call it.var controller = app.controller('orbitController', function orbitController($scope, objectsService){objectsService.retrieveObjects(function retrieveObjectsResult(values){$scope.$apply(function(){$scope.parentObject = values;});});});controller.$inject = ['$scope', 'objectsService'];
So we changed the controller function to now take in two parameters. The first one is still the $scope object, and the second one is now newly created objectsService. The main body of our controller now calls the retrieveObjects method and gives it a callback where we set the values to the parentObject property on the $scope. The other thing to note in that block is we have a $scope.$apply() call wrapped around our block. Why do we have that? The simple reason is we need to have a way to tell Angular to update the UI bindings. Other libraries such as Knockout.js use observable objects that you need to wrap around all of your objects and arrays you want to have 2 way binding. Since you don't have to do that in Angluar, and can use basic JS objects you just need to wrap your code in an $apply() call to notify the UI. A lot of the built in async calls such as $http should automatically wrap the return blocks in an $apply statement, but I haven't used it yet.
The last statement actually does the injection, and passes the objects in based on the order of the array. There is also another way of implicitly injecting the classes without using $inject by naming your parameters the same as your modules. However if you plan on minifying your code, you shouldn't use this method and stick with $inject.
Promises
This is all nice and works, but we can still improve on it by implementing the promises that come built in with Angular. If you aren't familiar with promises, here is a 1000 foot overview. They are a trend in the JS community to move away from your code from being a bunch of nested callback statements to implementing methods instead. The outcome is typically the same, but it typically results in cleaner, more readable, and maintainable code. My first exposure to them was with Windows 8 JS apps that utilize them. If you haven't jumped on the bandwagon yet, I suggest you start soon. Here is a good read on them.To implement promises we are going to change the code in the following to look like this:
var factory= service.factory('objectsService',['$q', '$rootScope', function(q$, $scope){ var objectService = { retrieveObjects: function(){ var deferred = q$.defer();Note that now I am passing in 2 parameters to my function now. The first one is $q. $q is the Angular object that is used for promises. The second one, $rootScope, is the root scope of the app. To create a promise we get a deferred object by calling $q.defer(). In our service callback we call the deferred object's resolve method passing in the same parameters as you would in the callback. If there was an error you would call the deferred object's reject method. At the end of the function call we want to return the deferred object so the controller can utilize the promise. The other change I made here was to make the $apply() call of the $scope here instead of in my controller. The main reason I did that is for maintainability purposes. If I needed to use this call somewhere else, I already have it wrapped in the $apply call so I don't have to worry about it the next time I use it.
window.setTimeout(function callback(){ try{ $scope.$apply(deferred.resolve( {Name: 'Sun', Type: 'Star', Children: [{Name: 'Mercury', Type: 'Planet', Orbit: 88}, {Name: 'Venus', Type: 'Planet' , Orbit: 244 }, {Name: 'Earth', Type: 'Planet' , Orbit: 365}, {Name: 'Mars', Type: 'Planet' , Orbit: 687}, {Name: 'Jupiter', Type: 'Planet', Orbit: 4332}, {Name:'Saturn', Type: 'Planet', Orbit: 10833}, {Name: 'Uranus', Type: 'Planet', Orbit: 30800}, {Name: 'Neptune', Type: 'Planet', Orbit: 60190}, {Name: 'Pluto', Type: 'Dwarf Planet', Orbit: 90520}]})); }catch(e){ deferred.reject(e); } }, 1000); return deferred.promise; } }; return objectService}]);
function orbitController($scope, objectsService){The updated controller is updated to implement the promise object returned by the retrieveObjects() call. by passing in a function to the then() method, it will be called when the resolve method of the promise is called. I also added a property to the scope called IsLoading that will display a loading message to the user until the promise is fulfilled.
$scope.IsLoading = true;
objectsService.retrieveObjects().then(function retrieveObjectsResult(values){
$scope.parentObject = values;
$scope.IsLoading = false;
});
Here is an updated JSFiddle with the entire sample above.
No comments:
Post a Comment