$scope.$watch of angular.js not triggered - angularjs

I figured out that the $scope.$watch does not get triggered, when the target of the $watch gets set to the same value as it currently holds.
I have created a sample JSFiddle (http://jsfiddle.net/VKHVq/) which shows the behavior.
Enter any value in the first input field (position_total). The total_before_discount gets adjusted as it should, and also the total_before_discount $watch triggers. As the discount percentage is 0%, the total_discount will always stay 0. Nevertheless the 0 gets assigned all the time to the $scope.total_discount, but unfortunately, the watch on the 'total_discount' does not get triggered. Am I doing something wrong or is this behavior intended?
For me this behavior looks not as intended, as we get newValue and oldValue within the $watch function and as can be seen in a lot of angular.js $watch examples, it is recommended to test if (newValue === oldValue) { return }.
HTML
<div id="container" ng-controller="MyCtrl">
<div>Position total: <input type="number" ng-model="position_total"/>
<div>Total before discount: {{total_before_discount}}</div>
<div>Discount (in %): <input type="number" ng-model="discount"/>
<div>Total discount: {{total_discount}}</div>
<div>Total after discount: {{total_after_discount}}</div>
</div>
JS
var myApp = angular.module('myApp', ['ngAnimate']);
function MyCtrl($scope) {
$scope.position_total = 0;
$scope.total_before_discount = 0;
$scope.discount = 0;
$scope.total_discount = 0;
$scope.total_after_discount = 0;
calculatePositionTotal = function() {
// Dummy method to simulate position calculation
$scope.total_before_discount = $scope.position_total
};
calculateTotalDiscount = function() {
var total_discount = ($scope.total_before_discount / 100) * $scope.discount;
console.log('Going to set total_discount to ' + total_discount);
$scope.total_discount = total_discount;
};
calculateTotalAfterDiscount = function() {
$scope.total_after_discount = $scope.total_before_discount - $scope.total_discount;
};
$scope.$watch('position_total', function (newValue, oldValue) {
calculatePositionTotal();
});
$scope.$watch('total_before_discount', function (newValue, oldValue) {
calculateTotalDiscount();
});
$scope.$watch('discount', function (newValue, oldValue) {
calculateTotalDiscount();
});
$scope.$watch('total_discount', function (newValue, oldValue) {
console.log('total_discount $watch triggered...');
calculateTotalAfterDiscount();
});
}

The documentation says:
The listener is called only when the value from the current watchExpression and the previous call to watchExpression are not equal (with the exception of the initial run, see below).
So it's expected behavior.

The initial value of total_discount is 0 and when you setup the watch for the first time it gets triggered with oldValue and newValue as 0. After that the watch would not trigger till the value changes for total_discount. If you keep assigning it value 0, the watch would not trigger.
Watch would only get triggered when value changes, except in rare cases as mentioned in the documentation
After a watcher is registered with the scope, the listener fn is
called asynchronously (via $evalAsync) to initialize the watcher. In
rare cases, this is undesirable because the listener is called when
the result of watchExpression didn't change. To detect this scenario
within the listener fn, you can compare the newVal and oldVal. If
these two values are identical (===) then the listener was called due
to initialization.

Related

Angular $watch - execute watch block only once

I have a simple $watch on a dropdown value :
$scope.view;
$scope.$watch('view', function(){
console.log(1);
// watch block
}
The value is chaining with ng-model:
<select ng-model="view" ng-options="x for x in name">
</select>
Due to the $digest the value 1 is printed twice, is there a way to tell angular to execute the the $watch block only once?
Simple answer: unregister the watch after the first successful run:
var viewWatch = $scope.$watch('view', function () {
viewWatch(); // Stops the watch
console.log(1);
//watch block
};
However this may or may not yield the results you want, because sometime the $watch is called upon initialization. You may want to set some conditions:
var viewWatch = $scope.$watch('view', function (currentValue, oldValue) {
if (currentValue && !oldValue) {
viewWatch(); // Stops the watch
console.log(1);
//watch block
}
};
You could simply test if view is about to be set for the first time (declared) or is about to be changed (by the select) by testing the params :
$scope.$watch('view'), function(newVal, oldVal) {
if (oldVal && oldVal != newVal) {
console.log(1)
}
}

Why do watches seem to avoid firing when number is set to same value

I am trying to do something similar to this plunker. Notice that I use an ng-click event to set the value of the number back to the same number. However, the change does not seem to fire.
$scope.$watch('value', function(newValue, oldValue){
console.log("Value changed from "+oldValue+" to "+newValue);
});
$scope.call = function(){
console.log("Call is called");
$scope.value=1;
}
So I change this to the following...
$scope.call = function(){
console.log("Call is called");
$scope.value=0;
$scope.value=1;
}
Still no watch fire, however, this will seem to solve the issue (albeit very in a messy way)
$scope.name = 'World';
$scope.value = {};
$scope.value.value=1;
$scope.$watch('value', function(newValue, oldValue){
console.log("Value changed from "+oldValue.value+" to "+newValue.value);
});
$scope.call = function(){
console.log("Call is called");
$scope.value = {};
$scope.value.value=1;
}
So the question is, what is going on here and why can't I watch a number that is set to the same value? Is there a cleaner way to handle this?
This behaviour is documented(scroll down to $watch section)
The listener is called only when the value from the current
watchExpression and the previous call to watchExpression are not equal
(with the exception of the initial run).
Inequality is
determined according to reference inequality, strict comparison via
the !== Javascript operator, unless objectEquality == true ...

$watch API description - first function described as listener function, why?

I'm reading through documentation on scope's method $watch here. The method accepts:
$watch(watchExpression, [listener];
Then they provide examples:
// let's assume that scope was dependency injected as the $rootScope
var scope = $rootScope;
scope.name = 'misko';
scope.counter = 0;
expect(scope.counter).toEqual(0);
scope.$watch('name', function(newValue, oldValue) {
scope.counter = scope.counter + 1;
});
expect(scope.counter).toEqual(0);
scope.$digest();
// the listener is always called during the first $digest loop after it was registered
expect(scope.counter).toEqual(1);
scope.$digest();
// but now it will not be called unless the value changes
expect(scope.counter).toEqual(1);
scope.name = 'adam';
scope.$digest();
expect(scope.counter).toEqual(2);
// Using a listener function
var food;
scope.foodCounter = 0;
expect(scope.foodCounter).toEqual(0);
scope.$watch(
// This is the listener function --------- WHY ?????????????
function() { return food; },
// This is the change handler ---- THIS SHOULD BE A LISTNER FUNCTION
function(newValue, oldValue) {
if ( newValue !== oldValue ) {
// Only increment the counter if the value changed
scope.foodCounter = scope.foodCounter + 1;
}
}
);
// No digest has been run so the counter will be zero
expect(scope.foodCounter).toEqual(0);
// Run the digest but since food has not changed count will still be zero
scope.$digest();
expect(scope.foodCounter).toEqual(0);
// Update food and run digest. Now the counter will increment
food = 'cheeseburger';
scope.$digest();
expect(scope.foodCounter).toEqual(1);
What I don't understand is that why they refer to function() { return food; } in the second example as a // This is the listener function if this is the function that should return the value that will be watched. which is a watchExpression?
The comment is a little bit misleading and you might file an issue or pull request to improve it.
As you said correctly there are 2 arguments for $watch(watchExpression, [listener])
watchExpression
The watchExpression is called on every call to $digest() and should return the value that will be watched.
The watch expression can be either a string or a function.
If you specify a function this is not the listener but rather a comparison function which will be called many many times so don't do any fancy stuff here ;)
This comparison function is used for the angular dirty checking.
More details on this can be found in this part of the video by the creator of angularJs: https://www.youtube.com/watch?feature=player_detailpage&v=ZhfUv0spHCY#t=1988
listener
This function is called whenever the value of your watchExpression changes.
So this a perfect place to update your models and do your fancy business logic

What does it mean watch expression missing

Newbie quesiton, I read the document of rootscope.$watch. the syntax of $watchis
$watch(watchExpression, [listener], [objectEquality], [deregisterNotifier]);
But I see the examples use it like below.
var food;
scope.foodCounter = 0;
expect(scope.foodCounter).toEqual(0);
scope.$watch(
// This is the listener function
function() { return food; },
// This is the change handler
function(newValue, oldValue) {
if ( newValue !== oldValue ) {
// Only increment the counter if the value changed
scope.foodCounter = scope.foodCounter + 1;
}
}
);
What does it mean missing WatchExpression? thanks.
watchExpression can be both string or function - refer back to angularjs documentation as posted in the question. Given example as posted in the question will never trigger change handler because food is always undefined.
If you are using function as watchExpression, the function must return different value in order to trigger change handler. As documented in angularjs official document
Expression that is evaluated on each $digest cycle. A change in the
return value triggers a call to the listener.
Code Snippet
function Ctrl($scope) {
$scope.food = "Laksa";
$scope.foodCounter = 0;
$scope.$watch(
//this is watch expression using function
function (scope) {
return scope.food;
},
//this is listener - change handler
function (newValue, oldValue) {
console.log(newValue, oldValue);
if (newValue !== oldValue) {
// Only increment the counter if the value changed
$scope.foodCounter = $scope.foodCounter + 1;
}
});
}
example - http://jsfiddle.net/4atA2/2/
this listener function is equivalent to $scope.$watch('food',... because it don't have additional logic. You have flexibility to add additional logic checking to decide whether want to trigger change handler or not. For example, watch on two scope variable.
example - http://jsfiddle.net/5NLNa/5/
function (scope) {
if(scope.drink == "Milo") {
return scope.food;
}
},

Angular service watch creates $digest loop

I am trying to get a scope to update when the return value of a service function changes. From reading, it sounds like I'm supposed to use $watch, but following the angular docs creates an infinite $digest loop in my application, here the controller:
.controller('navCtrl', function(CookieHandler, $scope){
// this works, but doesn't update when CookieHandler.get() changes
// $scope.user = CookieHandler.get();
$scope.$watch(
function() {return CookieHandler.get()},
function(newValue, oldValue) {
if( newValue !== oldValue) {
$scope.user = newValue
}
}
)
//etc
From the error message there's something about using $watch on functions that return an array, but CookieHandler.get() returns an object?
I looked around some more and read that I should use $apply instead of $watch here because it's easier to test, so I tried this:
.controller('navCtrl', function(CookieHandler, $scope){
// this works, but doesn't update when CookieHandler.get() changes
// $scope.user = CookieHandler.get();
$scope.$apply(function(scope){
scope.user = CookieHandler.get();
})
})
But this throws an error about the digest cycle already running. Is there a way to fix my $watch to stop the infinite loop? CookieHandler.get just checks the value with $cookieStore
$watch performs an equal comparison between the current value and the value from the previous digest cycle. If they are different then the function is executed.
If the object returned by CookieHandler.get() is always the same reference then the watch function will never get called. Even if you update properties of that object.
Arrays are a bad idea because they are often always a new object and JavaScript will see them as never equal.
You could serialize the object/array to JSON and use that value, but this won't work if there are changes in key/value orders.
It looks like you are trying to monitor when the current user is changed. Assuming you have a unique identifier for a user, then you might want to try something like this.
$scope.$watch(
function() {
var user = CookieHandler.get();
return (typeof user === 'undefined') ? 0 : user.id;
},
function(newValue, oldValue) {
if( newValue !== oldValue) {
$scope.user = CookieHandler.get();
}
}
);
Note that I call .get() twice. I don't know how your service works. Maybe that's not possible, but the point is to watch a value that is easily comparable.

Resources