angular-promises

Preloading data using deferred promises in Angular.js

I’ve been using Angular.js for a while now and I can say that I absolutely love it. The learning curve is specially steep if you are coming from jQuery-only front end development but once you get the hang of it, it is a fantastic MVW framework.

I am however, trying to find the best way to pre-load data into a controller by using deferred promises inside a factory. Let me first explain why.

In the app that I am building I need to access an asynchronous source of data that reads from local storage in chrome. The storage adapter I am using will trigger a callback once the data has been loaded from the disk, however the way I have designed the app, and the fact that I am using ui-router to create states instead of using Angular’s routeProvider, forces me to load all the data that the user is going to need before I present anything on the application.

At first I thought it would be Ok to create a factory wrapping the methods that that storage adapter provides, but this got messy very fast and now my controllers are becoming clutter and hard to read. I also found the problem that each data set that I have to load from the disk will need to be loaded by using a different asynchronous call and I will need to find a way to chain them all sequentially without reaching the callback hell.

So now I need to tackle the problem by using a more standard approach: services and promises.

The new data layer will have two main components:

1. A storage factory that will invoke the storage adapter methods wrapping them into deferred promises that won’t be resolved until the adapter loads the data from the disk.

2. A higher level data service that will encapsulate the caching of the data for the controllers by invoking the methods exposed by the storage factory of 1. If nothing hasn’t been loaded then the data service will invoke the storage service, also by using deferred promises that end up storing the data into private objects. The next time the data is queried by the controllers, the data service will return the cached copy instead of invoking the storage service.

Hard to understand without some code, so here it goes.

This will be the storage adapter wrapper factory, each getSomeDataX() method uses a promise. I’ve created some fake code inside each of it by using a 1 second timeout to simulate a delay in the storage adapter for testing purposes:

var storageService = app.factory('storageService',['$q', '$timeout', function($q, $timeout) {
return {
    getSomeData1: function() {
        var deferred = $q.defer();

        $timeout(function() {
            deferred.resolve('Data1 loaded from storageService');
        }, 1000);

        return deferred.promise;
    },
    getSomeData2: function() {
        var deferred = $q.defer();

        $timeout(function() {
            deferred.resolve('Data2 loaded from storageService');
        }, 1000);

        return deferred.promise;
    },
    getSomeData3: function() {
        var deferred = $q.defer();

        $timeout(function() {
            deferred.resolve('Data3 loaded from storageService');
        }, 1000);

        return deferred.promise;
    }
}
}]);

The code is very self explanatory, each method creates a deferred object and returns the promise instantly. When the storage adapter loads the data from the disk, the promise is resolved and the data itself is returned to the owner of the promise.

Now this will be the data factory (service) that will access the above:

var dataService = app.factory('dataService', ['$q', 'storageService',
    function($q, storageService) {
        // private data vars
        var data1 = {
            isLoaded: false,
            value: 'Data1 initial value, not loaded yet.'
        };
        var data2 = {
            isLoaded: false,
            value: 'Data2 initial value, not loaded yet.'
        };
        var data3 = {
            isLoaded: false,
            value: 'Data3 initial value, not loaded yet.'
        };

        // private load deferred functions
        var loadData1 = function() {
            var deferred = $q.defer();

            storageService.getSomeData1()
                .then(function(d) {
                    console.log('storageService.getSomeData1().then()');
                    deferred.resolve(d);
                });

            return deferred.promise;
        };
        var loadData2 = function() {
            var deferred = $q.defer();

            storageService.getSomeData2()
                .then(function(d) {
                    console.log('storageService.getSomeData2().then()');
                    deferred.resolve(d);
                });

            return deferred.promise;
        };
        var loadData3 = function() {
            var deferred = $q.defer();

            storageService.getSomeData3()
                .then(function(d) {
                    console.log('storageService.getSomeData3().then()');
                    deferred.resolve(d);
                });

            return deferred.promise;
        };

        return {
            // public, load all data sequentially
            loadAllData: function() {
                var deferred = $q.defer();

                loadData1()
                    .then(function(d) {
                        console.log('dataService.loadData1().then()');
                        data1.value = d;
                        data1.isLoaded = true;
                        return loadData2();
                    })
                    .then(function(d) {
                        console.log('dataService.loadData2().then()');
                        data2.value = d;
                        data2.isLoaded = true;
                        return loadData3();
                    })
                    .then(function(d) {
                        console.log('dataService.loadData3().then()');
                        data3.value = d;
                        data3.isLoaded = true;
                        deferred.resolve();
                    });

                return deferred.promise;
            },
            getData1: function() {
                return data1.value;
            },
            getData2: function() {
                return data2.value;
            },
            getData3: function() {
                return data3.value;
            },
            isData1Loaded: function() {
                return data1.isLoaded;
            },
            isData2Loaded: function() {
                return data2.isLoaded;
            },
            isData3Loaded: function() {
                return data3.isLoaded;
            },
            isDataLoaded: function() {
                return data1.isLoaded & amp; & amp;
                data2.isLoaded & amp; & amp;
                data3.isLoaded;
            }
        }
    }
]);

All right, that is a little longer.
data1, data2, and data3 are private objects that will store the contents coming from the storage service. They have a value property for that purpose, and an isLoaded flag set when the promises are resolved.

The loadData1, 2, and 3 methods are the private wrappers for the storage service methods themselves. They also use promises that only get resolved when the promises in the storage service do themselves, ugh!

Then it exposes a loadAllData method that chains all the promises together using .then() and sequentially loads all the data into their respective objects, setting the isLoaded flags in the process. When the last promise is resolved in the storageService, the current promise in the loadAllData method gets resolved.

The getData and isDataLoaded methods are descriptive enough.

And then the controller will only access the dataService like this:

var appController = app.controller('appController', ['dataService',
    function(dataService) {
      this.boundData1 = dataService.getData1();
      this.boundData2 = dataService.getData2();
      this.boundData3 = dataService.getData3();

      var self = this;

      this.doSomething = function() {
        if (!dataService.isDataLoaded()) {
          dataService.loadAllData()
            .then(function() {
              self.boundData1 = dataService.getData1();
              self.boundData2 = dataService.getData2();
              self.boundData3 = dataService.getData3();
            });
        } else {
          self.boundData1 = dataService.getData1();
          self.boundData2 = dataService.getData2();
          self.boundData3 = dataService.getData3();
        }
      }
]);

(Note that I am using the ‘Controller as’ syntax from Angular 1.1.5 if you are looking for $scope)

The doSomething method will be triggered by whatever means you want, and the first time it is triggered it will ask the dataService to load all the data by resolving the promises. If instead the data is already in the local objects, then it will just get their values, no need to ask the storage service again.

You could even preload the data on the .run() phase of the app and remove the check from the doSomething method itself:

app.run(['dataService',
    function(dataService) {
        if (!dataService.isDataLoaded()) { // I don't even need to check this, it will always be false in here.
            dataService.loadAllData()
                .then(function() {
                    console.log('Pre loading all data in controller.run() phase.');
                });
        }
    }
]);

// Then the controller becomes:

var appController = app.controller('appController', ['dataService',
    function(dataService) {
        this.boundData1 = dataService.getData1();
        this.boundData2 = dataService.getData2();
        this.boundData3 = dataService.getData3();

        var self = this;

        this.doSomething = function() {
            console.log('Data has already been loaded from storageService, getting cached data instead.')

            self.boundData1 = dataService.getData1();
            self.boundData2 = dataService.getData2();
            self.boundData3 = dataService.getData3();
        }
    }
]);

Yeah, much cleaner.

Next steps

I am sure there are other ways to do this better, I just started using $q and deferred objects and I am pretty happy this works well for my needs.

The objective of this separation was to make the controllers simpler and leave all the data access to the factories where it belongs. At some point I will have to add an extra storage service to be able to sync the data to a remote server while keeping the snappy local functionality intact, and this design allows me to do so.

The next thing I am doing will be to put all the this process inside a ‘loading’ modal screen the same way than gmail or the google play music library does, and only showing the first view once everything has been loaded from the disk. This should be easy by using resolve in the routeProvider, but go figure I am using ui-router and I am not sure just yet if I can inject and use a service before loading the state.

And after that I can work on invoking the sync factory right after the dataService is done, to request all the updates from the remote server while being able to keep working on the app using the cached data, sweet!

Here is a plunker to show this working, don’t forget to open the console (F12).

UPDATE: please check out the follow up to this post here

One thought on “Preloading data using deferred promises in Angular.js”

Leave a Reply