When your Angular.js application expands to a state that you no longer find it easy to understand, it's time you used dependency injection and modularise your application into units of interrelated modules.
AngularJS makes you write code with testability, dependency injection, and modularity. You didn't need to create a class to contain that piece of conversion logic, you needed a filter. But there will be situations where one controller needs to talk to another controller. This is a result of keeping all dependencies handled by a Dependency Injection framework. When your controller has to pass state to another controller, that's where you need a service.
State Service
A novice Angular developer would come with a solution like the following.
angular.module('services', [])
.factory('state', function () {
state;
return {
state: state,
};
});
What's the issue with this solution? In many cases, you may not even notice it. The answer is Objects and arrays are passed by reference. Values are not.
var state = { name: "one" };
var controller = { name: state.name };
state. = "two";
controller.name; // "one"
So updating the name property of this state service does not propagate changes to the scope of each controller. If your state happens to be an array or an object, and you stick to modifying it rather than reassigning it, this would still work (it is possible to implement the state service using a shared object instead, I’ve added an example of this at the bottom of the page). However, AngularJS’s $scope is a bit more complicated than my silly example, and has some inner machinery that will not react as you expect. You see, in order to keep the DOM and the data on the scope in sync, also known as two way binding, it uses something called the digest cycle. A new digest cycle is only triggered if AngularJS can recognize a change being made on the scope. Since changes are made far, far away, it will be unable to do so. Bummer!
Events
Luckily, we have events. In AngularJS, these are transmitted through the rootScope and bubble up through all scopes.
Using $scope.$on
we can attach listeners to these and react to changes in our state service!
So lets try again. In steps, what we want to do is this:
- Set an initial state and expose this
- Expose an updating function
- Let this function broadcast changes
angular.module('services', [])
.factory('state', function ($rootScope) {
'use strict';
state;
broadcast = function (state) {
$rootScope.$broadcast('state.update', state);
};
update = function (newState) {
state = newState;
broadcast(state);
};
return {
update: update,
state: state,
};
});
And on the controller side:
- Get the initial state
- Set up a listener on the change event
- Update the state when needed
angular.module('controllers', ['services'])
.factory('MainCtrl', function ($scope, state) {
$scope.state = state.state;
$scope.('state.update', function (newState) {
$scope.state = newState;
});
$scope.update = state.update;
});
As a convenience, we could move the boilerplate of event attaching to the state manager. Something like this:
// in the service
onUpdate = function ($scope, callback) {
$scope.('state.update', function (newState) {
callback(newState);
});
};
// and in the controller
state.onUpdate($scope, function (newState) {
$scope.state = newState;
};
Finishing up
Now you can share state between controllers in AngularJS. You can easily manage the logged-in user, the user settings, or handle navigation with one controller, and the page content with another!
angular.module('services', [])
.factory('state', function () {
'use strict';
state = {};
return {
state: state,
};
});
Using it our controller looks more like this.
angular.module('controllers', ['services'])
.factory('MainCtrl', function ($scope, state) {
$scope.state = state.state;
});
The big difference here will be in the markup, where you refer to your state, eg a property called username, as state.username
rather than just username
.
In most cases this is plenty! If you need to react to changes in the state object, you can $watch
.
angular.module('controllers', ['services'])
.factory('MainCtrl', function ($scope, state) {
$scope.state = state.state;
$scope.$watch('state', function (newVal, oldVal) {
// your code here
});
});