AngularJs Individual watches vs watchCollection - angularjs

I'm trying to optimize some parts of an app I've made, and I was wondering what would be the most efficient way to watch a set of parameters:
say we have the following object:
vm.search = {
query : '',
item1: ''
};
would it be most performant to do:
$watch() x2
$scope.$watch('vm.search.query', function(newv, oldv) {
// process value
});
$scope.$watch('vm.search.item', function (newv, oldv) {
// process value
});
Pros:
Only checking the value when it changes
only need to process the value that changed
Cons:
Need a lot of watchers if having lot's of values
$watch(, true)
$scope.$watch('vm.search', function (newVal, oldVal) {
if (!angular.equals(newVal.query, oldVal.query)) {
// process value
}
if (!angular.equals(newVal.item, oldVal.item)) {
// process value
}
}, true);
Pros:
One watcher for all values
When multiple changes at once, only one watcher is called
Cons:
Checks reference which is probably not needed in this case
need to manually check which value has changed
$watchCollection()
$scope.$watchCollection('vm.search', function (newVal, oldVal) {
if (!angular.equals(newVal.query, oldVal.query)) {
// process value
}
if (!angular.equals(newVal.item, oldVal.item)) {
// process value
}
});
Pros:
One watcher for all values
When multiple changes at once, only one watcher is called
Cons:
Doesn't check reference
Need to manually check which value has changed

If you need to watch them for different purpose, watch them in different $watch. If not you can use a $watchCollection.
Otherwise if you want to check like 5 properties you'll have a long function doing 5 different things (at least!). Better to keep them has having a single responsability.
Unless you're doing a really huge (way too huge ?) page, having some more $watch is nothing.

Related

Binding function to $interval

I have a service that I'm using to manage the state, like the time since a user last did something. In another directive, I use the interval service to check this state (lastActivity), but it's not getting updated:
// myservice
start() {
this.$document.on(this.DOMevents, this.updateActivity);
}
stop() {
this.$document.off(this.DOMevents, this.updateActivity)
}
updateActivity() {
this.lastActivity = new Date();
}
// other directive
$interval(() => {
console.log(myservice.lastActivity.getTime()); // lastActivity is just a javascript Date
}
}, 1000);
I think this is the closure problem in that $interval gets myservice before it runs every 1000 ms so the time is always the same. Is there a way to pass that in to get updated? I see in the docs https://docs.angularjs.org/api/ng/service/$interval
that there is a pass option as the last parameter, but since I'm using TypeScript and an older version of Angular, I won't be able to use this feature. Is there another way to do this?
Maybe this explains my problem clearer:
var now = new Date().getTime();
$interval(() => {
console.log(now);
}, 1000);
I have some service that is keeping track of time amongst other things. In another directive, after a certain amount of time has passed I want to check this value and do some logic. Problem is, now is always the same time. So in myservice, the lastActivity time is not getting read properly in the $interval function.

How to watch for changes in a scope variable manipulated by a service

I need to watch for changes in a countdown variable to run a task when the time is over.
I'm using a service to update the variable, as you can see in this plunker:
http://plnkr.co/edit/NJxqXcD2nhDKq4Q99Bgt
$scope.$watch('time', function (time) {
// This portion of code is reached only twice:
// Once when undefined and after first update.
console.log(time);
});
How can I watch for every change or trigger my task from the service (despite I think this is the wrong choice)?
Watcher fires only twice since reference to time not changed. You have two options: turn on object equality (slow) or specify your own value provider:
$scope.$watch('time', function () {
}, true); // Tells to check for object equality
$scope.$watch(function () {
return ($scope.time || {}).now;
}, function (now) {
// magic
});
But i think you should consider to use $timeout service and promises to run task.

AngularJS data in service trigger a calculate function

I have a multi-step wizard that binds to data within a service (wizardStateSvc). I want to have a totals element on the screen that updates whenever base values for the wizard that affect the total are updated. The calculation is a bit complex (more than shown here on my sample) so I want to have it performed in the controller. I figured a $watch would work for this but what is occuring is that the $watch function is being called once during initialization and never triggering as I update items. How do I get this $watch to trigger properly?
Totals controller:
myApp.controller('wizardTotalsCtrl', ['wizardStateSvc', '$scope', function (wizardStateSvc, $scope) {
$scope.products= wizardStateSvc.quote.products;
$scope.$watch(function() { return wizardStateSvc.quote.products; }, function(products) {
var total= 0;
products.forEach(function(product) {
total += product.price * (1 - (product.dealerDiscount * 0.01)) * product.quantity;
});
$scope.baseTotal = total;
});
}])
State Service:
myApp.service("wizardStateSvc", [function () {
var quote = {
products: [],
options: {},
customer: {},
shipping: {},
other: {}
}
return {
quote: quote
}
}]);
If the only thing that can change is the contents of the products array, i.e. products may be inserted or deleted from the array but their price does NOT change, then use $scope.$watchCollection. The code would be the same as yours, just replace $watch with $watchCollection.
Rationale: $watch checks for equality; since the products array itself does not change (i.e. products at time t1 === products at time t2), the watch is never triggered. On the other hand, $watchCollection watches the contents of the array, so it is what you want in this case.
If the price of the products may also change you need the costlier $scope.$watch(...,...,true). The true at the 3rd argument means deep watch, i.e. traverse the object hierarchy and check each nested property. Check out the docs.
Your watch watches the array 'products'. When it's initialized, products is a reference to an array, and when you add values, products remains is still a reference to the same array, it's only the array content which is different, so there really is no reason for your watch to invoke the function again. This problem has two solution:
Not so good solution: Watch the length of products, which will make the watch get called whenever the length of products change.
$scope.$watch(function() { return wizardStateSvc.quote.products.length; }, ...);
This is problematic in the use case where you add one item and remove another immediately afterwards. If before this action the value of the watch is x, it will be x after your action, and thus won't invoke.
Better solution: Use watch collection instead, which handles also the use cases the watching the length doesn't.
$scope.$watchCollection('products', ...);
From the docs (scroll to the $watchCollection part):
https://docs.angularjs.org/api/ng/type/$rootScope.Scope

How to find the call site that changed an object being watched

AngularJS allows listening for changes of an object and will call the supplied callback function supplied to the $watch function. With a largish library using AngularJS like ngGrid objects are frequently changed that's being "watched".
Once the watch callback is invoked, how can one trace back the call site that caused the change to the object?
Without knowing what caused the change, and so caused the watch handler to be invoked, it is proving very difficult to debug a library like ngGrid. I'm setting breakpoints everywhere I can foresee the variable could be changed, and then trying to build a graph for the execution pipeline to follow the chain of events that lead to an object being changed.
You simply can't do that. $watch will just add a callback to check whether the object changed, to be run during digests.
I guess that's one of the main differences with frameworks like Backbone where you extend a Model object.
That being said, you might have better luck trying to $scope.$digest(); intentionally (updating the model, and firing the watchers), but it's a stretch...
Update
The problem is you're thinking there's a correlation between watches and model changes, but there simply isn't. Adding a watch just adds something to be checked when the digest loop runs.
This loop isn't triggered by changes to something on a $scope, but rather calls to $scope.$apply, or directly calling $scope.$digest.
Note that most (all?) of Angular's directives and related components call $scope.$apply on your behalf. For example, the reason why $timeout and ngClick work as expected, is because they run $scope.$apply internally after executing your callbacks.
Update II
If you're merely interested in finding the call site, could something like this help you?
$scope.foo = {
get bar () { return getting(); },
set bar (value) { setting(value); }
};
var bar;
function setting (value) {
var stack = getStack();
console.log(value, stack);
bar = value;
}
function getting () {
console.log(getStack());
}
function getStack () {
try {
throw new Error('foo');
} catch (e) {
return e.stack || e;
}
}
Well, you may have found the answer by now, but because I was looking for it, too, and could not find it so I am posting the answer here.
Let's assume your javascript variable is name, and you would like to find who changed that.
The method I have found is this:
Change name to name_
Add getter: get name() { return this.name_ ; }
Add setter: set name(value) { this.name_ = value ; }
Put breakpoint in the setter, and watch the call stack when breakpoint is triggered.
Example for a structure:
Before:
scope = {
name:'',
};
After:
scope = {
name_:'',
get name() { return this.name_; },
set name(value) {
this.name_ = value;
alert('Changed!');
},
};
I hope this will help you and others!

$watch not being triggered on array change

I'm trying to figure out why my $watch isn't being triggered. This is a snippet from the relevant controller:
$scope.$watch('tasks', function (newValue, oldValue) {
//do some stuff
//only enters here once
//newValue and oldValue are equal at that point
});
$scope.tasks = tasksService.tasks();
$scope.addTask = function (taskCreationString) {
tasksService.addTask(taskCreationString);//modifies tasks array
};
On my view, tasks is clearly being updated correctly as I have its length bound like so:
<span>There are {{tasks.length}} total tasks</span>
What am I missing?
Try $watch('tasks.length', ...) or $watch('tasks', function(...) { ... }, true).
By default, $watch does not check for object equality, but just for reference. So, $watch('tasks', ...) will always simply return the same array reference, which isn't changing.
Update: Angular v1.1.4 adds a $watchCollection() method to handle this case:
Shallow watches the properties of an object and fires whenever any of the properties change (for arrays this implies watching the array items, for object maps this implies watching the properties). If a change is detected the listener callback is fired.
Very good answer by #Mark. In addition to his answer, there is one important functionality of $watch function you should be aware of.
With the $watch function declaration as follows:
$watch(watch_expression, listener, objectEquality)
The $watch listener function is called only when the value from the current watch expression (in your case it is 'tasks') and the previous call to watch expression are not equal. Angular saves the value of the object for later comparison. Because of that, watching complex options will have disadvantageous memory and performance implications. Basically the simpler watch expression value the better.
I would recommend trying
$scope.$watch('tasks | json', ...)
That will catch all changes to the tasks array, as it compares the serialized array as a string.
For one dimensional arrays you may use $watchCollection
$scope.names = ['igor', 'matias', 'misko', 'james'];
$scope.dataCount = 4;
$scope.$watchCollection('names', function(newNames, oldNames) {
$scope.dataCount = newNames.length;
});

Resources