OdeToCode IC Logo

Building a FileReader Service For AngularJS: Promises, Promises

Tuesday, July 2, 2013

Let’s build an AngularJS service to wrap an HTML 5 FileReader object.

The first question a curious person might ask is: why create a service? Why not use a FileReader directly from the code in a model or controller?

Here are two reasons:

1) To build an adapter around FileReader that works with promises instead of call back functions (which is what this post will focus on).

2) To achieve greater flexibility. Services in AngularJS can be decorated or replaced at runtime.

To understand the advantages of #1, we’ll need to learn about promises in AngularJS. For more general information about promises, see “What’s so great about JavaScript Promises”.

Promises and $q

In Angular, $q is the well known name of a promise provider, meaning you can use $q to create new promises. Here’s some code for a simple service that will perform async operations. The service requires $q as a dependency.

(function(module) {

    var slowService = function ($q) {

        var doWork = function () {
            var deferred = $q.defer();

            // asynch work will go here

            return deferred.promise;
        };

        return {
            doWork: doWork
        };
    };
    
    module.factory("slowService",
                   ["$q", slowService]);

}(angular.module("testApp")));

The doWork function uses $q.defer to create a a deferred object that represents the outstanding work for the service to complete. The function returns the deferred object’s promise property, which will give the caller an API for figuring out when the work is complete. This is the basic pattern most async services will use, but in order for anything interesting to happen with the promise, the service will also need to resolve or reject the promise when the async work is complete. Here is a new version of the doWork function .

var doWork = function (value, scope) {
    var deferred = $q.defer();

    setTimeout(function() {
        scope.$apply(function() {
            if (value === "bad") {
                deferred.reject("Bad call");
            } else {
                deferred.resolve(value);
            }
        });
    }, 2000);

    return deferred.promise;
};

This version is using setTimeout to simulate work that takes 2000 milliseconds to complete. Normally you’d want to use the Angular $timeout service instead of setTimeout, but setTimeout is here to illustrate an important point.

AngularJS promises do not propagate the result of a completed promise until the next digest cycle.

This means there has to be a call to scope.$apply (which will kick off a digest cycle) for the promise holder to have their callback functions invoked. With the $timeout service we wouldn’t need to use $scope.apply ourselves since the $timeout service will call into our code using $apply, but most things you’ll wrap with a service are not Angularized, so you’ll need to use $apply when resolving a promise.

Promises From The Client Perspective

Promises can lead to readable code for the client, as the code in the bottom of the controller demonstrates.

var TestController = function($scope, $log, slowService) {

    var callComplete = function (result) {
        $scope.serviceResult = result;
    };

    var callFailed = function(reason) {
        $scope.serviceResult = "Failed: " + reason;
    };

    var logCall = function() {
        $log.log("Service call completed");
    };

    $scope.serviceResult = "";

    slowService
        .doWork("Hello!", $scope)
        .then(callComplete, callFailed)
        .always(logCall);
};

One interesting feature of AngularJS is how the data-binding infrastructure understands how to work with promises. If all you want to do is assign the resolved value to a variable for data-binding, then you can assign the variable a promise instead of using a callback and Angular will know how to pick up the resolved value and update the view.

$scope.serviceResult =
    slowService.doWork("Hello!", $scope)
               .always(logCall);

And of course chained promises can kick off new async operations and the runtime will work through the promises in a serial fashion.

var TestController = function($scope, $log, slowService) {

    var saveResult = function (result) {
        $scope.callResults.push(result);
    };
 
    $scope.callResults = [];

    slowService
        .doWork("Hello", $scope)
        .then(saveResult)
        .then(function() {
            return slowService.doWork("World", $scope);
        })
        .then(saveResult);
};

With the above code and this bit of markup:

<li ng-repeat="result in callResults">
    {{ result }}
</li>

... then “World” appears on the screen roughly 2 seconds after “Hello”.

It’s interesting to note in the last example that if the first call fails, the 2nd call never happens (because the 2nd call is started from an “on success” function). If you want to know that the call failed you don’t have to add an error callback to every .then invocation.  A rejected promise will tunnel it’s way through the rest of the chain invoking error call backs as it goes, meaning you could get away with an error callback in the final .then.

For example, the following chained operations are doomed to failure since the service will reject the initial call with a parameter of “bad”.

slowService
    .doWork("bad", $scope)
    .then(saveResult)
    .then(function() {
        return slowService.doWork("World", $scope);
    })
    .then(saveResult, logFailure);

Even though the error handler logFailure doesn’t appear till the end of the chain, the initial failed service call will find it and the 2nd service is skipped.

In addition to serial processing, you can kick off multiple promises and wait for them to complete using $q.all.

var promises = [];
var parameters = ["Hello", "World", "Final Call"];

angular.forEach(parameters, function(parameter) {
    var promise = slowService.doWork(parameter, $scope).then(saveResult);
    promises.push(promise);
});

$q.all(promises).then(function() {
    $scope.message = "All calls complete!";
});

Now that we know a little more about promises, we can move on to the business of building a service in the next post.