Thursday, August 22, 2013

Getting Started With Angular Part 3. Factories, Promises, and Injections. Oh My!

This post will cover a few major topics in Angular including factories, promises, and injection.  In Angular, when you are writing your actual app you will most likely have some kind of service or rest endpoint that you are hitting.  Angular has a lot of features that help you write your code in a cleaner and more maintainable way, or allow you to use your existing services and just leverage the features you want in an Angular app.

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


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();
            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}]);
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.
function orbitController($scope, objectsService){
    $scope.IsLoading = true;
    objectsService.retrieveObjects().then(function retrieveObjectsResult(values){
        $scope.parentObject = values;
        $scope.IsLoading = false;
    });
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.

Here is an updated JSFiddle with the entire sample above.

No comments:

Post a Comment