function called multiple times from template - javascript

In my template I have something like this:
{{formatMyDate(date)}}
but date is a scope variable that is not immediately available so this expression will call the function formatMyDate() many times (returning undefined) before returning the correct value.
I could check if the date is not null within the function but I guess it would be more clean NOT to call the function at all if the date is null.
Any way to achieve this?
Would a custom filter help me out here?
EDIT:
It was suggested that this behaviour could be normal, depending on the $digest cycle.
I've then put a scope.$watch to verify how many times the value of date is changing.
Note that I'm defining these in a directive.
scope.$watch('date', function(value){
console.log('watched_date: ' + value)
})
and I've introduced a console.log() on my formatMyDate function as well
scope.formatMyDate = function(date){
console.log("called_date: " + date)
return dateService.format(date, 'YYYY-MM-DD')
}
Inspecting the console I get (pseudo code)
called_date: undefined
watched_date: undefined
called_date: undefined // many many times (around 20/30)
called_date: correctValue //2 or 3 times
watched_date: correctValue
called_date: correctValue //other 3/4 times
I'm wondering if this is still due to the $digest cycle or it is a bug in my code

I would recommend you to do things differently:
Either use the date $filter or if you are doing something VERY unique and the date $filter is not good enough for you, then you could create your own $filter, like this:
app.filter('formatMyDate', function () {
return function (date) {
if (!date) return "";
var result;
//your code here
return result;
};
});
And use it like this in your template:
{{date | formatMyDate}}
UPDATE:
I guess that I didn't quite answer your question, I just gave you advice on how to improve your code. This time I will try to answer your question:
The $digest cycle is the stage in which Angular ensures the changes of the model have settled,
so that it can render the view with the updated changes. In order to do that,
Angular starts a loop in which each iteration evaluates all the template expressions
of the view, as well as the $watcher functions of the $scope.
If in the current iteration the result is the same as the previous one,
then Angular will exit the loop. Otherwise, it will try again.
If after 10 attempts things haven't settled, Angular will exit
with an error: The "Infite $digest Loop Error" (infdig).
That's why the first time that the $digest cycle runs all the expressions are evaluated (at least) twice. And then Every time that you make a change to the $scope or that one of the $watchers of the $scope gets triggered, the $digest cycle will run again in order to make sure that things have settled, so your expressions will be evaluated again. This is how Angular makes "data-binding" happen, it's a normal behaviour.
So in your case, when in your template you do this: {{formatMyDate(date)}} or this {{date | formatMyDate}} you're defining Angular expressions that will be evaluated every time that the $digest cycle runs, which as you can imagine is very often. That's why is very important to make sure that the $filters (or functions) that you use in your view are efficient and stateless.

You can do this:
{{date && formatMyDate(date)}}
will only execute the second case if the first condition exists and is different from null and undefined.
Check this fiddle: http://jsfiddle.net/HB7LU/7512/

Related

Angular JS Error: 10 $digest() iterations reached. Aborting

I'm using angularjs-google-maps and am getting an error when trying to loop over my customers as markers on a map.
<map>
<custom-marker ng-repeat="cust in customers" position="[ {{ cust.lat }}, {{ cust.lon }} ]">
<div class="{{ cust.category }}">{{ cust.name }}</div>
</custom-marker>
</map>
The error seems to have something to do with cust.category and cust.name, as when I remove these they work fine.
This is the first couple lines of the error message I'm getting:
Watchers fired in the last 5 iterations: [["[ cust.category , cust.name ]; newVal:
[\"pro\",\"Fred\"]; oldVal: [\"pro\",\"Fred\"]","fn: function (context) {\n
try {\n for(var i = 0, ii = length, part; i<ii; i++) {\n
Full error message here.
Any help with this is appreciated. Thanks in advance!
UPDATE
Code for the custom-marker directive that's part of angular-google-maps is here.
It seems like the digest cycle is stuck in a loop. Angular has a digest cycle where they watch models (whatever is on $scope) and apply changes to the views if the models change. If in a digest cycle you're executing some function that changes a value again, you're triggering another digest cycle which triggers that same function again, changes a value on the model and triggers an infinite loop of digest cycles.
That said, you might want to add the code for your customMarker directive for answers to be more precise.
why are you binding class="{{ cust.category }}",try using ng-class.Also can you use track by $index in ng-repeat.I was getting this error when running my ionic app on ios9.They released a patch to fix this.
Code like this:
scope.$watch('[' + varsToWatch.join(',') + ']'...
looks like "[obj1.prop, obj2.prop]" after concatenation.
Expressions, like this parsed by $parse and cause to evaluate self to new instance of array every iteration, even if nothing changed inside.
You should interpolate/parse it before call $watch and write a condition, which changes only than related data really was changed.
$watch is a function(watchExp, listener, objectEquality, prettyPrintExpression)
You can try to use third parameter(objectEquality) to compare arrays by value, but not by reference equality.
#param {boolean=} objectEquality Compare for object equality using {#link angular.equals} instead of comparing for reference equality.

Why is a function in ng-repeat called several times?

I want to supply a ng-repeat element by a controller function as follows:
<div ng-repeat="picture in allPictures(data.pictures)"></div>
$scope.allPictures = function(pictures) {
alert("function called");
//return... extract all pictures and return as array
}
Result: my allPictures function is called several times, even though I'd expect it to be called only once and then iterate over the results.
Why? And moreover: how can I prevent this and really call the method only once for picture supply?
I would really avoid calling a function insides a ngRepeat attribute, since it will give errors and unexpected behaviour.
But to be honest I dont think that you would need to call a function inside a ngRepeat. I would suggest to do the following:
<div ng-repeat="picture in allPictures"></div>
$scope.getPictures = function(pictures) {
alert("function called");
//return... extract all pictures and return as array
};
$scope.allPictures = $scope.getPictures();
This way the $scope.getPictures function will get called and the $scope.allPictures will be created. ngRepeat can call that collection instead of a function.
See also my Fiddle: https://jsfiddle.net/ABr/w6kc8qyh/1/
a little bit about the digest cycle:
we need to check every time something in the application changes - what was effected by that change, and re-evaluate all the places that might depend on that change,
so that is why the function in the ng-repeat was called multiple times - it had to check wheter the repeated list is the same after some changes happend in the application
read more about the digest cycle and two-way data binding:
http://blog.bguiz.com/post/60397801810/digest-cycles-in-single-page-apps/

Why can't I overwrite the value of a variable like this?

I'm trying to figure out why I'm having trouble overwriting a value passed to an angularJS directive via an isolate scope (#). I try to overwrite the value of vm.index with the following:
vm.index = parseInt(vm.index, 10)
However, it doesn't work for some reason.
If I change it to:
vm.newIndex = parseInt(vm.index, 10)
It works. Also, assigning the value on the $scope works.
Why doesn't the first method work?
I've created this example plunker for reference.
As you used # here which need value from an attribute with {{}} interpolation directive. And seems like directive is getting loaded first & then the vm.index value is getting evaluated. So the changes are not occurring in current digest cycle. If you want those to be reflected you need to run digest cycle in safer way using $timeout.
$timeout(function(){
vm.index = parseInt(vm.index, 10)
})
Above thing is ensuring that value is converted to decimal value. The addition will occur on the on the directive html <h2>Item {{ vm.index + 1 }}</h2>
Working Demo
The possible reason behind this
As per #dsfq & my discussion we went through the angular $compile API, & found that their is one method call initializeDirectiveBindings which gets call only when we use controllerAs in directive with an isolated scope. In this function there are switch cases for the various binding #,= and & , so as you are using # which means one way binding following switch case code gets called.
Code
case '#':
if (!optional && !hasOwnProperty.call(attrs, attrName)) {
destination[scopeName] = attrs[attrName] = void 0;
}
attrs.$observe(attrName, function(value) {
if (isString(value)) {
destination[scopeName] = value;
}
});
attrs.$$observers[attrName].$$scope = scope;
if (isString(attrs[attrName])) {
// If the attribute has been provided then we trigger an interpolation to ensure
// the value is there for use in the link fn
destination[scopeName] = $interpolate(attrs[attrName])(scope);
}
break;
In above code you can clear see that there they placed attrs.$observe which is one sort of watcher which is generally used when value is with interpolation like in our case it is the same {{index}}, this means that this $observe gets evaluated when digest cycle run, That's why you need to put $timeout while making index value as decimal.
The reason #dsfq answer works because he use = provides two way binding which code is not putting watcher directly fetching value from the isolated scope, here is the code. So without digest cycle that value is getting updated.
Apparently it has something to do with one-way binding of the scope index value. So Angular won't update scope.index (or this.index in case of bindToController: true) because scope is configured as
scope: {
index: '#'
},
If you change it to two-way binding like:
scope: {
index: '='
},
It will work:
<some-directive index="$index"></some-directive>
Demo: http://plnkr.co/edit/kq16cpk7gyw8IE7HiaQL?p=preview
UPD. #pankajparkar made a good point that updating value in the next digest fixed the issue. This approach for the problem then is closer then what I did in this answer.

How to Update a Parent Scope Variable from within ng-repeat Expression?

I've run into a situation where I had to update a scope variable from within an ng-repeat expression (ng-class call within ng-repeat in fact).
<div id="page" ng-controller="MainCtrl">
.
<section id="body" ng-init="countMis = {num:0}">
.
<tbody>
<tr ng-repeat="upperCaseLetter in alphabet.specialsUpper" ng-controller="letterController">
<td>{{upperCaseLetter}}</td>
<td>{{filteredLetter=(upperCaseLetter | lowercase)}}</td>
<td>{{alphabet.specialsLower[$index]}}</td>
<td><span ng-class="lowercaseEqual($index,filteredLetter)"></span></td>
</tr>
</tbody>
and in app.js:
function letterController($scope)
{
$scope.lowercaseEqual = function(indx,letter)
{
var returnStr="";
if(letter == $scope.alphabet.specialsLower[indx])
{
returnStr = "glyphicon glyphicon-ok";
}
else
{
$scope.countMis.num = $scope.countMis.num + 1;
returnStr = "glyphicon glyphicon-remove";
}
return returnStr;
};
}
Ng-repeat is in a child controller and data I want to update is in its parent controller. I know you've read same question many times, please keep reading and see JsFiddle example.
So expression function checks filtered data of that particular ng-repeat iteration, if it doesn't match with some other corresponding parent scope variable it returns a class but also it should update parent scope counter.
As those expressions don't evaluate just once, but at least once (because of dirty check in digest) it results counter being incremented more than once for each case.
I've solved my problem by counting classes applied to that particular data after ng-repeat (you'll see in the JsFiddle example).
However I think I may run into same problem in the future, so I want to learn how to update parent scope within an expression of ng-repeat iteration.
I've put a second JsFiddle, I've tried using a factory on both parent controller and child, hoping to update a common variable for both controllers. When I log (consol.log) each iteration and factory method variable, it shows iteration times + 1 (still false value), but it's not reflected on page expression...doesn't make sense at all.
I'd appreciate any help. Thanks.
JsFiddle 1 JsFiddle 2
Since watched expressions (like the one implicitely set up by the ng-class directive) "can execute multiple times per $digest() and should be idempotent".
You should understand what the $watch() and $digest() functions do and look for resources regarding Angular's digest cycle.
So, incrementing a counter inside a watchExpression is not a good idea.
Counting classes seems ok though.
BTW, the factory-approach will help you share data across scopes, but it won't help with the "multiple executions per digest cycle" problem. Furthermore, it might be redundant in your situation, since the child- and parent-scopes can share data directly.
In any case, for the results to be displayed you have to "bind" the factory's getMismatchCount() function with the getMismatchCount() function that you use in your HTML (in Mismatches Count from Factory: {{getMismatchCount()}}):
function mainCtrl($scope, $window, repeatFactory)
...
$scope.getMismatchCount = repeatFactory.getMismatchCount;

AngularJS - Continuosly watch over a collection

I'm trying to build a real watcher for a collection in my app and, at first, I thought that Angular would provide me everything I needed.
I mean, I had the $watch, both shallow and deep. and the $watchCollection, a $digest cycle that loops over my $scope-exposed variables through the dirty checking mechanic and triggers all the watchers...
Great! What else could I need?
Wrong!
Turns out that $watchCollection gets triggered only at the first change of the watched variable...
And that's it for the mighty watchers... why???
After a reality check, I realized that I needed some kind of horrible loop to check this collection, or else I had to implement some sort of callback to do this, whenever the var gets modified.
Anybody knows how this can be done in the cleanest way possible?
Important note:
I don't why, but it seems that some horrific bug in my code was gnawing my ankles...
Now that I've fixed it, both $watchCollection(expr, foo) and $watch(expr, foo, true) works as expected...
I was mislead by this SO post , in which an user comments:
[...] I don't see anything in your code that makes the subsequent requests (to check for new messages). Where does that happen?
I took his comments as proof of my hypothesis... my bad!
I'm leaving this question as a memento
I'm pretty sure a regular $watch will do this if you utilize the 3rd parameter (objectEquality). This will check if the objects are equal and not just references.
So, you can use something like this:
$scope.$watch('prop', function(value) {
// do something
}, true);
The true value tells Angular to compare objects instead of references.
The documentation for this feature is with scope.
below solution is bit of an hacking solution and should only be used if $watchCollection does not work. rather than watching on the array, watch on json
$scope.$watch(function() {
return angular.toJson($scope.array);
},
function() {
// watch logic
}
I am using above solution to watch on multiple arrays like below:
$scope.$watch(function() {
return JSON.stringify([$scope.array1, $scope.array2]);
},
function() {
// watch logic
}
you can user either of JSON.stringify or angular.toJson.

Categories

Resources