AngularJS : How to watch service variables?
Solution 1
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
};
Solution 2
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.
Solution 3
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.
Solution 4
Without watches or observer callbacks (http://jsfiddle.net/zymotik/853wvv7s/):
JavaScript:
angular.module("Demo", [])
.factory("DemoService", function($timeout) {
function DemoService() {
var self = this;
self.name = "Demo Service";
self.count = 0;
self.counter = function(){
self.count++;
$timeout(self.counter, 1000);
}
self.addOneHundred = function(){
self.count+=100;
}
self.counter();
}
return new DemoService();
})
.controller("DemoController", function($scope, DemoService) {
$scope.service = DemoService;
$scope.minusOneHundred = function() {
DemoService.count -= 100;
}
});
HTML
<div ng-app="Demo" ng-controller="DemoController">
<div>
<h4>{{service.name}}</h4>
<p>Count: {{service.count}}</p>
</div>
</div>
This JavaScript works as we are passing an object back from the service rather than a value. When a JavaScript object is returned from a service, Angular adds watches to all of its properties.
Also note that I am using 'var self = this' as I need to keep a reference to the original object when the $timeout executes, otherwise 'this' will refer to the window object.
Solution 5
I stumbled upon this question looking for something similar, but I think it deserves a thorough explanation of what's going on, as well as some additional solutions.
When an angular expression such as the one you used is present in the HTML, Angular automatically sets up a $watch
for $scope.foo
, and will update the HTML whenever $scope.foo
changes.
<div ng-controller="FooCtrl">
<div ng-repeat="item in foo">{{ item }}</div>
</div>
The unsaid issue here is that one of two things are affecting aService.foo
such that the changes are undetected. These two possibilities are:
-
aService.foo
is getting set to a new array each time, causing the reference to it to be outdated. -
aService.foo
is being updated in such a way that a$digest
cycle is not triggered on the update.
Problem 1: Outdated References
Considering the first possibility, assuming a $digest
is being applied, if aService.foo
was always the same array, the automatically set $watch
would detect the changes, as shown in the code snippet below.
Solution 1-a: Make sure the array or object is the same object on each update
angular.module('myApp', [])
.factory('aService', [
'$interval',
function($interval) {
var service = {
foo: []
};
// Create a new array on each update, appending the previous items and
// adding one new item each time
$interval(function() {
if (service.foo.length < 10) {
var newArray = []
Array.prototype.push.apply(newArray, service.foo);
newArray.push(Math.random());
service.foo = newArray;
}
}, 1000);
return service;
}
])
.factory('aService2', [
'$interval',
function($interval) {
var service = {
foo: []
};
// Keep the same array, just add new items on each update
$interval(function() {
if (service.foo.length < 10) {
service.foo.push(Math.random());
}
}, 1000);
return service;
}
])
.controller('FooCtrl', [
'$scope',
'aService',
'aService2',
function FooCtrl($scope, aService, aService2) {
$scope.foo = aService.foo;
$scope.foo2 = aService2.foo;
}
]);
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body ng-app="myApp">
<div ng-controller="FooCtrl">
<h1>Array changes on each update</h1>
<div ng-repeat="item in foo">{{ item }}</div>
<h1>Array is the same on each udpate</h1>
<div ng-repeat="item in foo2">{{ item }}</div>
</div>
</body>
</html>
As you can see, the ng-repeat supposedly attached to aService.foo
does not update when aService.foo
changes, but the ng-repeat attached to aService2.foo
does. This is because our reference to aService.foo
is outdated, but our reference to aService2.foo
is not. We created a reference to the initial array with $scope.foo = aService.foo;
, which was then discarded by the service on it's next update, meaning $scope.foo
no longer referenced the array we wanted anymore.
However, while there are several ways to make sure the initial reference is kept in tact, sometimes it may be necessary to change the object or array. Or what if the service property references a primitive like a String
or Number
? In those cases, we cannot simply rely on a reference. So what can we do?
Several of the answers given previously already give some solutions to that problem. However, I am personally in favor of using the simple method suggested by Jin and thetallweeks in the comments:
just reference aService.foo in the html markup
Solution 1-b: Attach the service to the scope, and reference {service}.{property}
in the HTML.
Meaning, just do this:
HTML:
<div ng-controller="FooCtrl">
<div ng-repeat="item in aService.foo">{{ item }}</div>
</div>
JS:
function FooCtrl($scope, aService) {
$scope.aService = aService;
}
angular.module('myApp', [])
.factory('aService', [
'$interval',
function($interval) {
var service = {
foo: []
};
// Create a new array on each update, appending the previous items and
// adding one new item each time
$interval(function() {
if (service.foo.length < 10) {
var newArray = []
Array.prototype.push.apply(newArray, service.foo);
newArray.push(Math.random());
service.foo = newArray;
}
}, 1000);
return service;
}
])
.controller('FooCtrl', [
'$scope',
'aService',
function FooCtrl($scope, aService) {
$scope.aService = aService;
}
]);
<!DOCTYPE html>
<html>
<head>
<script data-require="[email protected]" data-semver="1.4.7" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body ng-app="myApp">
<div ng-controller="FooCtrl">
<h1>Array changes on each update</h1>
<div ng-repeat="item in aService.foo">{{ item }}</div>
</div>
</body>
</html>
That way, the $watch
will resolve aService.foo
on each $digest
, which will get the correctly updated value.
This is kind of what you were trying to do with your workaround, but in a much less round about way. You added an unnecessary $watch
in the controller which explicitly puts foo
on the $scope
whenever it changes. You don't need that extra $watch
when you attach aService
instead of aService.foo
to the $scope
, and bind explicitly to aService.foo
in the markup.
Now that's all well and good assuming a $digest
cycle is being applied. In my examples above, I used Angular's $interval
service to update the arrays, which automatically kicks off a $digest
loop after each update. But what if the service variables (for whatever reason) aren't getting updated inside the "Angular world". In other words, we dont have a $digest
cycle being activated automatically whenever the service property changes?
Problem 2: Missing $digest
Many of the solutions here will solve this issue, but I agree with Code Whisperer:
The reason why we're using a framework like Angular is to not cook up our own observer patterns
Therefore, I would prefer to continue to use the aService.foo
reference in the HTML markup as shown in the second example above, and not have to register an additional callback within the Controller.
Solution 2: Use a setter and getter with $rootScope.$apply()
I was surprised no one has yet suggested the use of a setter and getter. This capability was introduced in ECMAScript5, and has thus been around for years now. Of course, that means if, for whatever reason, you need to support really old browsers, then this method will not work, but I feel like getters and setters are vastly underused in JavaScript. In this particular case, they could be quite useful:
factory('aService', [
'$rootScope',
function($rootScope) {
var realFoo = [];
var service = {
set foo(a) {
realFoo = a;
$rootScope.$apply();
},
get foo() {
return realFoo;
}
};
// ...
}
angular.module('myApp', [])
.factory('aService', [
'$rootScope',
function($rootScope) {
var realFoo = [];
var service = {
set foo(a) {
realFoo = a;
$rootScope.$apply();
},
get foo() {
return realFoo;
}
};
// Create a new array on each update, appending the previous items and
// adding one new item each time
setInterval(function() {
if (service.foo.length < 10) {
var newArray = [];
Array.prototype.push.apply(newArray, service.foo);
newArray.push(Math.random());
service.foo = newArray;
}
}, 1000);
return service;
}
])
.controller('FooCtrl', [
'$scope',
'aService',
function FooCtrl($scope, aService) {
$scope.aService = aService;
}
]);
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body ng-app="myApp">
<div ng-controller="FooCtrl">
<h1>Using a Getter/Setter</h1>
<div ng-repeat="item in aService.foo">{{ item }}</div>
</div>
</body>
</html>
Here I added a 'private' variable in the service function: realFoo
. This get's updated and retrieved using the get foo()
and set foo()
functions respectively on the service
object.
Note the use of $rootScope.$apply()
in the set function. This ensures that Angular will be aware of any changes to service.foo
. If you get 'inprog' errors see this useful reference page, or if you use Angular >= 1.3 you can just use $rootScope.$applyAsync()
.
Also be wary of this if aService.foo
is being updated very frequently, since that could significantly impact performance. If performance would be an issue, you could set up an observer pattern similar to the other answers here using the setter.
berto
Updated on August 28, 2020Comments
-
berto over 3 years
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?
-
berto over 11 yearsThanks for the response. If the variable in the service is a "primitive" instead of an object, like a string, would that, in turn, require a
$watch
? -
Mark Rajcok over 11 yearsI tried, and I couldn't get
$watch
to work with a primitive. Instead, I defined a method on the service that would return the primitive value:somePrimitive() = function() { return somePrimitive }
And I assigned a $scope property to that method:$scope.somePrimitive = aService.somePrimitive;
. Then I used the scope method in the HTML:<span>{{somePrimitive()}}</span>
-
niklr over 10 yearsHow would you remove the registered callback after the user leaves the page with the FooCtrl?
-
Jamie over 10 years@Moo listen for the
$destory
event on the scope and add an unregister method toaService
-
Jimmy Kane over 10 years@MarkRajcok No don't use primitives. Add them in a object. Primitives are not mutables and so 2way data binding will not work
-
Mark Rajcok over 10 years@JimmyKane, yes, primitives should not be used for 2-way databinding, but I think the question was about watching service variables, not setting up 2-way binding. If you only need to watch a service property/variable, an object is not required -- a primitive can be used.
-
JNissi over 10 yearsCorrect me if I'm wrong, but you should $scope.$apply the change in the observer callback in the controller. At least I had to, because all the changes to my service come through events from a flash.
-
XML about 10 yearsGetting out of the $digest cycle is a Good Thing, especially if the changes you're watching aren't a value that will directly and immediately go into the DOM.
-
osjerick about 10 yearsWhat are pros of this solution? It needs more code in a service, and somewhat the same amount of code in a controller (since we also need to unregister on $destroy). I could say for execution speed, but in most cases it just won't matter.
-
Jin about 10 yearsnot sure how this is a better solution than $watch, the questioner was asking for a simple way of sharing the data, it looks even more cumbersome. I would rather use $broadcast than this
-
dtheodor about 10 years
$watch
vs observer pattern is simply choosing whether to poll or to push, and is basically a matter of performance, so use it when performance matters. I use observer pattern when otherwise I would have to "deep" watch complex objects. I attach whole services to the $scope instead of watching single service values. I avoid angular's $watch like the devil, there is enough of that happening in directives and in native angular data-binding. -
JerryKur almost 10 yearsIs there away to avoid .save() method. Seems like overkill when you are just monitoring the update of a single variable in the sharedService. Can we watch the variable from within the sharedService and broadcast when it changes?
-
Adam almost 10 yearsI would suggest this solution even when performance is not so much an issue because the behavior is explicitly defined in the code and doesn't depend on library magic. Good answer!
-
Code Whisperer almost 10 yearsThe reason why we're using a framework like Angular is to not cook up our own observer patterns.
-
Fabio over 9 yearsWhat would be the advantage of this method over the $broadcast described bellow by Matt Pileggi?
-
Krym over 9 yearsWell both methods have their uses. Advantages of broadcast for me would be human readability and possibility to listen on more places to the same event. I guess the main disadvantage is that broadcast is emiting message to all descendant scopes so it may be a performance issue.
-
Seiyria over 9 yearsI was having a problem where doing
$scope.$watch
on a service variable wasn't seeming to work (the scope I was watching on was a modal that inherited from$rootScope
) - this worked. Cool trick, thanks for sharing! -
shrekuu over 9 yearsThanks. A little question: why do you return
this
from that service instead ofself
? -
nclu over 9 yearsBecause mistakes get made sometimes. ;-)
-
Ouwen Huang over 9 yearsIn this set up I am able to change aService values from the scope. But the scope does not change in response the aService changing.
-
Darwin Tech over 9 yearsThis also doesn't work for me. Simply assigning
$scope.foo = aService.foo
does not update the scope variable automatically. -
Abris over 9 yearsHow would you clean up after yourself with this approach? Is it possible to remove the registered callback from the promise when the scope is destroyed?
-
Krym over 9 yearsGood question. I honestly don't know. I'll try to do some tests on how could you remove notify callback from promise.
-
lostintranslation about 9 yearsCorrect you cannot assign a scope variable from a Service variable and then $watch('foo' ,... I am using angular 1.3 to test and the watch does not get fired when the service variable changes
-
Cody about 9 yearsI don't find this too amazing -- apologies if I seem crude -- but this is a simple
getSet
approach. Either use anObject.defineProperties(object, ...)
or if you're looking for apubSub
methodology, you may as well use$rootScope.$on('changed:someData', setSomeData)
&$rootScope.$broadcast('changed:someData', someData)
. IMAGINARY POINTS OFF for this answer :( -
Cody about 9 yearsGood practice to
return this;
out of your constructors ;-) -
Cody about 9 yearsI CAN'T BELIEVE THIS ANSWER IS NOT THE TOP-MOST-VOTED!!! This is the most elegant solution (IMO) as it reduces informational-entropy & probably mitigates the need for additional Mediation handlers. I would vote this up more if I could...
-
abettermap about 9 yearsI tried quite a few ways to share data between controllers, but this is the only one that worked. Well played, sir.
-
Zymotik about 9 yearsAdding all your services to the $rootScope, its benefits and it's potential pitfalls, are detailed somewhat here: stackoverflow.com/questions/14573023/…
-
maxime1992 about 9 yearsI tried few times before ... I knew there was a better than using scope but i did not succeed when i tried it. This evening i finally did it, so awesome :) !!! You sir deserves more upvotes !
-
JacobF about 9 yearsThis is a great approach! Is there a way to bind just a property of a service to the scope instead of the entire service? Just doing
$scope.count = service.count
doesn't work. -
superluminary about 9 yearsYou need to have a very good use case to justify reimplementing events in Angular.
-
JMK almost 9 yearsI prefer this to the other answers, seems less hacky, thanks
-
Charles almost 9 yearsThis is the correct design pattern only if your consuming controller has multiple possible sources of the data; in other words, if you have a MIMO situation (Multiple Input/Multiple Output). If you're just using a one-to-many pattern, you should be using direct object referencing and letting the Angular framework do the two-way binding for you. Horkyze linked this below, and it's a good explanation of the automatic two-way binding, and it's limitations: stsc3000.github.io/blog/2013/10/26/…
-
Alex Ross over 8 yearsYou could also nest the property inside of an (arbitrary) object so that it's passed by reference.
$scope.data = service.data
<p>Count: {{ data.count }}</p>
-
CodeMoose over 8 yearsExcellent approach! While there are a lot of strong, functional answers on this page, this is by far a) the easiest to implement, and b) the easiest to understand when reading over the code. This answer should be a lot higher than it currently is.
-
Zymotik over 8 yearsThanks @CodeMoose, I've simplified it even further today for those new to AngularJS/JavaScript.
-
Afflatus over 8 yearscould you please tell me what is the advantage of having the line
var self = this;
? why not just usethis
-
Zymotik over 8 yearsHi @Afflatus, self is being used to maintain a reference to the original this. Please see stackoverflow.com/questions/962033/…
-
magnetronnie over 8 yearsYou can use
setTimeout
, or any other non-Angular function, but just don't forget to wrap the code in the callback with$scope.$apply()
. -
sarunast over 8 yearsIn my opinion you are confusing people with
self
right here. There is no point using them for creating function becausethis
andself
are referring to the same object. It makes sense to useself
only inside the function and then again it is not necessary to use it because you can use.bind(this)
and when you have a case when you can't use.bind()
only then you would useself
. -
Zymotik over 8 yearsThank-you for your comments @Stamy. Please can you demonstrate a simpler approach in a jsFiddle? We can then compare and update the example if needed. We do require a reference to the original
this
when using the$timeout
function. -
sarunast over 8 yearsHere is a link with a code: jsbin.com/guwogo/edit?js,console . I added two approaches, I am fine with either approach. But I would never use
self
for creating everything. It might be good if you don't understand howthis
and scopes works but I have never seen anything like this in any decent library. -
Sarpdoruk Tahmaz over 8 yearsThis is the correct, and easiest solution. As @NanoWizard say, $digest watches for
services
not for properties that belong to the service itself. -
ceebreenk about 8 years@lostintranslation what was your solution for 1.3?
-
Patrik Beck almost 8 yearsAren't we missing 'return this;' at the end of service definition?
-
Lee Goddard almost 8 yearsNot very Angular, compared to stackoverflow.com/a/20863597/418150
-
jedi almost 8 yearsi like the PostmanService but how i must change $watch function on controller if i need to listen more than one variable?
-
hayatbiralem almost 8 yearsHi jedi, thanks for the heads up! I updated the pen and the answer. I recomment to add another watch function for that. So, I added a new feature to PostmanService. I hope this helps :)
-
hayatbiralem over 7 yearsActually, yes it is :) If you share more detail of the problem maybe I can help you.
-
Fortuna over 7 years@CodeWhisperer why would we use angulars $broadcast here? Why would I broadcast an event which is probably meant for just a single controller rather than the whole app? And why shouldn't we use a well known pattern if it would fit in the case?
-
NoobSter over 7 years@Zymotik , So, you would still need $watch or $broadcast ,though, when displaying the service data on more than one controller at the same time? jsfiddle.net/qqejytbz/1
-
Zymotik over 7 years@NoobSter no, you would not
-
NoobSter over 7 years@Zymotik , Thanks for the reply. I know this is old. In my fiddle posted above, (forked from your example) the count is not able to update in both controllers, unless I use a $watch. What is the correct way to do this? Fiddle: jsfiddle.net/qqejytbz/1
-
NoobSter over 7 yearsi moved this to it's own question if you are interested: stackoverflow.com/questions/41786796/…
-
Zymotik over 7 years@noobster please see the first two comments, as I believe this answers your question.
-
João Pereira about 7 yearsI can't understand the upvotes since this doesn't work. Even if in the service I have an object, its value is not automatically updated in the controller.
-
Norbert Norbertson over 6 yearsAll this from a "framework" that was supposed to make our lives easier! No wonder they started again! LOL.
-
undefined about 6 yearswhy it was downvoted.. I have also used similar technique many times and it has worked.
-
Manuel Graf over 5 years$watch and $rootScope.$emit are both quite expensive as they have to traverse the whole scope, right? As I am working a lot with really cpu expensive frontend, this matters to me and I also already used it in 1.4. Observer pattern still is a direct connection and nothing has to be traversed, which for high n it might be faster. Especially if there already a lot of rootScope listeners? I didnt benchmark but thats how i understand it. if you have other information, please enlighten me!
-
Mrl0330 almost 5 years@CodeWhisperer 's upvoted response shouldn't be so upvoted. While true, it completely ignores the rationale of creating your own observer pattern in a service, i.e. performance. If you've ever used watchers in an angularjs application you know how quickly they can degrade performance. A lighter weight approach like this surely has some applicability.
-
Amna almost 5 yearsMay God bless you. I have wasted like million hours I would say. Because I was struggling with 1.5 and angularjs shifting from 1 to 2 and also wanted to share data
-
Oguzhan over 3 yearsconsider $watch works with the methodology of long polling where as observer pattern calls method(s) only once (with right garbage collecting implementation) when job is done. I think observer pattern leads far beyond in performance-wise in my opinion.