NVO NVO - 3 months ago 17
AngularJS Question

Angular watch doesn't always work

I have some functions in my controller and a watch which hide and show some elements on my page.

I initially created an object:

$scope.selectedItems = [];


With some functions I can select en deselect items:

$scope.selectAllItems = function(items){
for(var i =0; i < items.length; i++){
items[i].selected = true;
$scope.selectedItems.push(items[i]._id);
}
}

$scope.deselectAllItems = function(items){
for(var i=0; i<items.length; i++){
items[i].selected = false;
$scope.selectedItems = [];
}
}

$scope.inverseAllItems = function(items){
$scope.selectedItems = [];
for(var i=0; i<items.length; i++){
items[i].selected = items[i].selected ? false : true;
if(items[i].selected)
$scope.selectedItems.push(items[i]._id);
}
}

$scope.selectItem = function(item){
console.log(item.selected);
if(item.selected){
console.log(item._id);
$scope.selectedItems.push(item._id);
}else{
console.log("hier2");
if($scope.selectedItems.indexOf(item._id))
$scope.selectedItems.splice($scope.selectedItems.indexOf(item._id),1)
}
}


And a watch on
$scope.selectedItems


$scope.$watch("selectedItems", function handleSelectedItemsChange(newValue, oldValue){
console.log("$scope.selectedItems.length", $scope.selectedItems.length);
if($scope.selectedItems.length > 1){
$scope.multipleSelect = true;

$('.itemStatus').toggleClass('hidden', true);

}else{
$scope.multipleSelect = false;
$('.itemStatus').toggleClass('hidden', false);
}
})


The problem is that the watch is not always triggered. After using the '
inverseSelection
' and '
deselectAll
' functions it is always triggered, but it will not be triggered after using '
selectAll
' and '
selectItem
' (select items one for one), but I didn't see any difference's in the way I push the entry's in the
selectedItems
array.

Can someone here help me?

Answer

To elaborate a bit more on the reason why it is happening on the two functions you mentioned: in the inverseSelection and deselectAll functions you initialize a new array.

$scope.selectedItems = [];

By default, $scope.$watch only compares for object reference, i.e. "is it still the same object". It does not care about the actual content. That's why the watch fires when you initialize a new empty array. In the other functions, you modify an already existing array, so the reference is the same. For the watch, it is the same object so it does not fire.

As others already mentioned there are two ways to solve it.

  1. $scope.$watch with objectEquality === true

The third parameter of $scope.$watch tells angular to compare the content of the objects:

$scope.$watch("selectedItems", function handleSelectedItemsChange(newValue, oldValue){
    ...
}, true)

When objectEquality == true, inequality of the watchExpression is determined according to the angular.equals function

https://docs.angularjs.org/api/ng/type/%24rootScope.Scope#%24watch

  1. $scope.$watchCollection

In your case, this would be the better solution since it only "shallow watches" the object. This means it does work well for arrays but would not work for nested objects. For your array, it is better for performance than using $scope.$watch with objectEquality.

$scope.$watchCollection("selectedItems", function handleSelectedItemsChange(newValue, oldValue){
    ...
})

for arrays, this implies watching the array items; for object maps, this implies watching the properties

https://docs.angularjs.org/api/ng/type/%24rootScope.Scope#%24watchCollection