Angular $watch - execute watch block only once - angularjs

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)
}
}

Related

Angular: Why is $watch being called?

Any reason why a $scope.$watch would be called when the value it is watching for does not exist anywhere within the app?
As you can see below, no matter what string I assign for the watchExpression , the function runs ...
$scope.$watch( 'kwhefjkewgfweahhfwujad' , // a bunch of random letters
function()
{ console.log("it fired ... why?");
}
) ;
Firefox does not indicate any errors.
try
$scope.$watch( 'kwhefjkewgfweahhfwujad', function(val) {
if (val) {
console.log("it fired ... why?");
}
});
watchers are running with every digest cycle. as you are watching primitive value, attached to the scope, you need to verify that a new value exists (and has changed).
you could also do
$scope.$watch( 'kwhefjkewgfweahhfwujad', function(newVal, oldVal) {
if (newVal !== oldVal) {
console.log("it fired ... why?", newVal, oldVal);
}
});

Test if function has been called inside $scope.$watch

I'm trying to figure out how to test with karma jasmine if function was executed inside $watch with the conditions I need.
Here what is in the controller. $watch contains couple of condition.
$scope.$watch('player', function (newVal, oldVal) {
if (oldVal && newVal != undefined && newVal != oldVal) {
if (newVal.email == oldVal.email && newVal.emWallet == oldVal.emWallet)
$scope.saveSettings();
}
}, true)
This is the part of the test
it('when player property is changed saveSettings calls', function () {
var sspy = spyOn(scope, "saveSettings");
expect(scope.player.email).toEqual('');
expect(scope.player.emWallet).toEqual('');
expect(scope.player.balance).toEqual(10.0000);
//change player balance
scope.player.balance = 10.0304;
scope.$apply();
expect(scope.player.email).toEqual('');
expect(scope.player.emWallet).toEqual('');
expect(sspy).toHaveBeenCalled();
});
In the test above I'm changing the property that is outside condition so player.email and player.emWallet still the same and expect the call of saveSettings() function inside, but get "sspy has never been called" error.
I would appreciate a lot if someone point me right direction.
Update:
When I change actual value scope.player.balance = 10.0304; in my code, the function fires, but test does not pass successfully

Re-registering $watch after de-registering

I have ng-model on a date variable. On changing that variable, I want to make some validations and during those validations I might change the start variable itself, and to make sure I don't get infinite-watch trigger, I'm de-registering the watch before, and re-registering it when the validation finishes.
The re-registering doesn't work. What am I doing wrong?
var watchStartTime = $scope.$watch('timeSelection.startTime', function(newValue, oldValue) {
if (newValue === oldValue) return;
validateStartEndTime();
}, true);
function validateStartEndTime() {
// De-register watch
watchStartTime();
// Do some stuff that might change timeSelection.startTime
// Re-register watch
watchStartTime();
}
I also tried setTimeout with 0 on the re-registering, thought it might work, but it doesn't.
the timeSelection.startTime changes (I know for a fact it does) but the function in $watch doesn't get called again.
What am I missing?
As stated by #Nikos Paraskevopoulos $watch returns the deregistration fn.
I think you should do something like the following...
var register = function() {
var unregister = $scope.$watch('timeSelection.startTime', function(newVal, oldVal) {
if (newVal === oldVal) {
return;
}
unregister();
validateStartEndTime();
register();
}, true);
};
register();

$scope.$watch of angular.js not triggered

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.

Angularjs: restoring model value in $watch after failed update on server side

Here's the scenario:
fiddle:
$scope.$watch('obj.value',function(val, oldVal) {
if (val === oldVal) return;
MyService.update($scope.obj, $scope.result).then(function(response){
$scope.results.push(response);
}, function(response) {
$scope.obj.value = oldVal;
$scope.results.push(response);
});
});
I set a watch on a value and update it to db whenever it changes. But if the update fails for some reason (connection problem, server error, invalid session, insufficient permissions, .etc), I'd like to restore that value to the previous version. In the fiddle you can see what happens if you select "reject deferred" and try to change the value - it starts infinte loop of failed requests, restored values and $watch triggers.
For the time being I'm setting a flag on the scope to indicate that request has failed and next $watch should not call the service. But I'm looking for ways to reduce this boilerplate code.
Of course, I could always use some other ways to inform the scope, for example ng-change, but then I lose reference to the old value. I could keep the reference in my scope, but that is even worse than current situation.
Have you any ideas how these situations should be handled? Basically what I'm looking for is a way to update the model in the $watch without triggering further $watches, if that's even possible.
Use ng-change directive instead of the watcher, and use an internal state variable to store the value oft the last successful save.
See it in action: http://jsfiddle.net/Zmetser/vscGP/6/
function MyCtrl($scope, MyService) {
var lastSaved;
$scope.obj = {value: "foo"};
$scope.results = [];
$scope.result = "1";
lastSaved = $scope.obj.value;
$scope.sentinel = function ( value ) {
MyService.update($scope.obj, $scope.result).then(function(response){
lastSaved = angular.copy($scope.obj.value);
$scope.results.push(response);
}, function(response) {
if ( lastSaved )
$scope.obj.value = lastSaved;
$scope.results.push(response);
});
};
}
<input type="text" ng-model="obj.value" ng-change="sentinel(obj.value)"/>
Rather than using a timeout or $timeout, you could also use a closure to encapsulate a boolean flag that you reset.
(function WatchWithRevert () {
var justReverted = false;
$scope.$watch('obj.value',function(val, oldVal) {
//if (val === oldVal) return;
if (justReverted) {
justReverted = false;
return;
}
MyService.update($scope.obj, $scope.result).then(function(response){
$scope.results.push(response);
}, function(response) {
$scope.obj.value = oldVal;
justReverted = true;
$scope.results.push(response);
});
});
})();
Basically, you are doing it right. The watcher watches the watched object, so the best thing you can do is to tell it to close one eye for next watch.
You can use $timeout to set a temporary flag that gets cleaned up ASAP
_skipWatch = false
rollBackLocally = (newVal, oldVal) ->
_skipWatch = true
angular.copy oldVal, newVal
# schedule flag reset just at the end of the current $digest cycle
$timeout (-> _skipWatch = false), 0
$scope.$watch 'obj.value', (newVal, oldVal) ->
return if _skipWatch
MyService.doSomething().then
((response) -> $scope.results.push(response)),
(-> rollBackLocally newVal, oldVal)

Resources