AngularJS multiple $watch cause multiple requests on controller initialization - angularjs

Consider the following code;
function ListCtrl($scope){
this.fetchResults = function(){};
// so I can pass this as a reference & spyOn fetchResults
function fetchResults = function(){return this.fetchResults()}.bind(this);
$scope.page = 1;
$scope.order = null;
$scope.$watch('page',fetchResults);
$scope.$watch('order',fetchResults);
this.fetchResults();
}
I want to write this code in this manner, so I can test if each watch triggers the fetchResults. But this way, when ListCtrl is initalized, it calls fetchResults 3 times
explicitly called
for $watch('page')
for $watch('order')
What can I do to make the ListCtrl call fetchResults only once on initialization ?

From the $scope.$watch docs:
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.
So you can do something like this:
function watchFetchResults(newValue, oldValue) {
if(newValue !== oldValue){
fetchResults();
}
}
$scope.$watch('page',watchFetchResults);
$scope.$watch('order',watchFetchResults);

You could also do:
$scope.$watch('page + order', fetch);
This would watch both expressions at once, simply concatenating them in an angular expression.

Related

$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

Angular Watch and ng-click sequence of events

I have this code inside an angular directive, and I'm finding the $watch behavior a bit confusing. The updateSelect is called in an "ng-click":
scope.updateSelect = function (type) {
scope.selectionCtrl.activeList = scope.seedLists[type];
scope.selectionCtrl.activeListKey = type;
scope.selectionCtrl.activeSelection = scope.selection[type];
scope.selectionCtrl.staged = [];
scope.selectionCtrl.stageRemove = [];
if (type !== scope.activeTab) {
scope.activeTab = type;
}
console.log("update");
};
scope.$watch('selectionCtrl.activeList', function(newValue, oldValue) {
console.log("watch");
}, true);
When I click on the button (triggering updateSelect), and watch the console, I see "update" and then "watch". The first thing that happens inside the function is selectionCtrl.activeList is set, so I would expect to see "watch" and then "update".
Shouldn't watch trigger as soon as the array has changed?
The function has to finish first as javascript is single threaded.
Because the function was called via the ng-click directive, angular will run a digest cycle. Part of that digest cycle is to run through the watch list and resolve all the changes that may have occurred since the cycle last ran.
In the example you give, selectionCtrl.activeList is changed in updateSelect which subsequently results in the watch callback being called.
When does Angular execute watch callback?
It's related to $digest and $apply, and certainly it does not execute within your raw javascript code.
To make watch execute forcefully, you can run $scope.apply() manually, but may cause more problem and not necessary if it is within a angularjs function, i.e. $timeout, $interval, etc, because it will be called automatically after the function.
For more info., lookup;
How do I use $scope.$watch and $scope.$apply in AngularJS?
https://groups.google.com/forum/#!topic/angular/HnJZTEXRztk
https://docs.angularjs.org/api/ng/type/$rootScope.Scope :
The watchExpression is called on every call to $digest() and should return the value that will be watched. (Since $digest() reruns when it detects changes the watchExpression can execute multiple times per $digest() and should be idempotent.)
If you try i.e.:
scope.selectionCtrl.activeList = scope.seedLists[type];
scope.$digest();
you'll get Error: [$rootScope:inprog] $apply already in progress.

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

Is it possible to set a value on a $watched scope property that doesn't trigger the $watch callback?

Is it possible to set count and not trigger the $watch callback?
$scope.count=1;
$scope.$watch('count',function(){...});
Thanks.
You could use setTimeout to wait for Angular's digest cycle to complete and then run your code:
setTimeout(function(){
$scope.count = 1;
},0)
This way the angular code finishes running, checks for changes and then the property is set without getting Angular involved.
No, but you can add control variable logic within your callback
$scope.$watch('count',function(){
if ($scope.bypass) return;
//else ....
});
Kind of. You can use a $watch listener function. So instead of using:
$scope.$watch('count',function(){...});
You'd use:
scope.$watch(
function() {
// return the condition you do want evaluated. Whenever this return value changes
// from the previous $digest cycle the change handler function below will be called.
// The return value will be passed in as `newValue` (and the previous as `oldValue`)
// in the change handler.
},
function(newValue,oldValue){...});
Edit: Since it looks like you're trying to watch 2 different variables, another option may be to use watchCollection which will trigger if either changes (so you could have one consolidated handler for both):
$scope.$watchCollection(['count','otherCount'], function(newValues, oldValues) { ...});

Can $scope.$watch determine equivalency of two objects?

I've built a directive that gets its data by $parse'ing from an Angular expression in one of my $attrs. The expression is typically a simple filter applied to a list model, the evaluation of which will change when the parent $scope is modified.
To monitor when it should update the data it's using, my directive is using a $scope.$watch call, with a custom function that re-$parse's the expression. The problem I'm running into is that $parse will generate a new object instance from the expression, so $watch sees the value as changed even when the data in each object is completely equivalent. This results in my code hitting the $digest iteration cap very quickly due to actions taken in the $watch callback.
To get around this I am doing the following, currently:
var getter = $parse($attrs.myExpression);
$scope.$watch(function () {
var newVal = getter($scope);
if (JSON.stringify($scope.currentData) !== JSON.stringify(newVal)) {
return newVal;
} else {
return $scope.currentData;
}
}, function (newVal) {
$scope.currentData = newVal;
// other stuff
});
However, I don't like relying on JSON as an intermediary here, nor using my $watch'ed function itself to evaluate equivalency of old and new values. Is there a flag the $watch can take to determine if two objects are equivalent, or is there a better approach for handling this kind of situation?
Hi you should use this,
scope.$watch('data', function (newVal) { /*...*/ }, true);
This has been answerd here on stackoverflow

Resources