85

How to $scope.$watch multiple variables in Angular, and trigger callback when one of them has changed.

$scope.name = ...
$scope.age = ...
$scope.$watch('???',function(){
    //called when name or age changed
})
aztack
  • 4,376
  • 5
  • 32
  • 50
  • There's [a very good blog post by Ben Nadel](http://www.bennadel.com/blog/2566-Scope-watch-vs-watchCollection-In-AngularJS.htm) on the topic of $watch vs $watchCollection you might find useful. – Jan Molak Jan 30 '14 at 10:23

5 Answers5

120

UPDATE

Angular offers now the two scope methods $watchGroup (since 1.3) and $watchCollection. Those have been mentioned by @blazemonger and @kargold.


This should work independent of the types and values:

$scope.$watch('[age,name]', function () { ... }, true);

You have to set the third parameter to true in this case.

The string concatenation 'age + name' will fail in a case like this:

<button ng-init="age=42;name='foo'" ng-click="age=4;name='2foo'">click</button>

Before the user clicks the button the watched value would be 42foo (42 + foo) and after the click 42foo (4 + 2foo). So the watch function would not be called. So better use an array expression if you cannot ensure, that such a case will not appear.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <link href="//cdn.jsdelivr.net/jasmine/1.3.1/jasmine.css" rel="stylesheet" />
        <script src="//cdn.jsdelivr.net/jasmine/1.3.1/jasmine.js"></script>
        <script src="//cdn.jsdelivr.net/jasmine/1.3.1/jasmine-html.js"></script>
        <script src="http://code.angularjs.org/1.2.0-rc.2/angular.js"></script>
        <script src="http://code.angularjs.org/1.2.0-rc.2/angular-mocks.js"></script>
        <script>

angular.module('demo', []).controller('MainCtrl', function ($scope) {

    $scope.firstWatchFunctionCounter = 0;
    $scope.secondWatchFunctionCounter = 0;

    $scope.$watch('[age, name]', function () { $scope.firstWatchFunctionCounter++; }, true);
    $scope.$watch('age + name', function () { $scope.secondWatchFunctionCounter++; });
});

describe('Demo module', function () {
    beforeEach(module('demo'));
    describe('MainCtrl', function () {
        it('watch function should increment a counter', inject(function ($controller, $rootScope) {
            var scope = $rootScope.$new();
            scope.age = 42;
            scope.name = 'foo';
            var ctrl = $controller('MainCtrl', { '$scope': scope });
            scope.$digest();

            expect(scope.firstWatchFunctionCounter).toBe(1);
            expect(scope.secondWatchFunctionCounter).toBe(1);

            scope.age = 4;
            scope.name = '2foo';
            scope.$digest();

            expect(scope.firstWatchFunctionCounter).toBe(2);
            expect(scope.secondWatchFunctionCounter).toBe(2); // This will fail!
        }));
    });
});


(function () {
    var jasmineEnv = jasmine.getEnv();
    var htmlReporter = new jasmine.HtmlReporter();
    jasmineEnv.addReporter(htmlReporter);
    jasmineEnv.specFilter = function (spec) {
        return htmlReporter.specFilter(spec);
    };
    var currentWindowOnload = window.onload;
    window.onload = function() {
        if (currentWindowOnload) {
            currentWindowOnload();
        }
        execJasmine();
    };
    function execJasmine() {
        jasmineEnv.execute();
    }
})();

        </script>
    </head>
    <body></body>
</html>

http://plnkr.co/edit/2DwCOftQTltWFbEDiDlA?p=preview

PS:

As stated by @reblace in a comment, it is of course possible to access the values:

$scope.$watch('[age,name]', function (newValue, oldValue) {
    var newAge  = newValue[0];
    var newName = newValue[1];
    var oldAge  = oldValue[0];
    var oldName = oldValue[1];
}, true);
stofl
  • 2,950
  • 6
  • 35
  • 48
  • 1
    @Ajax3.14: What exactly is not working? I cannot reproduce that problem. Could it be, that you see the jasmie output "Failing 1 spec"? That would be the expected output, because the test fails (see the comment in the source code of the test). – stofl Nov 27 '13 at 13:00
  • 1
    This answer helped me, thanks! The beauty of this approach is you can access the newValue/oldValue as elements of the watched array (eg. newValue[0], newValue[1] and oldValue[0], oldValue[1]) – reblace Dec 02 '13 at 21:50
  • 2
    If the variables you want to watch are all simple primitives, then `$watchCollection` might be more appropriate. – Blazemonger Feb 11 '14 at 18:12
  • I kept for getting the `true` part. That part is important for it to work! Thanks for this, it works perfectly! – spasticninja Aug 24 '15 at 16:51
43
$scope.$watch('age + name', function () {
  //called when name or age changed
});
Florian F
  • 8,822
  • 4
  • 37
  • 50
8

Angular 1.3 provides $watchGroup specifically for this purpose:

https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$watchGroup

This seems to provide the same ultimate result as a standard $watch on an array of expressions. I like it because it makes the intention clearer in the code.

karlgold
  • 7,970
  • 2
  • 29
  • 22
6

There is many way to watch multiple values :

//angular 1.1.4
$scope.$watchCollection(['foo', 'bar'], function(newValues, oldValues){
    // do what you want here
});

or more recent version

//angular 1.3
$scope.$watchGroup(['foo', 'bar'], function(newValues, oldValues, scope) {
  //do what you want here
});

Read official doc for more informations : https://docs.angularjs.org/api/ng/type/$rootScope.Scope

Ema.H
  • 2,862
  • 3
  • 28
  • 41
  • 1
    Why is this voted down? What's wrong with this? – MGot90 Oct 31 '14 at 15:07
  • I believe `watchCollection` has to be a string. I'm going to confirm then edit. – KthProg Oct 31 '14 at 15:51
  • It's also worth mentioning that I believe the new and old values are accessed in different ways between these functions. – KthProg Oct 31 '14 at 15:57
  • Why was the edit not accepted? `watchCollection` takes a string for the first argument, this is incorrect and I'm removing my upvote. – KthProg Oct 31 '14 at 17:11
  • 1
    I ve tested my answer, be sure it works ^^ you can put a string or like that, i prefer this syntax – Ema.H Sep 03 '15 at 12:44
6

No one has mentioned the obvious:

var myCallback = function() { console.log("name or age changed"); };
$scope.$watch("name", myCallback);
$scope.$watch("age", myCallback);

This might mean a little less polling. If you watch both name + age (for this) and name (elsewhere) then I assume Angular will effectively look at name twice to see if it's dirty.

It's arguably more readable to use the callback by name instead of inlining it. Especially if you can give it a better name than in my example.

And you can watch the values in different ways if you need to:

$scope.$watch("buyers", myCallback, true);
$scope.$watchCollection("sellers", myCallback);

$watchGroup is nice if you can use it, but as far as I can tell, it doesn't let you watch the group members as a collection or with object equality.

If you need the old and new values of both expressions inside one and the same callback function call, then perhaps some of the other proposed solutions are more convenient.

Henrik N
  • 15,786
  • 5
  • 82
  • 131
  • it calls callback function even for the first time when my controller is loaded ? can't I stop it to call for the first time when controller is loaded ? – Sunil Garg Sep 16 '15 at 18:32