Friday, May 9, 2014

Global state in AngularJS controllers

Suprisingly for me the controller state in AngularJS is not preserved between the controller invocations. I at least expected an option to switch it on and off on demand. For the classic application it was difficult to achieve that we may restore the state for a given view (eg. to be on the same page as we were leaving the view), what sounds great for me from the application usability point of view.

But, it's right there, so it can be implemented. However, I don't like using the $rootScope for this, what can be found in many examples on the net, in the same way I don't use globals, because they produce a mess. Here I'd like to propose an elegant solution for this.

I have the service that holds all controller states, and provide the initialization function that may be used when the state hasn't been cached yet. The code snippet:

myModule.service('state',
function () {
/** The global/shared modules state (filled in modules.run) **/
this.global = {};
/**
* Initializes the scope state. If the scope state is already initialized and kept from previous invocation,
* the scope is initialized using the previous state. If the scope state doesn't exist, the scope is initialized
* using the initFunc.
*
* NOTE: that only objects are traced in the cached scope state. So it the scope contains primitives, they are
* not traced ie. the changes in the primitives on current scope are not reflected in cached version.
*
* @param scopeName {string} The virtual unique scope name (eg. 'ControllerName')
* @param $scope {object} Angular scope object that should be populated with the data
* @param initFunc {function} Scope initialization function(scope) - gets the cached scope in argument, not
* the real angular scope (!)
*/
this.initScope = function (scopeName, $scope, initFunc) {
// initializes the cached scope if is not initialized yet
if (!angular.isDefined(this.global[scopeName])) {
console.debug('state:service', 'Creating new cached scope:', scopeName);
var cachedScope = {};
initFunc(cachedScope);
this.global[scopeName] = cachedScope;
} else
console.debug('state:service', 'Using existing cached scope:', scopeName);
// populates the cached scope to current scope
angular.forEach(this.global[scopeName], function (propVal, propName) {
$scope[propName] = propVal;
});
};
/**
* Deletes the cached version of scope. After next initScope() the scope will be populated with
* initFunc data again.
* @param scopeName {string} The virtual unique scope name (eg. 'ControllerName')
*/
this.deleteScope = function (scopeName) {
if (angular.isDefined(this.global[scopeName])) {
console.debug('state:service', 'Deleting existing cached scope:', scopeName);
delete this.global[scopeName];
}
};
});
myModule.controller('myCtrl',
['$scope', 'state',
function($scope, state) {
// init the scope or use cached version of data
state.initScope('myCtrl', $scope, function(scope) {
scope.moduleState = {
detailsVisible: false // if the details are visible
};
});
}]);