Function scope messes with AngularJS Watches - javascript

I'm currently in a situation where I need to create a few watches based on the properties of an object.
These properties are used to group functions that depend on the same variable / expression.
While creating the $watch functions in the loop, it seems to well, but when the watches actually execute, only the last property is persisted in the function scope. Which means that for all $watches (which evaluate different expressions), the functions that get executed are the same.
for (var prop in obj) {
if (obj.hasOwnProperty(prop) {
$scope.$watch(function() {
return evaluateExpression(obj[prop].expression);
}, function(newVal, oldVal) {
evaluateDependentExpressions(obj[prop].dependentExpressions);
});
}
}
Now if my obj variable looks like this:
var obj = {
'Person.Name': {
expression: ...,
dependentExpressions: [...]
},
'Person.Age': {
expression: ...,
dependentExpressions: [...]
}
};
Then the function evaluateDependentExpressions is called twice in where the value of prop = 'Person.Age'.
Any help how to solve the function scope problem is greatly appreciated.
plunk

This is known problem in JavaScript prop variable is set to last used value in for (var prop in obj), simple workaround is to use IIFE:
for (var p in obj) {
(function(prop) {
// your code
if (obj.hasOwnProperty(prop) {
$scope.$watch(function() {
return evaluateExpression(obj[prop].expression);
}, function(newVal, oldVal) {
evaluateDependentExpressions(obj[prop].dependentExpressions);
});
}
})(p);
}
Explanation here: JavaScript closure inside loops – simple practical example

Related

Angular deep compare objects except for specific properties

Use case:
$scope.$watch('settings', function (newVal, oldVal) {
if (!angular.equals(oldVal, newVal)) {
setDirty();
}
}, true);
Now this is practically the same as
$scope.$watch('settings', function (newVal, oldVal) {
if (oldVal) {
setDirty();
}
}, true);
Since the $watch already compares the two.
However, there is one property that even though it changes i don't want to setDirty().
This is my working (hacky) solution so far:
$scope.$watch('settings', function (newVal, oldVal) {
if (!oldVal) return;
var editedOldVal = angular.copy(oldVal, {});
var editedNewVal = angular.copy(newVal, {});
delete editedOldVal.propertyIDontWannaWatch;
delete editedNewVal.propertyIDontWannaWatch;
if (!angular.equals(editedOldVal, editedNewVal)) {
setDirty();
}
}, true);
Is there a cleaner way to make angular.equals() or the $watch ignore specific properties?
EDIT:
This answer does not solve my problem since option 1 is not a solution at all, says that if the property that i don't want to watch hasn't changed - do nothing, but this is exactly the property i want to ignore (don't care if it changed or not). option 2 makes that property not comparable at all, i only want to ignore it on this specific case and not make it uncomparable by definition.
You can prefix properties with a $ and it will not be included in the equals comparison. The angular.equals method ignores properties that start with a $.
From the Angular.js code:
if (key.charAt(0) === '$' || isFunction(o1[key])) continue;
For example:
var object1 = {propertyOne: '1', $myCustomHiddenField: 'something'};
var object2 = {propertyOne: '1', $myCustomHiddenField: 'something else'};
var result = angular.equals(object1, object2);//this is true
The equals method can be referenced here.

Javascript pattern: ( function(){} (function(){}) )

I apologize in advance if this question is very simple, I'm a beginner in JavaScript.
I found a wealth of information about a resembling pattern (module pattern) but unless I am mistaken, this is either something different or an extension. Here is a typical code excerpt from the (wonderful) domjs project by Mariusz Nowak:
renameReserved = (function (rename) {
return function (scope) {
Object.keys(scope).forEach(rename, scope);
};
}(function (key) {
if (contains.call(reserved, key)) {
this['_' + key] = this[key];
delete this[key];
}
}));
I am finding it difficult to understand exactly what's happening here, even though each part taken independently is quite simple. Detailed help would be greatly appreciated, or a link to where I could learn more about this.
There are two functions involved here. First one
function (rename) {
return function (scope) {
Object.keys(scope).forEach(rename, scope);
};
}
And the other function object is passed as an argument to this function
function (key) {
if (contains.call(reserved, key)) {
this['_' + key] = this[key];
delete this[key];
}
}
Since we execute the first function, with an argument, (rename is the parameter which holds the function object passed) it returns another function which holds the function which we passed as argument because of the closure property.
I'm going to rewrite the code in a way that won't change what happens, but may make it a little clearer:
function makeNameReplacer( rename ) {
return function( scope ) {
Object.keys(scope).forEach(rename, scope);
}
}
function reservedWordRenamer( key ) {
if (contains.call(reserved, key)) {
this['_' + key] = this[key];
delete this[key];
}
}
renameReserved = makeNameReplacer( reservedWordRenamer );
So the first function is something that creates a function. The created function applies a name-substitution strategy to all the property names in a given object ("scope").
The second function is a strategy for replacing property names. Specifically, it checks to see if the property name passed in ("key") is in the set of reserved words. If it is, it replaces it with the name prefixed by an underscore, and removes the old property.
Thus the overall effect is that "renameReserved" becomes a function, one that takes an object as a parameter and which will scrub out property names that are reserved words.
You could come up with another strategy, and make another function. For example, if you wanted objects whose property names were all upper-case, you could do this:
function upperCaseRenamer( key ) {
var uckey = key.toUpperCase();
if (key !== uckey) {
this[uckey] = this[key];
delete this[key];
}
}
renameLowerCase = makeNameReplacer( upperCaseRenamer );

JavaScript dynamically defining properties

I'm working on a JS framework and came across something odd (or I'm missing completely obvious)
I define properties on objects using Object.defineProperty. However using this within a for loop will result in some funky values. Somehow the last property added will always be the assigned value. For example if I'd assign something to attribute one it gets assigned to attribute three. Here's an example (and here's a fiddle http://jsfiddle.net/5xLdC/)
var Test = function(){};
var props = ['one', 'two', 'three'];
for(var i = 0; i < props.length; i++) {
Object.defineProperty(Test.prototype, props[i], {
get: function() {
return this['_'+props[i]];
},
set: function(val) {
this['_'+props[i]] = val;
}
});
}
var test = new Test();
test.one = 'one';
console.log(test.three) // => 'one'
If I'd wrap this up in a forEach loop it works perfectly fine. What I'm guess at is that they all hold the same get/set functions due to scope errors (on my part?).
Could anyone explain as to why this happens?
EDIT:
Also possible to solve with a IIFE:
get:(function(y) {
return function() { return this['_'+props[y]]; }
})(i)
get and set are functions that all reference the same variable, i. By the time you call the function outside the loop, i is 3. When you use forEach, the function defining the properties gets the index or key as a parameter, which is a different entity in each call.

Watch multiple $scope attributes

Is there a way to subscribe to events on multiple objects using $watch
E.g.
$scope.$watch('item1, item2', function () { });
Starting from AngularJS 1.3 there's a new method called $watchGroup for observing a set of expressions.
$scope.foo = 'foo';
$scope.bar = 'bar';
$scope.$watchGroup(['foo', 'bar'], function(newValues, oldValues, scope) {
// newValues array contains the current values of the watch expressions
// with the indexes matching those of the watchExpression array
// i.e.
// newValues[0] -> $scope.foo
// and
// newValues[1] -> $scope.bar
});
Beginning with AngularJS 1.1.4 you can use $watchCollection:
$scope.$watchCollection('[item1, item2]', function(newValues, oldValues){
// do stuff here
// newValues and oldValues contain the new and respectively old value
// of the observed collection array
});
Plunker example here
Documentation here
$watch first parameter can also be a function.
$scope.$watch(function watchBothItems() {
return itemsCombinedValue();
}, function whenItemsChange() {
//stuff
});
If your two combined values are simple, the first parameter is just an angular expression normally. For example, firstName and lastName:
$scope.$watch('firstName + lastName', function() {
//stuff
});
Here's a solution very similar to your original pseudo-code that actually works:
$scope.$watch('[item1, item2] | json', function () { });
EDIT: Okay, I think this is even better:
$scope.$watch('[item1, item2]', function () { }, true);
Basically we're skipping the json step, which seemed dumb to begin with, but it wasn't working without it. They key is the often omitted 3rd parameter which turns on object equality as opposed to reference equality. Then the comparisons between our created array objects actually work right.
You can use functions in $watchGroup to select fields of an object in scope.
$scope.$watchGroup(
[function () { return _this.$scope.ViewModel.Monitor1Scale; },
function () { return _this.$scope.ViewModel.Monitor2Scale; }],
function (newVal, oldVal, scope)
{
if (newVal != oldVal) {
_this.updateMonitorScales();
}
});
Why not simply wrap it in a forEach?
angular.forEach(['a', 'b', 'c'], function (key) {
scope.$watch(key, function (v) {
changed();
});
});
It's about the same overhead as providing a function for the combined value, without actually having to worry about the value composition.
A slightly safer solution to combine values might be to use the following as your $watch function:
function() { return angular.toJson([item1, item2]) }
or
$scope.$watch(
function() {
return angular.toJson([item1, item2]);
},
function() {
// Stuff to do after either value changes
});
$watch first parameter can be angular expression or function. See documentation on $scope.$watch. It contains a lot of useful info about how $watch method works: when watchExpression is called, how angular compares results, etc.
how about:
scope.$watch(function() {
return {
a: thing-one,
b: thing-two,
c: red-fish,
d: blue-fish
};
}, listener...);
$scope.$watch('age + name', function () {
//called when name or age changed
});
Here function will get called when both age and name value get changed.
Angular introduced $watchGroup in version 1.3 using which we can watch multiple variables, with a single $watchGroup block
$watchGroup takes array as first parameter in which we can include all of our variables to watch.
$scope.$watchGroup(['var1','var2'],function(newVals,oldVals){
console.log("new value of var1 = " newVals[0]);
console.log("new value of var2 = " newVals[1]);
console.log("old value of var1 = " oldVals[0]);
console.log("old value of var2 = " oldVals[1]);
});

Having a closure issue. Can't seem to resolve it. Please advise

I have this code:
_trackit: function(){
for(var key in this.items.sublinks){
switch(key){
case 'shoes':
for(var innerkey in this.items.sublinks[key]){
(function(){
$(innerkey).observe('click', (function(e){
Event.stop(e);
someClass.click_link( this.items.sublinks[key][innerkey],false)
}));
)(this);
}
break;
}
}
}
The hash I am passing in has a size of 2. But as you would guess both of the links (since the hash maps to links), are passing the last hash value to come through (someClass.click_link <- in here this value, this.item.sublinks[key][innerkey]).
I've tried using an innerfuction etc... but something is messing up. If I go to "inner function deep", then this.items returns undefined.
Any help?
Since you're passing this in as an argument, you just need to create a parameter for it — call it, say, _this — and then you can refer to _this instead of this inside the function:
(function(_this, innerkey){
$(innerkey).observe('click', (function(e){
Event.stop(e);
someClass.click_link( _this.items.sublinks[key][innerkey],false)
}));
)(this, innerkey);
(There are other ways as well, but the above seems to be the way you were going for when you passed this in as an argument? And it's a perfectly respectable way to do it.)
Edited to add: Per Rob W's comment, I've edited the above to add innerkey as a parameter as well, since otherwise the inner function(e){...} expression will refer to the same innerkey variable as the outer function — a variable which, as a loop variable, is likely to have changed by the time the inner function actually runs. Passing it as a parameter gives the inner expression a new innerkey variable that's equal to what innerkey was when the inner function was created.
As others mentioned, you need to have an argument to receive the "this" you are passing. You will also need to pass copies of the "key" and "innerkey" variables, in order to avoid the closures inside for loops bug.
var make_event_listener = function(that, key, innerKey){
return function(e){
Event.stop(e);
someClass.click_link( that.items.sublinks[key][innerkey], false)
};
};
//...
for(var innerkey in this.items.sublinks[key]){
$(innerkey).observe('click', make_event_listener(this, key, innerKey) );
}
//...
OF course, you can use an anonymous version of make_event_listener instead but I find this way more readable.
The second call to "this" references the actual element being clicked. Change it to:
_trackit: function () {
var self = this;
for (var key in this.items.sublinks) {
switch (key) {
case 'shoes':
for (var innerkey in this.items.sublinks[key]) {
(function () {
$(innerkey).observe('click', (function (e) {
Event.stop(e);
someClass.click_link(self.items.sublinks[key][innerkey], false)
}));)(this);
}
break;
}
}
}
}

Categories

Resources