Insightful response to the question: "How does Angular handle $scope.$watch on undefined?"
Delve into the workings of digest cycles and watchers in Angular with these informative posts:
* http://www.benlesh.com/2013/08/angularjs-watch-digest-and-apply-oh-my.html
* http://onehungrymind.com/notes-on-angularjs-scope-life-cycle/
An important fact to note is that Angular will always execute each watchExpression
at least once during a digest cycle. However, it only triggers the listener
function if the value of the watchExpression
has changed since the previous invocation.
Consider this straightforward scenario where you aim to synchronize scope.otherCount
with scope.count
:
$scope.count = 1;
$scope.otherCount = 0;
$scope.$watch(function(){
// This function is invoked multiple times every digest cycle.
// A returned value indicates to Angular when to run the listener.
// Any variation in return value between calls triggers the listener.
console.log('watchExpression');
// Crucial for Angular to detect changes in our watched value.
return $scope.count;
}, function(newValue){
// When $scope.count changes, this listener updates $scope.otherCount.
// The listener is activated because the prior `watchExpression`
// yielded a distinct value.
$scope.otherCount = $scope.count;
console.log('count=', $scope.count);
console.log('count=', $scope.otherCount);
});
Comparison against your own watch will shed light on the nuances of this process.
Prior Answer:
This response attempts to address underlying queries posed by the question:
1 - "Why is there no error thrown?"
Your example conforms to valid JavaScript - a non-returning function inherently yields undefined
.
function a(){
// No explicit return.
}
a() === undefined; // true
Angular receives undefined
from each execution of your watchExpression
, interpreting its value as static throughout.
2 - "How does it function without errors?"
By updating the $scope
within each instance of the watchExpression
, correct views are rendered.
However, the watchExpression
undergoes multiple evaluations in every digest cycle, leading to more frequent $scope
modifications than necessary.
3 - "What's the optimal approach?"
Incorporate the new $scope.$watchCollection
method in Angular 1.2 or later versions to monitor array values effectively:
$scope.$watchCollection('items', function(){
var total = 0;
for (var i = 0; i < $scope.items.length; i++) {
total += $scope.items[i].price * $scope.items[i].quantity;
}
$scope.bill.total = total;
$scope.bill.discount = total > 100 ? 10 : 0;
$scope.bill.subtotal = total - $scope.bill.discount;
});
This implementation tracks changes in $scope.items
and executes the listening function upon any alteration to the array or its elements.
For users of earlier angular versions (e.g., Angular 1.0.x), a distinct strategy is required to prompt recalculations:
HTML:
Add an ng-change
event to input fields for triggering recalculation upon value adjustments:
...
<div ng-repeat="item in items">
<span>{{item.title}}</span>
<!-- Include ng-change="itemChanged()" in the input -->
<input ng-model="item.quantity" ng-change="itemChanged()" />
<span>{{item.price | currency}}</span>
<span>{{item.price * item.quantity | currency}}</span>
</div>
...
JavaScript:
// Define a unified function responsible for calculating totals.
function calculateTotals(){
var total = 0;
for (var i = 0; i < $scope.items.length; i++) {
total += $scope.items[i].price * $scope.items[i].quantity;
}
$scope.bill.total = total;
$scope.bill.discount = total > 100 ? 10 : 0;
$scope.bill.subtotal = total - $scope.bill.discount;
}
// Introduce a $watch to update totals upon array length changes.
$scope.$watch('items', calculateTotals);
// Reassess totals on input value modification.
$scope.itemChanged = function(){
calculateTotals();
};