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
Related
What is the purpose of $watch in angularjs. Can anyone explain me how it works and what is the purpose of $watch. Thanks in advance
The $scope.watch() function creates a watch of some variable. When you register a watch you pass two functions as parameters to the $watch() function:
A value function
A listener function
Here is an example:
$scope.$watch(function() {},
function() {}
);
The first function is the value function and the second function is the listener function.
The value function should return the value which is being watched. AngularJS can then check the value returned against the value the watch function returned the last time. That way AngularJS can determine if the value has changed. Here is an example:
$scope.$watch(function(scope) { return scope.data.myVar },
function() {}
);
This example valule function returns the $scope variable scope.data.myVar. If the value of this variable changes, a different value will be returned, and AngularJS will call the listener function.
The listener function should do whatever it needs to do if the value has changed. Perhaps you need to change the content of another variable, or set the content of an HTML element or something. Here is an example:
$scope.$watch(function(scope) { return scope.data.myVar },
function(newValue, oldValue) {
document.getElementById("").innerHTML =
"" + newValue + "";
}
);
This example sets the inner HTML of an HTML element to the new value of the variable, embedded in the b element which makes the value bold. Of course you could have done this using the code {{ data.myVar }, but this is just an example of what you can do inside the listener function.
Hope help ful to you.
Take a look at the Angular docs, they are generally pretty good and include examples.
$watch(watchExpression, listener, [objectEquality]);
Registers a listener callback to be executed whenever the
watchExpression changes.
https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$watch
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
I'm trying to get some graphing working with ng and d3, just having issues with digest cycle warnings.
I have a directive that does the graphing for me, and I want to filter the data when my checkboxes (that are bound to the same data) are changed.
This is the filter that I'm using:
<div data-d3-line data-chart-data="vm.chartData | filter:{show:true}" data-data-updated="vm.dataUpdated"></div>
If the filter is removed, the binding happens, no errors. I'm sure this is something simple that I'm overlooking, but it's one of those pull your hair out moments.
I put together this plunker in hopes of getting a hand:
http://plnkr.co/edit/5QVOvNKw0AqEogihcdyO
P.S. I know that I'm using a poor man's eventing with that dataUpdated watch. I was originally watching the vm.chartData, and thought that caused the error.
The filter can't be used with a two-way binding = of an isolated scope.
This is because in the two-way binding, the expression will be watched for an identity change. But everytime the expression is evaluated, the filter will produce a different (in identity) array, thus a digest cycle will go into a loop.
To solve this problem, it depends on how you use the vm.chartData.
If the d3 directive don't need to update and sync the chartData back to parent. One solution is to not use the two-way binding and manually watch the expression instead. For example:
var directive = {
scope: {
data: '&chartData' // use & instead of = here
},
link: function link(scope, element, attrs) {
scope.$watch('data()', function (newData) {
Update(angular.copy(newData));
}, true); // watch for equality, not identity (deep watch)
}
};
Or if each item of chartData will not be changed, may be using $watchCollection is enough.
scope.$watchCollection('data()', function (newData) {
Update(angular.copy(newData));
});
Example Plunker: http://plnkr.co/edit/0xArS1VAbZCwOpo4VYrw?p=preview
Hope this helps.
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.
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.