326

There is an array of objects in my scope, I want to watch all the values of each object.

This is my code:

function TodoCtrl($scope) {
  $scope.columns = [
      { field:'title', displayName: 'TITLE'},
      { field: 'content', displayName: 'CONTENT' }
  ];
   $scope.$watch('columns', function(newVal) {
       alert('columns changed');
   });
}

But when I modify the values, e.g. I change TITLE to TITLE2, the alert('columns changed') never popped.

How to deep watch the objects inside an array?

There is a live demo: http://jsfiddle.net/SYx9b/

Freewind
  • 193,756
  • 157
  • 432
  • 708

10 Answers10

543

You can set the 3rd argument of $watch to true:

$scope.$watch('data', function (newVal, oldVal) { /*...*/ }, true);

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

Since Angular 1.1.x you can also use $watchCollection to watch shallow watch (just the "first level" of) the collection.

$scope.$watchCollection('data', function (newVal, oldVal) { /*...*/ });

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

Ivar
  • 6,138
  • 12
  • 49
  • 61
Piran
  • 7,180
  • 1
  • 24
  • 37
51

There are performance consequences to deep-diving an object in your $watch. Sometimes (for example, when changes are only pushes and pops), you might want to $watch an easily calculated value, such as array.length.

wizardwerdna
  • 948
  • 6
  • 11
  • 1
    This should have more votes. Deep watching is expensive. I understand OP was looking for deep watching, but people might come here just wanting to know if the array itself changed. Watching the length is much faster, in that case. – Scott Silvi Sep 01 '13 at 14:55
  • 45
    This should be a comment, not an answer. – Blazemonger Sep 25 '14 at 16:13
  • 1
    The performance issues would be minimized by using $watchCollection, as mentioned by @Blazemonger comment (http://stackoverflow.com/questions/14712089#comment32440226_14713978). – Sean the Bean Jun 12 '15 at 19:23
  • Neat advice. Regarding this comment not being an answer, in my opinion it would be better to elaborate the suggestion to fulfil the acceptance criteria for an answer. I think that with this approach an elegant solution to the question may be achieved. – Amy Pellegrini Jul 21 '16 at 16:38
43

If you're going to watch only one array, you can simply use this bit of code:

$scope.$watch('columns', function() {
  // some value in the array has changed 
}, true); // watching properties

example

But this will not work with multiple arrays:

$scope.$watch('columns + ANOTHER_ARRAY', function() {
  // will never be called when things change in columns or ANOTHER_ARRAY
}, true);

example

To handle this situation, I usually convert the multiple arrays I want to watch into JSON:

$scope.$watch(function() { 
  return angular.toJson([$scope.columns, $scope.ANOTHER_ARRAY, ... ]); 
},
function() {
  // some value in some array has changed
}

example

As @jssebastian pointed out in the comments, JSON.stringify may be preferable to angular.toJson as it can handle members that start with '$' and possible other cases as well.

Jay
  • 18,959
  • 11
  • 53
  • 72
  • I also find there is a 3rd parameter to `$watch`, is it able to do the same? `Pass true as a third argument to watch an object's properties too.` See: http://www.cheatography.com/proloser/cheat-sheets/angularjs/ – Freewind Feb 05 '13 at 16:47
  • @Freewind If there is ever a case where you will need to watch multiple arrays it will fail as [seen here](http://jsfiddle.net/qsMhq/1). But yes, that will work too & provides the same functionality as using `angular.toJson` on a single array. – Jay Feb 05 '13 at 17:20
  • 2
    Just beware that angular.toJson does not seem to include include members that start with '$': angular.toJson({"$hello":"world"}) is just "{}". I used JSON.stringify() as an alternative – jssebastian Jun 07 '13 at 02:32
  • @ jssebastian Thanks - I've updated the answer to include that information. – Jay Jun 07 '13 at 15:07
  • 1
    can we know which property has changed? – Ashwin Oct 01 '14 at 07:30
  • I guess you can overcome the problem with `columns + ANOTHER_ARRAY` by $watching `[columns, ANOTHER_ARRAY]` instead. – corwin.amber Apr 14 '15 at 23:10
  • My implementation required an "Is Dirty" check for a form. The HTML form is modifying an object, but it's modifying the copy of the original JSON object retrieved from the server which allows the user to reset the form. An extremely easy implementation for this was to convert both objects to JSON and then compare them inside the scope.$watchCollection("editSettings", function () { }); function! Thanks @Trevor Senior. Worked great. – David Gunderson Feb 11 '16 at 22:19
  • This technique of watching multiple arrays by JSONifying them is rather expensive! It would be preferable to have two separate watches. If the same function is to be invoked on change, then the function can be defined separately and passed into both watches. – Hamish Mar 10 '17 at 17:47
  • how do we add multiple watching objects along with json array. Can we use `$scope.$watchGroup([function() { return angular.toJson($scope.arrayList)} , 'anotherScopeVar'], function (nv,ov) {}};` – xkeshav Aug 21 '17 at 06:58
21

It's worth noting that in Angular 1.1.x and above, you can now use $watchCollection rather than $watch. Although the $watchCollection appears to create shallow watches so it won't work with arrays of objects like you expect. It can detect additions and deletions to the array, but not the properties of objects inside arrays.

Jonathan Rowny
  • 7,588
  • 1
  • 18
  • 26
20

Here is a comparison of the 3 ways you can watch a scope variable with examples:

$watch() is triggered by:

$scope.myArray = [];
$scope.myArray = null;
$scope.myArray = someOtherArray;

$watchCollection() is triggered by everything above AND:

$scope.myArray.push({}); // add element
$scope.myArray.splice(0, 1); // remove element
$scope.myArray[0] = {}; // assign index to different value

$watch(..., true) is triggered by EVERYTHING above AND:

$scope.myArray[0].someProperty = "someValue";

JUST ONE MORE THING...

$watch() is the only one that triggers when an array is replaced with another array even if that other array has the same exact content.

For example where $watch() would fire and $watchCollection() would not:

$scope.myArray = ["Apples", "Bananas", "Orange" ];

var newArray = [];
newArray.push("Apples");
newArray.push("Bananas");
newArray.push("Orange");

$scope.myArray = newArray;

Below is a link to an example JSFiddle that uses all the different watch combinations and outputs log messages to indicate which "watches" were triggered:

http://jsfiddle.net/luisperezphd/2zj9k872/

Luis Perez
  • 27,650
  • 10
  • 79
  • 80
12

$watchCollection accomplishes what you want to do. Below is an example copied from angularjs website http://docs.angularjs.org/api/ng/type/$rootScope.Scope While it's convenient, the performance needs to be taken into consideration especially when you watch a large collection.

  $scope.names = ['igor', 'matias', 'misko', 'james'];
  $scope.dataCount = 4;

  $scope.$watchCollection('names', function(newNames, oldNames) {
     $scope.dataCount = newNames.length;
  });

  expect($scope.dataCount).toEqual(4);
  $scope.$digest();

  //still at 4 ... no changes
  expect($scope.dataCount).toEqual(4);

  $scope.names.pop();
  $scope.$digest();

  //now there's been a change
  expect($scope.dataCount).toEqual(3);
Community
  • 1
  • 1
Jin
  • 1,076
  • 10
  • 10
  • 14
    OP specified an array of objects. Your example works with an array of strings, but $watchCollection does not work with an array of objects. – KevinL Oct 09 '14 at 16:15
4

This solution worked very well for me, i'm doing this in a directive:

scope.$watch(attrs.testWatch, function() {.....}, true);

the true works pretty well and react for all the chnages (add, delete, or modify a field).

Here is a working plunker for play with it.

Deeply Watching an Array in AngularJS

I hope this can be useful for you. If you have any questions, feel free for ask, I'll try to help :)

EPotignano
  • 111
  • 1
  • 6
4

In my case, I needed to watch a service, which contains an address object also watched by several other controllers. I was stuck in a loop until I added the 'true' parameter, which seems to be the key to success when watching objects.

$scope.$watch(function() {
    return LocationService.getAddress();
}, function(address) {
    //handle address object
}, true);
RevNoah
  • 2,344
  • 3
  • 20
  • 28
1

Setting the objectEquality parameter (third parameter) of the $watch function is definitely the correct way to watch ALL properties of the array.

$scope.$watch('columns', function(newVal) {
    alert('columns changed');
},true); // <- Right here

Piran answers this well enough and mentions $watchCollection as well.

More Detail

The reason I'm answering an already answered question is because I want to point out that wizardwerdna's answer is not a good one and should not be used.

The problem is that the digests do not happen immediately. They have to wait until the current block of code has completed before executing. Thus, watch the length of an array may actually miss some important changes that $watchCollection will catch.

Assume this configuration:

$scope.testArray = [
    {val:1},
    {val:2}
];

$scope.$watch('testArray.length', function(newLength, oldLength) {
    console.log('length changed: ', oldLength, ' -> ', newLength);
});

$scope.$watchCollection('testArray', function(newArray) {
    console.log('testArray changed');
});

At first glance, it may seem like these would fire at the same time, such as in this case:

function pushToArray() {
    $scope.testArray.push({val:3});
}
pushToArray();

// Console output
// length changed: 2 -> 3
// testArray changed

That works well enough, but consider this:

function spliceArray() {
    // Starting at index 1, remove 1 item, then push {val: 3}.
    $testArray.splice(1, 1, {val: 3});
}
spliceArray();

// Console output
// testArray changed

Notice that the resulting length was the same even though the array has a new element and lost an element, so as watch as the $watch is concerned, length hasn't changed. $watchCollection picked up on it, though.

function pushPopArray() {
    $testArray.push({val: 3});
    $testArray.pop();
}
pushPopArray();

// Console output
// testArray change

The same result happens with a push and pop in the same block.

Conclusion

To watch every property in the array, use a $watch on the array iteself with the third parameter (objectEquality) included and set to true. Yes, this is expensive but sometimes necessary.

To watch when object enter/exit the array, use a $watchCollection.

Do NOT use a $watch on the length property of the array. There is almost no good reason I can think of to do so.

Community
  • 1
  • 1
0

$scope.changePass = function(data){
    
    if(data.txtNewConfirmPassword !== data.txtNewPassword){
        $scope.confirmStatus = true;
    }else{
        $scope.confirmStatus = false;
    }
};
  <form class="list" name="myForm">
      <label class="item item-input">        
        <input type="password" placeholder="ใส่รหัสผ่านปัจจุบัน" ng-model="data.txtCurrentPassword" maxlength="5" required>
      </label>
      <label class="item item-input">
        <input type="password" placeholder="ใส่รหัสผ่านใหม่" ng-model="data.txtNewPassword" maxlength="5" ng-minlength="5" name="checknawPassword" ng-change="changePass(data)" required>
      </label>
      <label class="item item-input">
        <input type="password" placeholder="ใส่รหัสผ่านใหม่ให้ตรงกัน" ng-model="data.txtNewConfirmPassword" maxlength="5" ng-minlength="5" name="checkConfirmPassword" ng-change="changePass(data)" required>
      </label>      
       <div class="spacer" style="width: 300px; height: 5px;"></div> 
      <span style="color:red" ng-show="myForm.checknawPassword.$error.minlength || myForm.checkConfirmPassword.$error.minlength">รหัสผ่านต้องมีจำนวน 5 หลัก</span><br>
      <span ng-show="confirmStatus" style="color:red">รหัสผ่านใหม่ไม่ตรงกัน</span>
      <br>
      <button class="button button-positive  button-block" ng-click="saveChangePass(data)" ng-disabled="myForm.$invalid || confirmStatus">เปลี่ยน</button>
    </form>