Angular: Why is $watch being called? - angularjs

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

Related

$watch is only working without a watchExpression

In my $onInit I have a $watch:
public $onInit() {
const name = this.$state.current.name;
console.log(name);
this.$scope.$watch(name, (oldVal, newVal) => {
console.log(oldVal, newVal);
console.log(this.$state.current.name;
});
}
When I run this code the oldVal, newVal are both undefined and the this.$state.current.name value is the same in the scope of the $watch and outside of it.
When I run the code without the name expression:
public $onInit() {
const name = this.$state.current.name;
console.log(name);
this.$scope.$watch((oldVal, newVal) => {
console.log(oldVal, newVal);
console.log(this.$state.current.name);
});
}
The $watch constantly runs. The oldVal is ScopeĀ {$id: 353, .... etc but the newVal is undefined. But the this.$state.current.name is now updated inside the $watch scope.
So the value of this.$scope.current.name does change. But when I use that as the expression in the $watch the oldVal, newVal are both undefined and the this.$state.current.name isn't changed inside the $watch.
It looks like I'm doing something wrong in the watchExpression. Any suggestions?
This is expected behaviour $watch function.
From docs
$watch(watchExpression, listener, [objectEquality]);
// watchExpression - can be `expression/function`
// listener - callback
// objectEquality - deepWatch
$watch function evaluate its expression wrt $scope on each digest cycle run. So in the first approach you're just passing name as string, so what happens is, when digest cycle kicks in, it tries to evaluate name variable, which obviously not exist in $scope, that's the reason you're getting undefined value for each $watch evaluation.
Whereas in 2nd approach you're passing function/callback to $watch function. which evaluates on each digest cycle run, which is wrong. What you supposed to do is, pass callback function that will return this.$state.current.name, then only you will get desired results in $watch listener function.
It can be solved using below
this.$scope.$watch(() => {
return this.$state.current.name
}, (oldVal, newVal) => {
console.log(oldVal, newVal);
console.log(this.$state.current.name;
});
The watchExpression should be set as a string (even if it is already a string)
Did you try this ?
var name = 'some value'
this.$scope.$watch('name', (oldVal, newVal) => {
console.log(oldVal, newVal);
console.log(this.$state.current.name);
});
Documentation

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

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();

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 ...

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