$scope.$watch callback executes after a delay - angularjs

I have the following:
$scope.$watch('duration.dayPreference',function(value){
console.log(value);
if(value=='every')
{
that.duration.days = 1;
}
else if(value=='selected')
{
//alert('test');
that.duration.days=[];
}
else if(value=='everyday')
{
that.duration.days='everyday';
}
});
this.selectDay = function (day) {
$scope.duration.dayPreference = 'selected';
//$scope.$apply();
/*if(typeof(this.duration.days)!='object')
{
this.duration.days=[];
}*/
var index = this.duration.days.indexOf(day);
if (index == -1) {
//alert('test2');
this.duration.days.push(day);
}
else {
this.duration.days.splice(index, 1);
}
}
In this, when I do $scope.duration.dayPreference = 'selected'; I expect the line below it to have the this.duration.days set to a blank array. But it doesn't. Upon a closer inspection, I found that the callback in the $watch runs after the line below the assignment.
It may be very probable that, $watch may be using some kinda timers internally. What should be the way to do it then.

The watch won't be triggered until the digest is run. This will be after your entire function is compete.
If you consider that AngularJS is itself written in JavaScript, there would be no way for it to react to your setting of a property at the time. You are using the thread yourself. It can only wait for you to finish and then react.
As for what to do instead...
Perhaps you could call that watch function manually?
Or maybe the code which expects the empty array should belong inside the watch?

Watch will trigger on the $digest, which will occur after current cycle/code finishes running. You need to figure out a way of rearranging your code that handles things asynchronously. One possible quick solution might be:
var selectedDays = [];
$scope.$watch('duration.dayPreference',function(value){
console.log(value);
if(value=='every')
{
that.duration.days = 1;
}
else if(value=='selected')
{
//alert('test');
that.duration.days = selectedDays;
}
else if(value=='everyday')
{
that.duration.days='everyday';
}
});
this.selectDay = function (day) {
$scope.duration.dayPreference = 'selected';
var index = selectedDays.indexOf(day);
if (index == -1) {
//alert('test2');
selectedDays.push(day);
}
else {
selectedDays.splice(index, 1);
}
}

Related

Infinite Digest Loop in AngularJS filter

I have written this custom filter for AngularJS, but when it runs, I get the infinite digest loop error. Why does this occur and how can I correct this?
angular.module("app", []).
filter('department', function(filterFilter) {
return function(items, args) {
var productMatches;
var output = [];
var count = 0;
if (args.selectedDepartment.Id !== undefined && args.option) {
for (let i = 0; i < items.length; i++) {
productMatches = items[i].products.filter(function(el) {
return el.Order__r.Department__r.Id === args.selectedDepartment.Id;
});
if (productMatches.length !== 0) {
output[count] = {};
output[count].products = productMatches;
output[count].firstProduct = items[i].firstProduct;
count++;
}
}
}
return output;
};
}).
This is the relevant HTML:
<tr class='destination' ng-repeat-start='pickupAccount in pickupAccounts | department : {"selectedDepartment": selectedDepartment, "option": displayExclusive }'>
<!-- td here -->
</tr>
displayExclusive is boolean.
I have written this custom filter for AngularJS, but when it runs, I get the infinite digest loop error.
Keep in mind that filter should return array of the same object structure. When we activate filter, it fires digest cycle that will run over our filter again. If something changed in output list - fires new digest cycle and so on. after 10 attempts it will throw us Infinite Digest Loop Exception
Testing
This empty filter will works (100%). Actually we do nothing here but return the same object that filter receives.
filter('department', function(filterFilter) {
return function(items, args) {
var output = items;
return output;
};
})
Now the main idea is: write some condition to push to output objects from input list a.e. items based on some if statement, a.e.
var output = [];
if (args.selectedDepartment.Id !== undefined && args.option) {
angular.forEach(items, function(item) {
if(<SOME CONDITION>) {
output.push(item);
}
});
}
By this way it will work too.
our case:
we have this logic:
productMatches = items[i].products.filter(function(el) {
return el.Order__r.Department__r.Id === args.selectedDepartment.Id;
});
if (productMatches.length !== 0) {
output[count] = {};
output[count].products = productMatches;
output[count].firstProduct = items[i].firstProduct;
count++;
}
Here we completely modified object that has been stored in output.
So next digest cycle our items will change again and again.
Conclusion
The main purpose of filter is to filter list and not modify list object content.
Above mentioned logic you wrote is related to data manipulation and not filter. The department filter returns the same length of items.
To achieve your goal, you can use lodash map or underscorejs map for example.
This happens when you manipulate the returned array in a way that it does not match the original array. See for example:
.filter("department", function() {
return function(items, args) {
var output = [];
for (var i = 0; i < items.length; i++) {
output[i] = {};
output[i] = items[i]; // if you don't do this, the next filter will fail
output[i].product = items[i];
}
return output;
}
}
You can see it happening in the following simplified jsfiddle: https://jsfiddle.net/u873kevp/1/
If the returned array does have the same 'structure' as the input array, it will cause these errors.
It should work in your case by just assigning the original item to the returned item:
if (productMatches.length !== 0) {
output[count] = items[i]; // do this
output[count].products = productMatches;
output[count].firstProduct = items[i].firstProduct;
count++;
}
output[count] = {};
Above line is the main problem. You create a new instance, and ng-repeat will detect that the model is constantly changed indefinitely. (while you think that nothing is changed from the UI perspective)
To avoid the issue, basically you need to ensure that each element in the model remains the 'same', i.e.
firstCallOutput[0] == secondCallOutput[0]
&& firstCallOutput[1] == secondCallOutput[1]
&& firstCallOutput[2] == secondCallOutput[2]
...
This equality should be maintained as long as you don't change the model, thus ng-repeat will not 'wrongly' think that the model has been changed.
Please note that two new instances is not equal, i.e. {} != {}

Return promise inside the for loop

I have a logic like below,
getSpecificCell: function(tableObject, rowText, columnCss) {
var ele = element.all(by.repeater(tableObject)).count().then(function(count) {
for (var i = 0; i < count; i++) {
return element(by.repeater(tableObject).row(i)).getText().then(function(txt) {
if (txt.indexOf(rowText) !== -1) {
return element(by.repeater(tableObject).row(i)).element(by.css('[' + columnCss + ']'));
}
});
}
});
return ele;
}
But it is returning the value in first iteration itself.
Is that possible to return the promise inside this kind of for loop or do we have any other solution for this?
First, you don't need to use for loops with an ElementArrayFinder. That's what the each() method is for.
Second, you shouldn't need to loop at all. It sounds like you should be using filter() to get the table cells that match your specification, though I'm not sure what exactly you're trying to accomplish.
var table = element.all(by.repeater(tableObject));
// list is an ElementArrayFinder of all elements that matched the filter
var list = table.filter(function (elem) {
return elem.getText().then(function (text) {
return txt.indexOf(rowText) !== -1
})
});
// do something with list
list.count().then(function (count) {
console.log(count);
});

Promises in loop

I have the following problem in Angular JS. I have this loop:
angular.forEach(objects, function(object)
{
UpdateFactory.updateObjectInDatabase(version, object).then(function(newVersion)
{
version = newVersion;
alert("Update successful! Version number increased.);
});
});
But my problem is:
I want only to call the Factory method, if previous call is finished. Otherwise I get status code 409, because of the wrong version.
I would be pleased if someone could help me!
Best regards.
You can solve this with a recursive function that calls itself when previous request is done:
function update(objects, current) {
UpdateFactory.updateObjectInDatabase(version, objects[current]).then(function (newVersion) {
version = newVersion;
if (objects[++current]) {
update(objects, current);
}
});
}
// Start with first object
update(objects, 0);
Note: this assumes objects is an array of objects
Try this
var keys = Object.keys(objects)
var i = 0;
update(function() {
console.log("should be called when the operation end");
})
function update(cb) {
cb = (angular.isFunction(cb) ? cb : angular.noop);
if(i <= keys.length-1 ) {
i++; //increment the counter
UpdateFactory.updateObjectInDatabase(version, objects[keys[i]])
.then(function(newVersion) {
version = newVersion;
alert("Update successful! Version number increased.");
update(cb)
}, function(){
console.log("a promise return a reject");
cb();
});
} else {
cb() //Finish the operation
}
}
Only get the keys of the object and call the function when the promise ends, make a recursive call and stop when the keys ends

Angularjs freezes in loop suing $timeout to call function multiple times sequential after or less than a second?

I want to be able to call a function repeatedly every second (or faster).
Assuming "startdate" contains the starting date in the text... In my controller:
scope.arrayoftimes=[{time:XXXX},{time:XXXXXX}]
scope.startdate = $('#startingdate'); //start date comes from text in an element
scope.speed = 1000;
scope.index = 0;
scope.timer = $timeout(scope.doSomething(), scope.speed);
scope.doSomething = function() {
var currentTime = scope.startDate.html();
for (var i=scope.index; i < scope.arrayoftimes.length; i++) {
if(currentTime == roundTo(scope.arrayoftimes[i].time)) {
$rootScope.$emit('timeMatch', scope.index);
scope.index++;
break;
}
}
$timeout(scope.doSomething(), scope.speed);
}
The above code frezes my browser on execution. HOWEVER if I stick in an "alert" anywhere after the scope.index++ or inside the if condition, then it works fine (as long as I check "Prevent opening multiple dialog boxes")
scope.arrayoftimes=[{time:XXXX},{time:XXXXXX}]
scope.startdate = "//Insert Starting Date Here"
scope.speed = 1000;
scope.index = 0;
scope.timer = $timeout(scope.doSomething(), scope.speed);
scope.doSomething = function() {
var currentTime = scope.startDate;
for (var i=scope.index; i < scope.arrayoftimes.length; i++) {
if(currentTime == roundTo(scope.arrayoftimes[i].time)) {
$rootScope.$emit('timeMatch', scope.index);
scope.index++;
alert("alert"); // the existence of this alert message and checking prevent from opening more dialog boxes in chrome allows this to execute fine without freezing
break;
}
}
$timeout(scope.doSomething(), scope.speed);
}
What is going on? How can I get around this? Is there a better way to do this?
Your timeout call probably needs to be:
$timeout(function() { scope.doSomething(); }, scope.speed);
or
$timeout(scope.doSomething, scope.speed);
Otherwise you're executing doSomething() immediately which causes an infinite loop.

Trying to not use watchers in my controllers

So, i'm trying to not use watcher in my controller, but, how could i get a value when it changes?
here is my Controller(MyController) using a listener from my Service:
/**
* #param Int index
*/
myService.listener.setDrawAttentionIndex = function (idTask) {
if (lodash.isNumber(idTask) && !lodash.isNaN(idTask)) {
if ($scope.task.idFromTask === idTask) {
$scope.goal.selectedDrawAttention = true;
} else {
$scope.goal.selectedDrawAttention = false;
}
}
};
and here is my Service:
/**
* #param Int index
*/
this.setDrawAttention = function (idTask) {
if (lodash.isNumber(idTask) && !lodash.isNaN(idTask)) {
drawAttentionIndex = idTask;
that.listener.setDrawAttentionIndex(idTask);
}
};
and my possible watcher:
$scope.$watch('myService.getDrawAttention()', function (index) {
if (lodash.isNumber(index) && !lodash.isNaN(index)) {
if ($scope.task.idFromTask === index) {
$scope.goal.selectedDrawAttention = true;
} else {
$scope.goal.selectedDrawAttention = false;
}
}
});
What's better to use? I don't want use watcher in my controller because it's difficult to test, but, i don't know how i could get the value when it changes... It's a good idea to put a listener from my Service, like i did above? And what happens with my $scope variable inside my service listener?

Resources