457

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

E.g.

$scope.$watch('item1, item2', function () { });
Mosh Feu
  • 28,354
  • 16
  • 88
  • 135
Greg
  • 31,180
  • 18
  • 65
  • 85

11 Answers11

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 
});
Paolo Moretti
  • 54,162
  • 23
  • 101
  • 92
  • 7
    what's the difference against $watchCollection('[a, b]') ?? – Kamil Tomšík Jan 12 '15 at 15:14
  • 30
    The difference is that you can use a proper array instead of a string that looks like an array – Paolo Moretti Feb 25 '15 at 15:31
  • 2
    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'],...` – morels Oct 15 '15 at 14:26
  • 1
    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. – Alejandro García Iglesias Apr 03 '17 at 20:36
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

Răzvan Flavius Panda
  • 21,730
  • 17
  • 111
  • 169
  • 109
    Note that the first parameter is **not** an array. It's a string that looks like an array. – M.K. Safi Sep 22 '13 at 16:33
  • 1
    thanks for this. if it works for me, I will give you a thousand gold coins - virtual ones, of course :) – AdityaSaxena Aug 20 '14 at 13:47
  • 5
    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! – Mattygabe Feb 28 '15 at 01:26
  • 1
    Somehow, newValues and oldValues are always equal when you use this method: http://plnkr.co/edit/SwwdpQcNf78pQ80hhXMr – mostruash May 04 '16 at 09:45
  • Thanks. It solved my problem. Is there any performance hit/issue of using $watch? – Syed Nasir Abbas Jul 03 '17 at 00:18
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
});
Andrew Joslin
  • 43,033
  • 21
  • 100
  • 75
  • 8
    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. – XML Aug 24 '12 at 21:27
  • 4
    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. – Andrew Joslin Aug 24 '12 at 22:17
  • The plus in 'firstName + lastName' is string concatenation. So angular just checks whether the result of the concatenation is different now. – mb21 Sep 05 '12 at 22:11
  • 16
    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 definitive – Dave Edelhart Oct 07 '12 at 19:05
  • 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. – Aladdin Mhemed Oct 24 '12 at 06:43
  • 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. – Stone Nov 07 '12 at 21:30
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.

Karen Zilles
  • 7,633
  • 3
  • 34
  • 33
  • I had a similar question. And this is the correct answer for me. – Calvin Cheng May 31 '13 at 03:47
  • Both have a slight performance overhead if the items in the array expression aren't simple types. – null Aug 18 '13 at 10:10
  • 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. – Karen Zilles Aug 26 '13 at 20:48
  • For some reason, when I was trying to use the $watchCollection over an array, I got this error `TypeError: Object # has no method '$watchCollection'` but this solution helps me to solve my problem ! – abottoni Sep 10 '13 at 08:05
  • Cool, you can even watch values within objects: `$scope.$watch('[chaptercontent.Id, anchorid]', function (newValues, oldValues) { ... }, true);` – Serge van den Oever Jan 28 '14 at 10:30
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();
             }
         });
Yang Zhang
  • 4,540
  • 4
  • 37
  • 34
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.

Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
  • 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. – John Doe May 13 '13 at 06:58
  • No, log is only executed once per change per watched property. Why would it be invoked multiple times? –  May 13 '13 at 14:18
  • Right, I mean it wouldn't work nice if we want to watch all of them simultaneously. – John Doe May 13 '13 at 14:24
  • 1
    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. –  May 13 '13 at 14:35
  • 1
    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. – bingles Oct 29 '14 at 16:48
  • 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. – binki Jun 28 '18 at 21:42
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
  });
Jay
  • 18,959
  • 11
  • 53
  • 72
guyk
  • 113
  • 5
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.

Artem Andreev
  • 19,942
  • 5
  • 43
  • 42
4

how about:

scope.$watch(function() { 
   return { 
      a: thing-one, 
      b: thing-two, 
      c: red-fish, 
      d: blue-fish 
   }; 
}, listener...);
vinculis
  • 475
  • 3
  • 19
wacamo
  • 49
  • 1
  • 1
  • 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. – Peter V. Mørch Jan 27 '14 at 03:42
  • 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. – RevNoah Mar 18 '14 at 01:53
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.

Akash Shinde
  • 925
  • 3
  • 14
  • 31
  • 2
    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 changed – Akash Shinde Apr 16 '15 at 19:21
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]);
});
Vinit Solanki
  • 1,863
  • 2
  • 15
  • 29