Watch multiple $scope attributes - javascript

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]);
});

Related

AngularJs how to use $scope in callback method

Hi I'm new to AngularJs and have some issues with the controller
For me this is working great:
var m= angular.module('m', []);
m.controller('myC', function myC($scope) {
$scope.myVal = [{/*..*/}]; //assigning values directly
});
used e.g. like this
<div data-ng-app="m">
<div ng-controller="myC">
{{myVal}}
</div>
<div>
but I have a bit more complex method to acquire the data I want to use in myVal. Therfore I try to transfer the $scope to use it in an callback method (a web request is performed there, which take some time, but values get returned!). My approach is the following:
var m= angular.module('m', []);
var s;
m.controller('myC', function myC($scope) {
s = $scope;
bigFunction("foo1", "foo2", myCallback);
});
function myCallback(a, b) {
s.myVal = b; //trying to assign the value to $scope
}
but Angular is not working anymore. The same (as above listed) html snippit is not working anymore. But myCallback is called!
Have I missed something obviously? Or: How can I access $scope in the callback method in order to use it in the Angular within an HTML page?
You could use .bind to set the value of this inside myCallback to $scope:
m.controller('myC', function myC($scope) {
bigFunction("foo1", "foo2", myCallback.bind($scope));
});
function myCallback(a, b) {
this.myVal = b; //trying to assign the value to $scope
}
Or, you could use .bind to set the first parameter:
m.controller('myC', function myC($scope) {
bigFunction("foo1", "foo2", myCallback.bind(null, $scope));
});
function myCallback($scope, a, b) {
$scope.myVal = b; //trying to assign the value to $scope
}

Function scope messes with AngularJS Watches

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

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.

Angular, watch specific keys in object?

I'm wondering if it's possible to just watch a specific key in an array of objects, so instead of
$scope.$watch('data', function (newValue) {
would it be possible to do something like
$scope.$watch('data.name', function (newValue) {
This would be in an array, so the desired functionality woud be if the any of the data[i].name items change it would fire?
Don't know of something like this would be possible. Thanks for reading!
You have 2 options:
1) Shallow watch over the Array (with $watchCollection), like this:
$scope.$watchCollection('data.name', function (newValue) {...});
2) Deep watch over the Array, like this:
$scope.$watch('data.name', function (newValue) {...}, true);
With option 1 the $watch function will only get triggered if you are adding/removing elements, but not if you are making changes into one of the Array elements.
On the other hand with option 2, the $watch function will run every time that there is any change.
Here's an example of what you're trying to do from http://blogs.microsoft.co.il/choroshin/2014/03/26/angularjs-watch-for-changes-in-specific-object-property/.
var app=angular.module('App', []);
function ctrl($scope){
$scope.count=0;
$scope.people = [{id:1,name: "bill"}, {id:2,name: "jim"}, {id:3,name: "ryan"}]
$scope.$watch(function($scope) {
return $scope.people.
map(function(obj) {
return obj.name
});
}, function (newVal) {
$scope.count++;
$scope.msg = 'person name was changed'+ $scope.count;
}, true);
}
This watches the name property on each object as you can see from the map function returning obj.name.

Angular $watch | returning the item from function

I'm interested to find out why i always have to do this
$scope.$watch( function() {
return $scope.someData;
}, function( value ) {
console.log( value );
});
for angular to actually watch the data, why do I have to do this, this is one of the things that really bug me because it looks pointless.
If I do something like this
$scope.$watch($scope.someData, function( value ) {
console.log( value );
});
Which is nicer, it never works?
I also use this a lot with factories
say that $data is a factory I have to do
$scope.$watch( function() {
return $data.someData;
}, function( value ) {
console.log( value );
});
I guess it's worth mentioning that passing a function to $watch is useful when you want to monitor a condition:
$scope.$watch(function() {
return $scope.data.length > 0;
}, function() {
// Do something every time $scope.data.length > 0 changes
});
or
$scope.$watch(function() {
return $scope.prop1 && $scope.prop2;
}, function() {
// Do something every time $scope.prop1 && $scope.prop2 changes
});
This works:
$scope.$watch("someData", function( value ) {
console.log( value );
});
With a factory, you need to watch a function because if you pass a string, Angular will evaluate it as an expression against the $scope. Since $data.someData is not defined on your $scope, it won't work.
To elaborate on #Codezilla's comment, you could assign your factory data to some $scope property, and then watch that:
$scope.data = $data.someData;
$scope.$watch('data', function(newValue) { ... });
Since the how-to-do answer is already given, I'll try to explain you what goes on actually and why it didn't work the way you tried at first time.
First of all this code sure works,
$scope.$watch(function() {
return $scope.someData;
}, function(value) {
console.log(value);
});
But this is NOT the perfect way. To be more precise $watch injects the scope in the function, like this,
$scope.$watch(function(injectedScope) {
return injectedScope.someData;
}, function(value) {
console.log(value);
});
Previously it works because $scope and injectScope are one and the same thing.
Now as this article explains,
Since $scope is passed into the watch function, it means that we can
watch any function that expects the scope as an argument and returns a
value in response. A great example of this is $interpolate
function.
So in your case, we can also make use of $interpolate as following:
$scope.$watch($interpolate("{{someData}}"), function(value) {
...
});
Now this is where we come to the short-hand method of using just a watch expression.
$watch can also accept an expression, which actually is interpolated.
Thus by providing $scope.$watch("someData", ... ),
someData will be:
interpolated as {{someData}}
using the scope injected by $watch function
This is a nice, clean, readable, short-hand way of writing the expression instead of the actual function. But finally after all such compilations, it is ultimately the function which returns a value to watch.

Categories

Resources