How to $watch multiple variable change in angular

Angularjs

Angularjs Problem Overview


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
})

Angularjs Solutions


Solution 1 - Angularjs


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);

Solution 2 - Angularjs

$scope.$watch('age + name', function () {
  //called when name or age changed
});

Solution 3 - Angularjs

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.

Solution 4 - Angularjs

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

Solution 5 - Angularjs

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionaztackView Question on Stackoverflow
Solution 1 - AngularjsstoflView Answer on Stackoverflow
Solution 2 - AngularjsFlorian FView Answer on Stackoverflow
Solution 3 - AngularjskarlgoldView Answer on Stackoverflow
Solution 4 - AngularjsEma.HView Answer on Stackoverflow
Solution 5 - AngularjsHenrik NView Answer on Stackoverflow