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:
- all expected
initFunctions
need to be registered viainit(...)
- as soon as all initFunctions have been registered,
initAppWhenReady()
calls the actual initialization functioninitApplication()
initApplication()
calls each initFunction in specified order. Using promises it waits for each initFunction to finish before calling the next initFunction- 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!