I need to communicate state which several services need and originates in data bound to the scope of a controller. What would a good and \'Angular zen\' way to
I made a demo plunker :http://plnkr.co/edit/RihW4JFD8y65rDsoGNwb?p=preview
There are lots of different ways and this question is somehow very broad, but I wanted to share another approach.
Your problem starts with the way you communicate between controllers and services.
Instead of creating services as objects with methods (which enforce you to use the observer pattern) , you can point scopes directly to services by creating data objects services and let $digest
make the work for you.
The very reason why angular uses $scope is to let you use POJO rather than an observer pattern like other frameworks. When you create these kind of method driven services you introduce the same pattern that angular tries to avoid.
It's important to note that you must point to properties on the service object and not to the object reference itself.
app.factory('FooService', function($rootScope, Configuration){
$scope = $rootScope.$new();
$scope.Configuration = Configuration;
var foo = {
data : null
};
$scope.$watch('Configuration', function(config){
foo.data = // do something with config
}, true)
return foo;
});
app.factory('Configuration', function(){
return {
data : 'lol'
}
});
app.controller('SettingsCtrl', function($scope, Configuration){
$scope.config = Configuration;
});
app.controller("HomeCtrl",function($scope, FooService){
$scope.foo = FooService;
});
Actually, I prefer the use of $scope.$broadcast
mostly for performance reasons but also because it's the most elegant way to share states across differnet parts of the application. I really don't care about global states, I use namespaces for my events.
Very interesting question.
We've been using angular for a few months now and are currently considering how this can be done better. We're still trying to figure out what can be an optimal solution, maybe this will help in getting there.
I think the original solution you've provided is pretty similar, but there are a few considerations which should be taken:
I also believe using a shared service which is injected when needed is the way to go.
I've modified Ilan Frumer's cool plunker example: http://plnkr.co/edit/YffbhCMJbTPdcjZDl0UF?p=preview
Breaking down the problem into two can help in thinking of what can be a solution.
Updating the config service with changes done in the settings page
For this, using a $watch looks like the optimal solution, you wait for the specific config to be changes and as a response let the config service know what have been changed. I prefer of doing it explicitly, to keep the flow of the change clear and consistent.
This can be done by making a local copy of the Configuration data and watching for changes.
app.factory('Configuration', function($rootScope){
return {
var config = {
user: "xxxx"
}
return {
config: config,
set: function(item, value) {
config[item] = value;
$rootScope.$emit("configChanged." + item);
},
changed: function(item, callback, scope) {
var deregister = $rootScope.$on("configChanged." + item, function() {
callback(config[item], config)
});
callback(config[item], config);
if (scope) {
scope.$on("$destroy", deregister);
}
}
}
}
});
app.controller('SettingsCtrl', function($scope, $timeout, Configuration){
// Get a local copy - configuration shouldn't change until change
// is completed
$scope.data = angular.copy(Configuration.config);
// Keep UI interactions in the controller
// If more complex UI is required another way could even use a
// directive for this
$scope.$watch("data.user", function(user) {
Configuration.set('user', $scope.data.user);
});
});
app.factory('DetailsService', function(Configuration, $http){
var details = {
data : null,
};
Configuration.changed("user", function(user) {
// Handle user change ...
});
});
How services / controllers observe changes
This also has two options.
In case there are multiple states services, another optimization can be to extract the "set", "changed" functions to a generic implementation.
Hope it helps.
You have two options:
$watch
. I.e. what you implemented, or put differently the Mediator Pattern.$scope.$broadcast
and/or $scope.$emit
with $scope.$on
events to communicate changes.Personally I don't see anything wrong with option (1). I also don't consider that to be global state in the traditional sense. Your configuration is contained within the Configuration
service, you can only access this service through DI injection which makes it testable.
One possible improvement is to create a ConfigMediator service and move the update and callback functionality to it to separate concerns.
I have faced something similar in the past, and considered four possible approaches:
$broadcast
$rootScope
$watch
from a controllerHere are my thoughts on each:
$broadcast
In an AngularJS presentation I saw, Miško Hevery spoke about the use of $broadcast
(i.e. events) and the use cases for such. The gist was that $broadcast
is more intended for reacting to events that are not closely coupled with whatever you are working with, otherwise an alternative is likely preferable. Also on this subject, the Best Practices guide on the angular wiki recommends that:
Only use .$broadcast(), .$emit() and .$on() for atomic events: Events that are relevant globally across the entire app (such as a user authenticating or the app closing).
Here, as you have settings which are closely associated to whatever populates ng-view
, it would suggest an alternative to using $broadcast
is preferable.
$rootScope
This is a global state as you mention (and want to avoid). It wasn't/isn't my personal preference either to expose settings to my entire app, despite it often being the easy option. I personally reserve $rootScope
for configuration settings and 'soft' variables, like page title etc. I wouldn't elect to use this option.
Observer Pattern
Registering callbacks against the Configuration factory is a solid approach. In regard to your persistent callbacks, you can listen for the $destroy
event on the scope, calling a remove
method on your Configuration factory to remove the callback. This could be considered a good example of how $broadcast
be used; the controller is concerned with the event and must react to it, but the event itself is not specific to the data shared by the controllers/Configuration service.
$watch
By using a shared service, it can be injected it into any controller concerned with the settings. Right now, any change to the config will trigger your callback, when perhaps some views may only be concerned with one or two configuration settings. $watch
will allow you to easier observe changes to only those attributes. I can't speak to the overhead vs registering callbacks, but this feels like the most 'angular' way to me.
This is how this could be implemented using $watch
:
var app = angular.module("myApp",[]);
app.factory("Configuration",function(){
var data = {
settingOne: true,
settingTwo: false
};
return data;
})
app.controller("SettingsCtrl",function($scope, Configuration){
// do something
})
app.controller("HomeCtrl",function($scope, Configuration){
// detect any change to configuration settings
$scope.$watch(function() {
return Configuration;
}, function(data) {
// do something
}, true)
// alternatively only react to settingTwo changing
$scope.$watch(function() {
return Configuration.settingTwo
}, function(data) {
// do something
})
})
Note that if you were to require a slightly more complicated Configuration factory, you could shift to using getter/setter methods and keep the config settings themselves private. Then, in the $watch
, you should watch the method call instead of the property itself.
UPDATE:
At the time of answering, I preferred the approach of a $watch
within a controller. After some time developing with the framework, I now try to keep $watch
out of the controller altogether, instead preferring, where possible, to directly invoke a function at the point of change of the value, or through leveraging ng-change
.
One reason for such is the complexity it adds to testing the controller, but perhaps moreso that it's inefficient: for every $digest
cycle angular invokes, every registered $watch
will be evaluated regardless, and it may very well be responding to a change made to a value with an existing $watch
.
Rather than surmize the cons and solutions on this perspective, there is a very good article on exactly this issue here: Angular JS - you probably shouldn't use $watch in your controllers.