I have a service, say:
factory('aService', ['$rootScope', '$resource', function ($rootScope, $resource) {
var service = {
foo: []
};
return service;
}]);
And I would like to use foo
to control a list that is rendered in HTML:
<div ng-controller="FooCtrl">
<div ng-repeat="item in foo">{{ item }}</div>
</div>
In order for the controller to detect when aService.foo
is updated I have cobbled together this pattern where I add aService to the controller's $scope
and then use $scope.$watch()
:
function FooCtrl($scope, aService) {
$scope.aService = aService;
$scope.foo = aService.foo;
$scope.$watch('aService.foo', function (newVal, oldVal, scope) {
if(newVal) {
scope.foo = newVal;
}
});
}
This feels long-handed, and I've been repeating it in every controller that uses the service's variables. Is there a better way to accomplish watching shared variables?
You can always use the good old observer pattern if you want to avoid the tyranny and overhead of $watch
.
In the service:
factory('aService', function() {
var observerCallbacks = [];
//register an observer
this.registerObserverCallback = function(callback){
observerCallbacks.push(callback);
};
//call this when you know 'foo' has been changed
var notifyObservers = function(){
angular.forEach(observerCallbacks, function(callback){
callback();
});
};
//example of when you may want to notify observers
this.foo = someNgResource.query().$then(function(){
notifyObservers();
});
});
And in the controller:
function FooCtrl($scope, aService){
var updateFoo = function(){
$scope.foo = aService.foo;
};
aService.registerObserverCallback(updateFoo);
//service now in control of updating foo
};
In a scenario like this, where multiple/unkown objects might be interested in changes, use $rootScope.$broadcast
from the item being changed.
Rather than creating your own registry of listeners (which have to be cleaned up on various $destroys), you should be able to $broadcast
from the service in question.
You must still code the $on
handlers in each listener but the pattern is decoupled from multiple calls to $digest
and thus avoids the risk of long-running watchers.
This way, also, listeners can come and go from the DOM and/or different child scopes without the service changing its behavior.
** update: examples **
Broadcasts would make the most sense in "global" services that could impact countless other things in your app. A good example is a User service where there are a number of events that could take place such as login, logout, update, idle, etc. I believe this is where broadcasts make the most sense because any scope can listen for an event, without even injecting the service, and it doesn't need to evaluate any expressions or cache results to inspect for changes. It just fires and forgets (so make sure it's a fire-and-forget notification, not something that requires action)
.factory('UserService', [ '$rootScope', function($rootScope) {
var service = <whatever you do for the object>
service.save = function(data) {
.. validate data and update model ..
// notify listeners and provide the data that changed [optional]
$rootScope.$broadcast('user:updated',data);
}
// alternatively, create a callback function and $broadcast from there if making an ajax call
return service;
}]);
The service above would broadcast a message to every scope when the save() function completed and the data was valid. Alternatively, if it's a $resource or an ajax submission, move the broadcast call into the callback so it fires when the server has responded. Broadcasts suit that pattern particularly well because every listener just waits for the event without the need to inspect the scope on every single $digest. The listener would look like:
.controller('UserCtrl', [ 'UserService', '$scope', function(UserService, $scope) {
var user = UserService.getUser();
// if you don't want to expose the actual object in your scope you could expose just the values, or derive a value for your purposes
$scope.name = user.firstname + ' ' +user.lastname;
$scope.$on('user:updated', function(event,data) {
// you could inspect the data to see if what you care about changed, or just update your own scope
$scope.name = user.firstname + ' ' + user.lastname;
});
// different event names let you group your code and logic by what happened
$scope.$on('user:logout', function(event,data) {
.. do something differently entirely ..
});
}]);
One of the benefits of this is the elimination of multiple watches. If you were combining fields or deriving values like the example above, you'd have to watch both the firstname and lastname properties. Watching the getUser() function would only work if the user object was replaced on updates, it would not fire if the user object merely had its properties updated. In which case you'd have to do a deep watch and that is more intensive.
$broadcast sends the message from the scope it's called on down into any child scopes. So calling it from $rootScope will fire on every scope. If you were to $broadcast from your controller's scope, for example, it would fire only in the scopes that inherit from your controller scope. $emit goes the opposite direction and behaves similarly to a DOM event in that it bubbles up the scope chain.
Keep in mind that there are scenarios where $broadcast makes a lot of sense, and there are scenarios where $watch is a better option - especially if in an isolate scope with a very specific watch expression.
I'm using similar approach as @dtheodot but using angular promise instead of passing callbacks
app.service('myService', function($q) {
var self = this,
defer = $q.defer();
this.foo = 0;
this.observeFoo = function() {
return defer.promise;
}
this.setFoo = function(foo) {
self.foo = foo;
defer.notify(self.foo);
}
})
Then wherever just use myService.setFoo(foo)
method to update foo
on service. In your controller you can use it as:
myService.observeFoo().then(null, null, function(foo){
$scope.foo = foo;
})
First two arguments of then
are success and error callbacks, third one is notify callback.
As far as I can tell, you dont have to do something as elaborate as that. You have already assigned foo from the service to your scope and since foo is an array ( and in turn an object it is assigned by reference! ). So, all that you need to do is something like this :
function FooCtrl($scope, aService) {
$scope.foo = aService.foo;
}
If some, other variable in this same Ctrl is dependant on foo changing then yes, you would need a watch to observe foo and make changes to that variable. But as long as it is a simple reference watching is unnecessary. Hope this helps.
Without watches or observer callbacks (http://jsfiddle.net/zymotik/853wvv7s/):
JavaScript:
angular.module("Demo", [])
.factory("DemoService", function($timeout) {
var DemoService = function() {
var self = this;
self.count = 0;
self.counter = function(){
self.count++;
$timeout(self.counter, 1000);
}
self.counter();
}
var demoService = new DemoService();
return demoService;
})
.controller("DemoController", function($scope, DemoService) {
$scope.service = DemoService;
});
HTML
<div ng-app="Demo" ng-controller="DemoController">
<div>
<h4>{{service.name}}</h4>
<p>Count: {{service.count}}</p>
</div>
</div>
Worked for me. This guy explained it well and with nice demo. http://stsc3000.github.io/blog/2013/10/26/a-tale-of-frankenstein-and-binding-to-service-values-in-angular-dot-js/
You can insert the service in $rootScope and watch:
myApp.run(function($rootScope, aService){
$rootScope.aService = aService;
$rootScope.$watch('aService', function(){
alert('Watch');
}, true);
});
In your controller:
myApp.controller('main', function($scope){
$scope.aService.foo = 'change';
});
Other option is to use a external library like: https://github.com/melanke/Watch.JS
Works with: IE 9+, FF 4+, SF 5+, WebKit, CH 7+, OP 12+, BESEN, Node.JS , Rhino 1.7+
You can observe the changes of one, many or all object attributes.
Example:
var ex3 = {
attr1: 0,
attr2: "initial value of attr2",
attr3: ["a", 3, null]
};
watch(ex3, function(){
alert("some attribute of ex3 changes!");
});
ex3.attr3.push("new value");
You can watch the changes within the factory itself and then broadcast a change
angular.module('MyApp').factory('aFactory', function ($rootScope) {
// Define your factory content
var result = {
'key': value
};
// add a listener on a key
$rootScope.$watch(function () {
return result.key;
}, function (newValue, oldValue, scope) {
// This is called after the key "key" has changed, a good idea is to broadcast a message that key has changed
$rootScope.$broadcast('aFactory:keyChanged', newValue);
}, true);
return result;
});
Then in your controller:
angular.module('MyApp').controller('aController', ['$rootScope', function ($rootScope) {
$rootScope.$on('aFactory:keyChanged', function currentCityChanged(event, value) {
// do something
});
}]);
In this manner you put all the related factory code within its description then you can only rely on the broadcast from outside
Building on dtheodor's answer you could use something similar to the below to ensure that you don't forget to unregister the callback... Some may object to passing the $scope
to a service though.
factory('aService', function() {
var observerCallbacks = [];
/**
* Registers a function that will be called when
* any modifications are made.
*
* For convenience the callback is called immediately after registering
* which can be prevented with `preventImmediate` param.
*
* Will also automatically unregister the callback upon scope destory.
*/
this.registerObserver = function($scope, cb, preventImmediate){
observerCallbacks.push(cb);
if (preventImmediate !== true) {
cb();
}
$scope.$on('$destroy', function () {
observerCallbacks.remove(cb);
});
};
function notifyObservers() {
observerCallbacks.forEach(function (cb) {
cb();
});
};
this.foo = someNgResource.query().$then(function(){
notifyObservers();
});
});
Array.remove is an extension method which looks like this:
/**
* Removes the given item the current array.
*
* @param {Object} item The item to remove.
* @return {Boolean} True if the item is removed.
*/
Array.prototype.remove = function (item /*, thisp */) {
var idx = this.indexOf(item);
if (idx > -1) {
this.splice(idx, 1);
return true;
}
return false;
};
Here's my generic approach.
mainApp.service('aService',[function(){
var self = this;
var callbacks = {};
this.foo = '';
this.watch = function(variable, callback) {
if (typeof(self[variable]) !== 'undefined') {
if (!callbacks[variable]) {
callbacks[variable] = [];
}
callbacks[variable].push(callback);
}
}
this.notifyWatchersOn = function(variable) {
if (!self[variable]) return;
if (!callbacks[variable]) return;
angular.forEach(callbacks[variable], function(callback, key){
callback(self[variable]);
});
}
this.changeFoo = function(newValue) {
self.foo = newValue;
self.notifyWatchersOn('foo');
}
}]);
In Your Controller
function FooCtrl($scope, aService) {
$scope.foo;
$scope._initWatchers = function() {
aService.watch('foo', $scope._onFooChange);
}
$scope._onFooChange = function(newValue) {
$scope.foo = newValue;
}
$scope._initWatchers();
}
FooCtrl.$inject = ['$scope', 'aService'];
I came to this question but it turned out my problem was that I was using setInterval when I should have been using the angular $interval provider. This is also the case for setTimeout (use $timeout instead). I know it's not the answer to the OP's question, but it might help some, as it helped me.
A wee bit ugly, but I've added registration of scope variables to my service for a toggle:
myApp.service('myService', function() {
var self = this;
self.value = false;
self.c2 = function(){};
self.callback = function(){
self.value = !self.value;
self.c2();
};
self.on = function(){return self.value;};
self.register = function(obj, key){
self.c2 = function(){
obj[key] = self.value;
obj.$apply();
}
};
return this;});
And then in the controller:
function MyCtrl($scope, myService) {
$scope.name = 'Superhero';
$scope.myVar = false;
myService.register($scope, 'myVar');}
For those like me just looking for a simple solution, this does almost exactly what you expect from using normal $watch in controllers. The only difference is, that it evaluates the string in it's javascript context and not on a specific scope. You'll have to inject $rootScope into your service, although it is only used to hook into the digest cycles properly.
function watch(target, callback, deep) {
$rootScope.$watch(function () {return eval(target);}, callback, deep);
};
while facing a very similar issue I watched a function in scope and had the function return the service variable. I have created a js fiddle. you can find the code below.
var myApp = angular.module("myApp",[]);
myApp.factory("randomService", function($timeout){
var retValue = {};
var data = 0;
retValue.startService = function(){
updateData();
}
retValue.getData = function(){
return data;
}
function updateData(){
$timeout(function(){
data = Math.floor(Math.random() * 100);
updateData()
}, 500);
}
return retValue;
});
myApp.controller("myController", function($scope, randomService){
$scope.data = 0;
$scope.dataUpdated = 0;
$scope.watchCalled = 0;
randomService.startService();
$scope.getRandomData = function(){
return randomService.getData();
}
$scope.$watch("getRandomData()", function(newValue, oldValue){
if(oldValue != newValue){
$scope.data = newValue;
$scope.dataUpdated++;
}
$scope.watchCalled++;
});
});
I have found a really great solution on the other thread with a similar problem but totally different approach. Source: $watch within directive is not working when $rootScope value is changed
Basically the solution there tells NOT TO use $watch
as it is very heavy solution. Instead they propose to use $emit
and $on
.
My problem was to watch a variable in my service and react in directive. And with the above method it very easy!
My module/service example:
angular.module('xxx').factory('example', function ($rootScope) {
var user;
return {
setUser: function (aUser) {
user = aUser;
$rootScope.$emit('user:change');
},
getUser: function () {
return (user) ? user : false;
},
...
};
});
So basically I watch my user
- whenever it is set to new value I $emit
a user:change
status.
Now in my case, in the directive I used:
angular.module('xxx').directive('directive', function (Auth, $rootScope) {
return {
...
link: function (scope, element, attrs) {
...
$rootScope.$on('user:change', update);
}
};
});
Now in the directive I listen on the $rootScope
and on the given change - I react respectively. Very easy and elegant!