Watch multiple $scope attributes
Asked Answered
S

11

457

Is there a way to subscribe to events on multiple objects using $watch

E.g.

$scope.$watch('item1, item2', function () { });
Schwerin answered 14/8, 2012 at 12:49 Comment(0)
G
603

Starting from AngularJS 1.3 there's a new method called $watchGroup for observing a set of expressions.

$scope.foo = 'foo';
$scope.bar = 'bar';

$scope.$watchGroup(['foo', 'bar'], function(newValues, oldValues, scope) {
  // newValues array contains the current values of the watch expressions
  // with the indexes matching those of the watchExpression array
  // i.e.
  // newValues[0] -> $scope.foo 
  // and 
  // newValues[1] -> $scope.bar 
});
Gringo answered 6/5, 2014 at 12:51 Comment(4)
what's the difference against $watchCollection('[a, b]') ??Barometer
The difference is that you can use a proper array instead of a string that looks like an arrayGringo
I had a similar problem but using controllerAs pattern. In my case the fix consisted in prepend to the variables the name of the controller: $scope.$watchGroup(['mycustomctrl.foo', 'mycustomctrl.bar'],...Alroy
This doesn't seem to work for me on Angular 1.6. If I put a console log on the function, I can see that it just runs once with newValues and oldValues as undefined, while individually watching the properties work as usual.Narcotize
L
292

Beginning with AngularJS 1.1.4 you can use $watchCollection:

$scope.$watchCollection('[item1, item2]', function(newValues, oldValues){
    // do stuff here
    // newValues and oldValues contain the new and respectively old value
    // of the observed collection array
});

Plunker example here

Documentation here

Lineation answered 20/6, 2013 at 22:25 Comment(5)
Note that the first parameter is not an array. It's a string that looks like an array.Outstare
thanks for this. if it works for me, I will give you a thousand gold coins - virtual ones, of course :)Decomposer
To be clear (took me a few minutes to realize) $watchCollection is intended to be handed an object and provide a watch for changes to any of the object's properties, whereas $watchGroup is intended to be handed an array of individual properties to watch for changes to. Slightly different to tackle similar yet different problems. Phew!Armhole
Somehow, newValues and oldValues are always equal when you use this method: plnkr.co/edit/SwwdpQcNf78pQ80hhXMrDecoction
Thanks. It solved my problem. Is there any performance hit/issue of using $watch?Steelman
S
118

$watch first parameter can also be a function.

$scope.$watch(function watchBothItems() {
  return itemsCombinedValue();
}, function whenItemsChange() {
  //stuff
});

If your two combined values are simple, the first parameter is just an angular expression normally. For example, firstName and lastName:

$scope.$watch('firstName + lastName', function() {
  //stuff
});
Scorpio answered 14/8, 2012 at 13:4 Comment(6)
This seems like a use-case that's crying out for inclusion in the framework by default. Even a computed property as simple as fullName = firstName + " " + lastName requires passing a function as the first argument (rather than a simple expression like 'firstName, lastName'). Angular is SO powerful, but there are some areas where it can be enormously more developer-friendly in ways that don't (seem to) compromise performance or design principles.Mononucleosis
Well $watch just takes an angular expression, which is evaluated on the scope it's called on. So you could do $scope.$watch('firstName + lastName', function() {...}). You could also just do two watches: function nameChanged() {}; $scope.$watch('firstName', nameChanged); $scope.$watch('lastName', nameChanged);. I added the simple case to the answer.Scorpio
The plus in 'firstName + lastName' is string concatenation. So angular just checks whether the result of the concatenation is different now.Haploid
String concatenation requires a little care - if you change "paran orman" to "para norman" than you won't trigger a response; best to put a divider like "\t|\t" between the first and second term, or just stick to JSON which is definitiveDulcedulcea
although amberjs sucks, but it has this feature build in! hear that angular guys. I guess doing to watches is the most rational doable way.Popper
This is brilliant! You could also rewrite it as: $scope.$watch(itemsCombinedValue, function whenItemsChange() { //stuff }); Where itemsCombined is a function that returns a value. It's a little more readable.Abhor
P
72

Here's a solution very similar to your original pseudo-code that actually works:

$scope.$watch('[item1, item2] | json', function () { });

EDIT: Okay, I think this is even better:

$scope.$watch('[item1, item2]', function () { }, true);

Basically we're skipping the json step, which seemed dumb to begin with, but it wasn't working without it. They key is the often omitted 3rd parameter which turns on object equality as opposed to reference equality. Then the comparisons between our created array objects actually work right.

Periodicity answered 19/1, 2013 at 1:5 Comment(5)
I had a similar question. And this is the correct answer for me.Hedgcock
Both have a slight performance overhead if the items in the array expression aren't simple types.Caelian
That is true.. it's doing a full depth comparison of the component objects. Which may or may not be what you want. See the currently approved answer for a version using modern angular that doesn't do a full depth comparison.Periodicity
For some reason, when I was trying to use the $watchCollection over an array, I got this error TypeError: Object #<Object> has no method '$watchCollection' but this solution helps me to solve my problem !Stratfordonavon
Cool, you can even watch values within objects: $scope.$watch('[chaptercontent.Id, anchorid]', function (newValues, oldValues) { ... }, true);Territorial
B
15

You can use functions in $watchGroup to select fields of an object in scope.

        $scope.$watchGroup(
        [function () { return _this.$scope.ViewModel.Monitor1Scale; },   
         function () { return _this.$scope.ViewModel.Monitor2Scale; }],  
         function (newVal, oldVal, scope) 
         {
             if (newVal != oldVal) {
                 _this.updateMonitorScales();
             }
         });
Brazenfaced answered 20/5, 2015 at 23:57 Comment(2)
Why do you use _this? Doesn't the scope get carried down into the functions without explicitly defining it?Kast
I use typescript, the presented code is compiled result.Brazenfaced
D
12

Why not simply wrap it in a forEach?

angular.forEach(['a', 'b', 'c'], function (key) {
  scope.$watch(key, function (v) {
    changed();
  });
});

It's about the same overhead as providing a function for the combined value, without actually having to worry about the value composition.

Drayman answered 12/5, 2013 at 14:31 Comment(6)
In this case log() would be executed several times, right? I think in case of nested $watch functions it wouldn't. And it should not in the solution for watching several attributes at once.Heyes
No, log is only executed once per change per watched property. Why would it be invoked multiple times?Drayman
Right, I mean it wouldn't work nice if we want to watch all of them simultaneously.Heyes
Sure why not? I do it that way in a project of mine. changed() is invoked whenever something changes in either of the properties. That's the exact same behavior as if a function for the combined value was provided.Drayman
It's slightly different in that if multiple of the items in the array change at the same time the $watch will only fire the handler once instead of once per item changed.Disenchant
With this method, if all of the items changed in a single tick, changed() would be called three times. With something like $watchGroup (or writing a function which combines the different values into a key), it would only be called once.Falkirk
L
11

A slightly safer solution to combine values might be to use the following as your $watch function:

function() { return angular.toJson([item1, item2]) }

or

$scope.$watch(
  function() {
    return angular.toJson([item1, item2]);
  },
  function() {
    // Stuff to do after either value changes
  });
Lindsaylindsey answered 2/10, 2012 at 8:16 Comment(0)
H
5

$watch first parameter can be angular expression or function. See documentation on $scope.$watch. It contains a lot of useful info about how $watch method works: when watchExpression is called, how angular compares results, etc.

Hairpiece answered 14/8, 2012 at 13:35 Comment(0)
A
4

how about:

scope.$watch(function() { 
   return { 
      a: thing-one, 
      b: thing-two, 
      c: red-fish, 
      d: blue-fish 
   }; 
}, listener...);
Amourpropre answered 12/11, 2012 at 19:45 Comment(2)
Without the third true parameter to to scope.$watch (as in your example), listener() would fire every single time, because the anonymous hash has a different reference every time.Winy
I found this solution worked for my situation. For me, it was more like: return { a: functionA(), b: functionB(), ... }. I then check the returned objects of the new and old values against each other.Zone
R
4
$scope.$watch('age + name', function () {
  //called when name or age changed
});

Here function will get called when both age and name value get changed.

Rhino answered 16/4, 2015 at 19:21 Comment(1)
Also be sure in this case not use the newValue and oldValue arguments that angular passes into the callback because they will be the resulting concatenation and not just the field that was changedRhino
D
1

Angular introduced $watchGroup in version 1.3 using which we can watch multiple variables, with a single $watchGroup block $watchGroup takes array as first parameter in which we can include all of our variables to watch.

$scope.$watchGroup(['var1','var2'],function(newVals,oldVals){
   console.log("new value of var1 = " newVals[0]);
   console.log("new value of var2 = " newVals[1]);
   console.log("old value of var1 = " oldVals[0]);
   console.log("old value of var2 = " oldVals[1]);
});
Displant answered 2/4, 2018 at 8:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.