I have encountered an issue with watchers in Angular and array splice that is exhibiting a strange behavior. To demonstrate the problem, I have created a small demo with logs showing this behavior. You can view the code below or see it in action on this Plunker.
html:
<div class="logs">
<div><b>LOGS:</b></div>
<ul ng-repeat="watchEntry in watchersLogs track by $index">
<li>{{watchEntry}}</li>
</ul>
<div><b>Watcher Count:</b> {{watchers.length}}</div>
</div>
<div class="item" ng-repeat="item in list" ng-init="InitItem()">
<div class="item-title-wrapper">
<span class="item-title">Item {{ $index + 1 }}</span>
<button ng-click="AddNewItem()">Add New</button>
<button ng-click="RemoveItem()">Remove</button>
</div>
<div class="field">
<div>
CREDIT:
<input type="number" name="credit" ng-model="item.credit" />
</div>
<div>
DEBIT:
<input type="number" name="debit" ng-model="item.debit" />
</div>
</div>
</div>
js:
var app = angular.module("listApp", []);
app.controller("listController", function($scope) {
// list with all data:
$scope.list = [{
credit: 2000,
debit: 0
}, {
credit: 100000,
debit: 1000
}];
// list containing all watchers:
$scope.watchers = [];
// logs containing all watcher event:
$scope.watchersLogs = [];
function SetWatcher(itemIndex) {
$scope.watchers.splice(itemIndex, 0, $scope.$watch("list[" + itemIndex + "]", function(newValues, oldValues, scope) {
$scope.watchersLogs.push("Item " + itemIndex + " watcher fired!");
}, true));
}
$scope.InitItem = function() {
// set a watcher for newly create item:
SetWatcher(this.$index);
}
$scope.AddNewItem = function() {
var newItem = {
credit: 0,
debit: 0
};
// put the item into the array:
$scope.list.splice((this.$index + 1), 0, newItem);
};
$scope.RemoveItem = function() {
// destroy the watcher:
$scope.watchers[this.$index]();
$scope.watchers.splice(this.$index, 1);
// remove the item from the list:
$scope.list.splice(this.$index, 1);
};
});
css:
.logs {
margin-bottom: 50px;
}
.item {
margin-bottom: 25px;
}
As demonstrated above, when initializing or adding a new item to the `$scope.list` array, each item gets assigned a new watcher. The issue arises when using `splice` to add items at specific positions, causing the watcher expression to execute multiple times unexpectedly. This is due to the change in reference of items after inserting a new one. To resolve this, I added a function called `RemoveWatcher` to clear watchers before setting up a new one. This function is called before initializing a new watcher and also within the `RemoveItem` method. By ensuring that the watchers are removed correctly, the issue with multiple executions of the watcher expression has been successfully addressed. I appreciate any further insights or solutions to improve this implementation. Thank you!
...UPDATE...
The issue has been resolved! Special thanks to @Deblaton Jean-Philippe for the assistance. I introduced a new function for removing watchers:
function RemoveWatcher(itemIndex) {
if ($scope.watchers[itemIndex]) {
// destroy the watcher:
$scope.watchers[itemIndex]();
// remove it from the array as well:
$scope.watchers.splice(itemIndex, 1);
}
}
This function is now called before setting up a new watcher to ensure proper clearing. Additionally, it is invoked within the `RemoveItem` method, always removing the last watcher entry from the array due to changes in field order when splicing the `$scope.list` array. The solution seems to be working flawlessly now! Refer to the updated Plunker for verification. :)