Watch variable and change it - angularjs

In AngularJS I have a directive that watches a scope variable. When the variable contains certain data then I need to alter that variable a bit. The problem is that when I change the variable that my $watch is triggered again. So I end up in a continuous loop.
scope.$watch('someVar', function(newValue, oldValue) {
console.log(newValue);
scope.someVar = [Do something with someVar];
});
This keeps triggering $watch again, which makes sense. But I do need a way to change the watched variable. Is there a way to do this?

When a variable is watched for changes using $scope.$watch, angular checks if the reference has changed. If it has, then the $watch handler is executed to update the view.
If you plan on changing the scope variable within the $watch handler, it will trigger an infinite $digest loop because the scope variable reference changes every time that it is called.
The trick to getting around the infinite digest issue is to preserve the reference inside your $watch handler using angular.copy (docs):
scope.$watch('someVar', function(newValue, oldValue) {
console.log(newValue);
var someVar = [Do something with someVar];
// angular copy will preserve the reference of $scope.someVar
// so it will not trigger another digest
angular.copy(someVar, $scope.someVar);
});
Note: This trick only works for object references. It will not work with primitives.
In general, its not a good idea to update a $watched variable within its own $watch listener. However, sometimes it may be unavoidable.

Inside function use if condition to avoide continues for loop
scope.$watch('someVar', function(newValue, oldValue) {
if(newValue!==oldValue) {
console.log(newValue);
scope.someVar = [Do something with someVar];
}
});

This is how dirty-checking works. Every time something on the $scope changes Angular will spin through everything that is attached to the scope and it will keep doing so untill there are no more changes.
If you want to do something like that, you will have to make sure that your $watch function is idempotent. You will have to look at both newValue and oldValue and figure out wether you've already applied the changes to your variable in this $digest loop. How you can do that depends a bit on what kind of changes you're doing to someVar.
Generally speaking, changing a watched variable in a watch-function is unfortunately not a good idea.

you can manage it using a bool variable
$scope.someVarChanged = false;
scope.$watch('someVar', function(newValue, oldValue) {
console.log(newValue);
$scope.someVarChanged = !$scope.someVarChanged!;
if($scope.someVarChanged) {
scope.someVar = [Do something with someVar];
}
});

yes you can cancel it like this
var wathcer = scope.$watch('someVar', function(newValue, oldValue) {
console.log(newValue);
scope.someVar = [Do something with someVar];
});
wathcer(); // clear the watch

Related

angular watch not fired

There is 1 angular app, with 1 parent controller, and a child controller.
In the child, there is 1 $watch WATCH-CHILD for OBJ-CHILD, which triggers an $emit.
In the parent, there is a listener for the $emit, we'll call it ON-LISTENER, and a $watch WATCH-PARENT for OBJ-PARENT (which uses true as the 3rd argument).
When the child's OBJ-CHILD is changed, it triggers WATCH-CHILD, which triggers the $emit.
The parent listener ON-LISTENER is fired, and changes OBJ-PARENT. It also sets some $location properties.
The $watch WATCH-PARENT for OBJ-PARENT is never fired (even though the value has changed), as well as the properties set on $location not changed in the browser URL (I know they are indeed changed inside the JavaScript, cause I print them).
In order to make sure that ON-LISTENER is called within a $digest, I tried to call $digest at the end of ON-LISTENER, and got the expected exception.
Any idea if I'm doing something wrong? I expect the changes that occur in ON-LISTENER to trigger WATCH-PARENT and browser URL change.
I will try to reproduce on jsfiddle and edit this post if successful.
The code looks like:
CHILD:
$scope.$watch('vars.model', function(newValue, oldValue) {
console.log('model changed');
$scope.$emit('highlightChange', newValue);
}, true);
PARENT:
$scope.$watch('vars.model.highlight', function(newValue, oldValue) {
console.log('highlight changed');
}, true);
$scope.$on('highlightChange', function(event, value) {
console.log('listener', $scope.vars.model.highlight.categoryId, value.categoryId);
$location.search('category-id', value.categoryId);
$scope.vars.model.highlight.categoryId = value.categoryId;
}
Next time please provide more code which works, that way you can get better answers.
Here is a Demo plunker which I created to test the code which you provided. It works just fine. If you could provide more code then we could find the real reason why it did not work.
I created two controllers parentCtrl and childCtrl which uses your code and object of provided structure.
$scope.vars = {
model:{
highlight:{
categoryId : 5 //This value is set for testing purposes
}
}
};
Also, I changed watch target (vars.model -> vars.model.highlight) to be the same as in parent controller
$scope.$watch('vars.model.highlight', function(newValue, oldValue) {
console.log('child model changed (new/old)', newValue, oldValue);
$scope.$emit('highlightChange', newValue);
console.log('Emited change event');
}, true);
Thanks for the help. I found out that my event originated from a manual call to $scope.$digest() due to the originating event being triggered from a daterangepicker 'apply.daterangepicker' event.
When I changed that to $scope.$apply, the problem seemed to go away.
I can assume from that, that $apply() is the one in charge of keep calling $watch as long as there are changes, and that $digest() doesn't do so.
For future reference, I placed the problem here:
https://plnkr.co/edit/ItkALhw16Aqukk3EFrRz?p=preview
$scope.digest();
should become
$scope.apply();

Angular JS prevent $watch listener

I've use dropdown list which render some results via $scope.$watch listener callback in Angular JS controller.
When i change dropdown list other value it works fine.
But in some case i need to keep last selected results(does not need rise that event).
I've tried ng-change and ng-click but it works after
$scope.$watch raised.
Any help would be appreciated
There is an overload on $scope.$watch() where you can also capture the previous value of the watched property.
$scope.$watch(propertyExpression, newValue, oldValue)
Is this what you are looking for? It is hard to read your mind without code samples ;)
If you want to keep even older values, you might want to consider keeping some state in your controller.
BTW, it can be interesting to always use this overload and check first whether the object you are watching really changed using (newValue !== oldValue) because (if I recall correctly) $scope.$watch is also triggered when $scope.$apply is called on a higher level node.
See if this works,
var unregisterWatch =
$scope.$watch('param', function(newVal, oldVal) {
if (newVal === oldVal)
return; //
});
call this function when required, it will cancel Angular's watch on the variable.

$scope.$watch with non existing variable as value

I've been trying to understand $watch function on $scope object. This looks pretty straight forward but the thing that I don't understand is why (on page load) listener function is being executed when I pass a non existing $scope object variable in value function.
$scope.$watch ('nonExistingVariableIdentifier', function () { console.log('Variable value changed'); });
Am I missing something?
The watch runs when it is created.
The full use of a $watch is:
$scope.$watch("nonExistantVariable", function(newValue, oldValue) {
if (newValue == oldValue) {
// First run
}
else {
// After First run
}
})
This is the correct way to differentiate between the initialization and an actual change.
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.
From the Angular JS Docs for $watch - Docs

How can I get notifications of rootScope changes?

I've got a global variable in my rootScope for AngularJS which has properties updated in other various places (outside of angular). For example, lets say the property 'name' is updated on it. It seems like it updates on the root scope fine and after doing an apply or firing a controller function on any child function the view eventually updates, but this is a problem.
How can I get the controllers to update the templates to reflect the rootScope changes immediately?
How can I observe any changes whatsoever on this object, and invoke apply?
You can create a $watch as so:
// Assuming that $rootScope.name exists
$rootScope.$watch('name', function(newValue, oldValue)
{
// You have access to both the newValue
// and the oldValue
});
To $watch for objects, taking this from Angular's Site, a third boolean character is needed
objectEquality (optional) boolean: Compare object for equality rather
than for reference.
// Assuming that $rootScope.obj exists
$rootScope.$watch('obj', function(newValue, oldValue)
{
// You have access to both the newValue
// and the oldValue
}, true);
But know that it is not a good idea to use the $rootScope to store content. You should consider using a service or a factory for that purpose. Read more about this here.
Create a watch for it on rootscope ($rootScope.$watch('varName', function (newValue) {}[,true])).
The varName parameter accepts expressions with '.' for sub-objects and '[]' for array indexes. The third parameter indicates listening fire changes "inside" the object.

$watch waiting too long to recognize a change

This is some logic in a controller:
function newGame(){
$scope.gameOver = true;
$timeout(function(){
//do stuff
$scope.gameOver = false;
}, 3000);
}
In a directive I have:
scope.$watch(scope.gameOver,function(){ console.log("changed!", scope.gameOver);})
I'd like to do something based on scope.gameOver. I use the timeout function to give the game 3 seconds of time where gameOver = true. However, watch does nothing during those 3 seconds and instead fires off at the end of those 3 seconds where scope.gameOver has already been turned back into false.
What is the proper way of doing this?
Your $watch callback function will be invoked at least once when the $watch is set up, irrespective of whether or not your scope.gameOver variable changes.
This is pointed out in the official documentation:
After a watcher is registered with the scope, the listener fn is called asynchronously (via $evalAsync) to initialize the watcher.
I think you may be running into unexpected behaviour because you are specifying to $watch a primitive value instead of a reference to the variable holding the value of interest.
In other words,
scope.$watch(scope.gameOver, function() { ... });
as you have specified, would be the same as,
scope.$watch(true, function() { ... });
which obviously will not do anything productive.
Instead, prefer specifying your $watch using a function to return a reference to scope.gameOver or alternatively take advantage of how the variable to $watch can be an Angular expression:
// Function to return reference to the variable to watch
scope.$watch(function() { return scope.gameOver; }, function() { ... });
// Expression for Angular to evaluate
scope.$watch('gameOver', function() { ... });
Hope that helps.
Watch will be triggered only when the watch parameter changes. So in your code the $scope.gameOver
changes only at the end of 3 seconds and hence the watch is triggered.

Resources