/ angularjs

Ordered Initialization of an AngularJS Application with Asynchronous Data Using Promises

AngularJS helps with loading your app through dependency injection based on ordered class instantiation. Nevertheless, you sometimes need to know whether any pending request or function call has been finished in order to initialize your controllers, $scope or services. This article describes our solution to split the application lifecycle into several phases, namely „instantiation“, „initialization“ and „running“. You can find the downloadable code at GitHub, a working demo is available at jsfiddle.

Naive implementation

The example below shows how you let AngularJS inject other services into your controller or service:

myModule.service(‚myService‘, function() {  
  this.getOptions = function() {  
    return [{name:’entry1′, value:0},  
            {name:’entry2′, value:1}];  
  };  
});

myModule.controller(’simpleController‘, function($scope, myService) {  
  $scope.options = myService.getOptions();  
  $scope.selectedOption = $scope.options[0];  
});

You see the simpleController using some options provided by myService. The getOptions implementation returns a static list of strings, which are ultimately used in the controller to configure its $scope.

Now let’s assume we cannot simply return a static list of strings but we have to perform an http call to a remote backend. In addition to the backend call, let’s add a $watch to the selectedOption, since it’s bound to a dropdown and we want to handle any user driven selection change (see jsfiddle demo 1 including html code):

var myModule = angular.module(‚myModule‘, []);

myModule.service(‚myService‘, function($http) {  
  this.getOptions = function() {  
    return $http({  
      "method": "get",  
      "url": ‚http://www.example.com/echo/json/‘  
    });  
  };  
});

myModule.controller(’simpleController‘, function($scope, myService) {  
  $scope.selectedOption = null;  
  $scope.options = [];  
  myService.getOptions().then(function(result) {  
    $scope.options = result.data.options;  
    $scope.selectedOption = 0;  
  });  
  $scope.$watch(’selectedOption‘, function(newValue, oldValue) {  
    // handle selection change …  
    console.log("selection: " + $scope.selectedOption);  
  });  
});  

When running the code you’ll notice the console log twice. The first log output is triggered by AngularJS due to adding the $watch, the second log output is triggered in the promise revolve handler when we set selectedOption to zero. Both outputs illustrate situations where no user has triggered a change, but the application itself during the initialization phase. The example only logs to the console, but you certainly want to implement more complex logic and you might prefer to only be notified when the user has changed the value. The code adding the $watch on selectedOption could be moved inside the promise resolve handler, but you’d still get the first trigger, which is still not optimal.

You can imagine that adding more services to the dependency tree and adding more $watches or event listeners makes things worse. The example above doesn’t have major problems, but we had a bigger and more complex application with more lifecycle dependencies and some assumptions on valid state in the $scope.

Implementation with initialization in mind

We reached a situation which showed that only adding $watches at one place triggered a cascade of mistimed events and $watch triggers at other code parts of our application, which let controllers or services fail during initialization. The events needed to be filtered or disabled in such an early phase. We preferred not to implement a complete state machine or use similar frameworks, because we considered our initialization phase to be only a small part of our application lifecycle – without saying that its impact was less important.

Our approach was to explicitly let the controllers declare their initialization tasks and to implement a service coordinating all of those steps. We also dislike the initial $watch triggers, so we wanted to disable them, too.

The example below shows a controller with dedicated initialization code:

var myModule = angular.module(‚myModule‘, []);

myModule.service(‚myService‘, function($http) {  
  this.getOptions = function() {  
    return $http({  
      "method": "get",  
      "url": ‚http://www.example.com/echo/json/‘  
    });  
  };  
});

myModule.controller(’simpleController‘, function($scope, myService, init) {  
  $scope.selectedOption = null;  
  $scope.options = [];

  init(’simpleController‘, [myService.getOptions()], function(result) {  
    $scope.options = result.data.options;  
    $scope.selectedOption = 0;  
  });  
  init.watchAfterInit($scope, ’selectedOption‘, function(newValue, oldValue) {  
    // handle selection change …  
    console.log("selection: " + $scope.selectedOption);  
  });  
});  

The code doesn’t differ too much from the original example, because the most important part is hidden in the init service, which has been introduced as new dependency. We changed the call to myService.getOptions() in a way that the init service can decide when to deliver the result of the backend call to the simpleController. In addition we wrapped the $watch call, so that the init service also decides which $watch triggers can arrive at the simpleController callback function.

AngularJS initialization service

We needed both $watch and $on to be encapsulated by the init service, our current implementation looks like the code snippet below:

.factory(‚init‘, function () {  
  var initialized = false;

  var init = function () {  
    // …  
  };

  init.watchAfterInit = function (scope, expression, listener, deepEqual) {  
    scope.$watch(expression, function (newValue, oldValue, listenerScope) {  
    if (initialized) {  
      listener(newValue, oldValue, listenerScope);  
    }  
  }, deepEqual);  
};

  init.onAfterInit = function (scope, event, listener) {  
    scope.$on(event, function (event) {  
      if (initialized) {  
        listener(event);  
      }  
    });  
  };

  return init;  
});  

Both functions watchAfterInit and onAfterInit are decoupled from the rest of the init service except for the initialized flag, which makes the init service stateful. The *AfterInit functions solve two problems mentioned above:

  • don’t publish initial triggers when a $watch is added (see the AngularJS docs)
  • don’t publish events and $watch triggers before the application initialization is finished

In addition to the *AfterInit functions, the init service orchestrates several services with their configured init(...) functions. In the example above we configured the simpleController with an init function, which is implemented as follows. You can find a comparison of the first demo and the new implementation at jsfiddle demo 2.

.factory(‚init‘, function ($q, $rootScope, $browser) {

  var initFunctions = [  
    'simpleController',  
    'anotherController',  
    'thirdController'  
  ];  
  var registeredInitFunctions = {};  
  var initialized = false;

  var initApplication = function () {  
    var simpleController = registeredInitFunctions[’simpleController‘];  
    var anotherController = registeredInitFunctions[‚anotherController‘];  
    var thirdController = registeredInitFunctions[‚thirdController‘];

    var broadcastAppInitialized = function () {  
      $browser.defer(function () {  
        initialized = true;  
        $rootScope.$apply(function () {  
          $rootScope.$broadcast(‚appInitialized‘);  
        });  
      });  
    };  
    simpleController.init()  
      .then(anotherController.init)  
      .then(thirdController.init)  
      .then(broadcastAppInitialized);  
  };

  $rootScope.$on(‚$routeChangeStart‘, function () {  
    registeredInitFunctions = {};  
    initialized = false;  
  });

  var initAppWhenReady = function () {  
    var registeredInitFunctionNames = _.keys(registeredInitFunctions);  
    var isRegistered = _.partial(_.contains, registeredInitFunctionNames);  
    if (_.every(initFunctions, isRegistered)) {  
      initApplication();  
      registeredInitFunctions = null;  
    }  
  };

  var init = function (name, dependencies, initCallback) {  
    registeredInitFunctions[name] = {  
      init: function () {  
        var internalDependencies = $q.all(dependencies);  
        return internalDependencies.then(initCallback);  
      }
    };  
    initAppWhenReady();  
  };

  init.watchAfterInit = function (scope, expression, listener, deepEqual) {  
    // …  
  };  
  init.onAfterInit = function (scope, event, listener) {  
    // …  
  };

  return init;  
});  

To show you how to coordinate a more complex setup, we declared two additional controllers anotherController and thirdController, but the basic idea is independent of the number of initialized controllers or services.

How it works

Our init service essentially has to decide when to toggle the initialized flag to true. There are some steps necessary to reach the initialized state:

  1. all expected initFunctions need to be registered via init(...)
  2. as soon as all initFunctions have been registered, initAppWhenReady() calls the actual initialization function initApplication()
  3. initApplication() calls each initFunction in specified order. Using promises it waits for each initFunction to finish before calling the next initFunction
  4. finally, broadcastAppInitialized() is called

The first step converts each init(…) call by wrapping the dependency return values (which are expected to be promises) in $q.all(…) and sets the initCallback to be called after resolving all dependencies. That way, the init service generates specific init functions and remembers them in the internal registeredInitFunctions map.

initAppWhenReady() checks on each addition to the registeredInitFunctions whether all expected initFunctions have been collected. The expected initFunctions are declared in the internal list and would be a configurable part of the init service.

initApplication() knows the order of initFunction calls, so it can retrieve each registered initFunction and call them in order. By using promises, there’s no need to create additional callbacks and the code can be written like a simple chain.

The last step in the chain toggles the initialized flag, so that future events and $watch triggers won’t be suppressed by the init service. Furthermore it broadcasts an event named "appInitialized", so that other parts in our app can react as early as possible on the finished initialization. You’ll notice the $browser.defer(...) around the flag toggling and broadcasting. To understand the reasons, you need to know about the AngularJS $digest loop. By using $browser.defer, we let the $digest loop finish and enqueue our finalizing step after all other currently enqueued tasks. That way, we prevent our init service to publish $watch/$on events too early. Since $browser.defer doesn’t call our callback function during a $digest loop, we need to compensate by wrapping the $broadcast() in a $rootScope.$apply() call.

Summary

After explicitly thinking about the application lifecycle split into several phases, we got additional awareness on how to integrate with AngularJS and its suggested lifecycle. Our tests have been improved by testing initialization (where applicable) and normal interaction in dedicated contexts.

The init service is completely unobtrusive for usual services and controllers. Only when necessary, any controller can be added to the initialization chain. As soon as the initialization is finished, the init service is quite inactive. Only on $route changes, and subsequent controller re-instantiation, the init service also needs to be reset, which is solved by listening to $routeChangeStart events.

Holding state inside the init service might be considered a bad design, but since all AngularJS services are designed to be singletons and JavaScript is single threaded, we currently don’t have any issues.

We would appreciate any kind of feedback. You can use the comment feature or connect via @gesellix or @hypoport. You can join a discussion at the AngularJS group, too!