angularjs watch is still being called after deregisterWatch - javascript

I'm getting a very weird behavior and I would like to know if this is the expected one or something should be changed. I have a controller with the following piece of code:
See demo here: http://fiddle.jshell.net/cqofLqwm/
UserSvc.refreshCyclic(20 * 1000);
$scope.$on('userchanged', function onUserChanged(event, user) {
stopWatchScopeUser();
$scope.model.user = user;
watchScopeUser();
});
function watchScopeUser() {
stopWatchScopeUser = $scope.$watch('model.user', function scopeUserChanged(newValue, oldValue) {
if (newValue !== oldValue) {
console.log('user changed');
}
}, true);
}
watchScopeUser();
UserSvc.refreshCyclic gets the user from server every 20 sec., and checks for user object changes. If there are changes, it raises userchanged event.
What is happening here is that even after calling stopWatchScopeUser(), when assigning user to $scope.model.user, the watch expression is called with newValue equal to oldValue, both to the most recent user. (So the condition newValue !== oldValue fails).
If I comment stopWatchScopeUser then the scopeUserChanged is called but newValue is different than oldValue.
It's quite strange that after detaching the watch on model.user, it is still being called. Is this the normal behavior or there is something incorrect? What should I do to definitely deatach watch listener?

Related

What happens behind the scenes when binding to a function in AngularJS

Can anyone explain what happens behind the scenes when you bind to a function in AngularJS? What kind of watch does it create? I have a feeling it would create two watches (in my example below) one for each property that makes up the return value. However I'm certainly not sure about this but it feels like something we shouldn't do.
e.g.
<div ng-show="vm.someFunc()">
JS
vm.someFunc = function() {
return vm.property1 || vm.property2;
}
If you created the angular scope method "vm.someFunc()", this will continuously get polled. You can verify this by setting a breakpoint in this method, it will continuously keep getting hit. If you check the task manager and show the browser running your website, the memory keeps going up and won't stop.
In my opinion, scope functions should only be used when using event triggers: click event, change event, keypressed are some of the examples.
Showing or hiding aren't events, so this is why it gets polled like that.To fix and provide the same functionality, turn this into a scope variable.
change the html tag from:
<div ng-show="vm.someFunc()">
to
<div ng-show="vm.someFunc">
And in your controller:
$scope.KeyPressed = false;
$scope.Tags = '';
then create a watch event on what you want to watch for:
//initialize showtag when page loads
$scope.vm.someFunc = $scope.KeyPressed && $scope.Tags !== '';
//watch for any new changes on keypressed
$scope.$watch('KeyPressed', function (newValue, oldValue) {
if (newValue && $scope.Tags !== '') {
$scope.vm.someFunc= true;
} else {
$scope.vm.someFunc= false;
}
}
//watch for any new changes on keypressed
$scope.$watch('Tags', function (newValue, oldValue) {
if (newValue !== "" && $scope.KeyPressed) {
$scope.vm.someFunc= true;
} else {
$scope.vm.someFunc= false;
}
}
Or you can change to a "watchCollection" instead of having multiple watches like:
$watchCollection('[KeyPressed, Tags]', function (newValue) { }
newValue[0] is the value of KeyPressed, and newValue[1] is the value of Tags
Or to go along with the accepted answer and minimize the amount of watches:
$scope.TruthyVal= function () {
return $scope.KeyPressed && $scope.Tags !== '';
};
$scope.$watch('TruthyVal', function (newValue, oldValue) {
if (newValue) {
$scope.vm.someFunc= true;
} else {
$scope.vm.someFunc= false;
}
}
In fact angular does not care what you write in html - function, variable or whatever. It takes expression as string, parse it and calculate its value each digest cycle. So, {{1 + 2}} and {{sum(1, 2)}} and {{1 | sum:2}} are actually doing same job at more or less same speed.
All three ways are legit and do not create memory leaks.
The reason why it is always not recommended to use functions in ng-show is that lot of functions are time consuming, so your digest becomes very slow. And even if your functions are fast, you are not guaranteed that they wont grow up in future.

how angularjs finds correct property from return value

angular uses watchers to detect property changes and call a listener function , a watcher declaration is for example;
$scope.food = "chicken";
scope.$watch(
function() { return food; },
function(newValue, oldValue) {
if ( newValue !== oldValue ) {
// Only increment the counter if the value changed
scope.foodCounter = scope.foodCounter + 1; }}
);
first param is only just a function with return value , but how does angular know this is the food property from return value . how is this possible
Angular will execute your function, store the result.
To see if something changed, he will simply execute the function again, and see if the value is equal to previous value.
I think
you need to pass what you want to keep in watch and whenever the object/variable changes you will have the old and newvalue
$scope.food = "chicken";
$scope.$watch(function(scope) { return scope.food },
function(newValue, oldValue) {
//your Code here.It will return the whenever food value change
}
});
Take a look at this syntax:
scope.$watch('food', function(newValue, oldValue) {
});
It's the same as doing:
scope.$watch(function() { return food; }, function(newValue, oldValue) {
});
Basically what happens, is that the function returns the value being watched and it is called for each turn of the $digest loop, that's why your counter is getting incremented.

logic behind providing the $scope object as the argument to watchers within $digest

I'm not too clear why is the $scope object provided as an argument to both the watcher as well as the listener function.
I found this bit of code on GitHub, it's the piece that iterates over the watchers array within $digest:
this.$$watchers.forEach(function(watcher) {
var newValue = watcher.watchFn(self);
var oldValue = watcher.last;
if (watcher.deep && newValue === oldValue) {
deppCompare(newValue, oldValue);
} else {
if (newValue !== oldValue) dirty = true;
}
watcher.listenerFn(newValue, oldValue, self);
watcher.last = newValue;
});
Behind the scenes, when you declare it a watch function in a certain scope, angular parse the expression given in a function:
//The code of a random watcher
$scope.$watch('myVariable', listenerFunction);
//Behind the scenes
$scope.$watch(function(scope){
return scope.myVariable;
}, listenerFunction);
When you register a watcher, the expected behaviour it's that "in THIS SCOPE, i need watch the changes for the variable attached in THIS SCOPE", therefore, behind the scenes the first argument of the $watch() parsed by angular for return the expected variable should be a variable in the scope where a watch function was attached.

How to invoke model events forcefully

I written a function, it will trigger whenever model attribute change just like in the following way.
modelEvents:{
"change:Name":"callback1"
},
callback1:function(){
console.log("This is testing");
}
Initially I set model.set("Name","hi") so automatically callback1 was invoked. Again If I set the same value into model, callback1 not triggering because model attribute not modified. so For this every time I am doing in the following.
model.set({"Name":""},{silent:true});
model.set({"Name":"hi"});
If I do like above it's working fine, but I want to know is there any option(like silent) to forcefully invoke callback.
Thanks.
If you want to go the route of passing an option then the only way to accomplish this would be to override the set method with something like this in your Model, although i haven't done testing on this to make sure it would not produce unexpected results.
set: function(key, val, options) {
//call the origonal set so everything works as normal
Backbone.Model.prototype.set.call(this, key, val, options);
current = this.attributes, prev = this._previousAttributes;
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options || (options = {});
//new option to always trigger the change on an attribute
if (options.loud) {
for (var key in attrs) {
//if the value is the same as before trigger the change
//otherwise backbone will have already triggered the chage
if (_.isEqual(prev[key] , attrs[key])) {
this.trigger('change:' + key, this, current[key], options);
}
}
}
}
then to make use of it call the normal set but pass loud: true
this.model.set({
name: "Hi"
}, {
loud: true
});
here a fiddle that makes use of it http://jsfiddle.net/leighking2/ktvj0kgp/
To show that the event is triggered even when the attribute is the same i have added an attribute called renders to show how many times it has been called.
Why don't you use Model.hasChanged for this? basically it will listen for changes in an attribute.
Take a look here.
http://backbonejs.org/#Model-hasChanged
Hope it helps

Changing A ko.observable into a ko.computed and vise versa

I am designing a Javascript-based Ohms law calculator (voltage, resistance, current) using knockout.js.
I want the ability of the user being able to select what is calculated, , e.g. voltage, resistance, or current, given the other two parameters, via radio buttons.
So my question is, can you change a ko.observable into a ko.computed and vise versa, after ko.applyBindings() has been called?
My initial attempts say no, I have tried this and slaved over the non-working code for hours, trying to get it to work.
You can't do it that way, but you can make all of them read/write ko.computeds that store a "shadow" value when written to and return that value when read from if they aren't the selected quantity (and return a calculated value if they aren't)
You dont even need a writable computed for this like ebohlman suggests
A simple demo
http://jsfiddle.net/K8t7b/
ViewModel = function() {
this.selected = ko.observable("1");
this.valueOne = ko.observable(1);
this.valueTwo = ko.observable(5);
this.result = ko.computed(this.getResult, this);
}
ViewModel.prototype = {
getResult: function() {
if(this.selected() === "1") {
return this.valueOne() - this.valueTwo();
}
return this.valueOne() + this.valueTwo();
}
};
ko.applyBindings(new ViewModel());
edit: hm, if you want the result to be presented in the correct value textbox you need to make them writable computed like ebohlman suggets
As ebohlman mentioned, the vital thing I was missing was shadow-variables, and the use of separate read/write procedures (a recently added feature to knockout) for ko.computed.
The code for one of the three variables is:
this.voltageS = ko.observable();
this.voltage = ko.computed({
read: function () {
if(this.calcWhat() == 'voltage')
{
console.log('Calculating voltage');
if(this.currentS == null)
return;
if(this.resistanceS == null)
return;
this.voltageS(this.currentS()*this.resistanceS());
return(this.currentS()*this.resistanceS());
}
else
{
console.log('Reading from voltage');
return this.voltageS();
}
},
write: function (value) {
console.log('Writing to voltage');
this.voltageS(value)
},
owner: this
});
I have created a JSFiddle here, which demonstrates being able to switch between which variable is calculated.
Another key part to this code is that on read, if it did happen to be the selected variable, as well as calculating it from the other two, I also had to write this result back to the shadow variable. This preventing some of the variables from mysteriously dissapearing/reappearing when the selected variable was changed.

Categories