ng-repeat too many iterations - angularjs

I am using angular-meteor and would like to perform a function on each object. I tried running this function within an ng-repeat in the view, but I am getting massive amounts of function calls and can't figure out why. I tried to make it as simple as possible to demonstrate what is going on.
constructor($scope, $reactive) {
'ngInject';
$reactive(this).attach($scope);
this.loaderCount = 0;
this.helpers({
loaders() {
return Loaders.find( {isloader:true}, {sort: { name : 1 } })
}
});
That gives me 26 Loaders. My function just adds 1 to the count every time the function is called:
displayLoaderCount()
{
return ++this.loaderCount;
}
Now in my view, I am looping through each loader, and calling the function. This should in my mind give me 26, but instead I am getting 3836.
<tr ng-repeat="loader in loaderExhaustion.loaders">
<td>{{loaderExhaustion.displayLoaderCount()}}</td>
Can anyone help explain this to me? Ideally I would like to loop over the contents in my module but as the collection is async, when the loop starts the length of the collection is 0, hence why I made the call in the view.
THANKS!

Every time angular enters a change detection cycle, it evaluates loaderExhaustion.displayLoaderCount(), to know if the result of this expression has changed, and update the DOM if it has. This function changes the state of the controller (since it increments this.loaderCount), which thus triggers an additional change detection loop, which reevaluates the expression, which changes the state of the controller, etc. etc.
You MAY NOT change the state in an expression like that. For a given state, angular should be able to call this function twice, and get the same result twice. Expressions like these must NOT have side effects.
I can't understand what you want to achieve by doing so, so it's hard to tell what you should do instead.

Related

AngularJS: Nested http call doesn't update the view

In a particular scenario, I need to call the the github api to retrieve a specific user's info. Then issue a second call to retrieve the user's repositories:
search(login: string): void {
this.user = undefined;
// first call
this.githubApi.getUser(login)
.then(user => {
this.user = user;
// second call
this.githubApi.getRepos(user.repos_url)
.then(reposResponse => {
this.repos = reposResponse.repos;
// I don't like to call this.$scope.$apply() !!;
});
});
}
The first call gets executed and the bound elements to this.user gets updated with no problem in the view.
The second call gets executed and the result is returned successfully and this.repos is set correctly. But, the bound elements on the view are not updated.
If I call this.$scope.$apply() in the very last line of the second callback, it makes the view update work but I guess this is not correct approach.
Any solution?
Well, if you are not willing to use $scope.apply();, try updating your getRepos service response code with:
setTimeout(
() => {
this.repos = reposResponse.repos;
}, 0
)
First you need to know , why Angular-Js is not updating the view.
You have used $scope.$apply(), so I'm assuming you already know , how it works and why we use it. Now , to the problem!
Sometimes when you make a callback - nested callback in particular - Angular does not update the view. Sometimes angular thinks that it does not need to update the view because of callbacks. And the watchers do not take action when the value changes of the variable that they are watching.
Then you use $scope.$apply() to run the digest cycle again (assuming you already know the digest cycle if you don't then let me know). And it makes the watchers to update the view.In your case, digest cycle is not running, that is why angular is not updating the view. If your digest cycle was running , angular would have given you error. So, it will tell angular to run digest cycle again because two-way binding is not working properly.
I don't think there is another way. But if there is a way, I would love to know that way. Also its not a bad approach. It was made for these kind of problems.

Need explanation on a angular direcive load

I just want to understand why in the following jsFiddle 'here is a lo' is printed three times.
http://jsfiddle.net/wg385a1h/5/
$scope.getLog = function () {
console.log('here is a log');
}
Can someone explain me why ? What should I change to have only one log "here is a log" (that's what I would like this fiddle do). Thanks a lot.
Angular uses digest cycles/iterations to determine when state has changed and needs to update the UI. If it finds any change on one of it's cycles, it keeps rerunning cycles until the data stabilizes itself. If it's done 10 cycles and the data is still changing, you'll see a rather know message: "angularjs 10 iterations reached. aborting".
Therefor, The fact that you are seeing the message displayed 3 times is because you have a simple interface. In fact, you can get up to many more such messages in the log, due to the fact that your directive uses {{getLog()}}. Angular keeps evaluating the expression to see if it changed.
To avoid such problems, under normal circumstances, you should store the value returned by the function you want called only once in the $scope object inside the controller and use that variable (not the function call) in the UI.
So in the controller you'd have $scope.log = getLog() [assuming it returns something, and not just writing to the console] and in the directive use the template {{log}}. This way, you'll get the value only once, per controller instance.
Hope I was clear enough.

Non idempotent filter causing infinite $digest error

Trying to understand the nature of Angular filters. So I have this:
<p>RandomCase: {{ aString | randomCase }}</p>
and this:
.filter 'randomCase', () ->
(input) ->
input.replace /./g, (c) ->
if Math.random() > 0.5 then c.toUpperCase() else c
Coffeescript makes for a cleaner code here, JS version is found in JSFiddle along with the complete example:
http://jsfiddle.net/nmakarov/5LdKV/
The point is to decorate a string by having random letters capitalized.
it works, but throws "10 $digest() iterations reached. Aborting!" most of the time. I figured that for some reason Angular would re-run the filter at least twice to see that outputs are the same. And if not, will run it again until the last two matches. Indeed, since the filter's code produces a random string, it is quite unlikely it will repeat itself twice in a row.
Now to the question: is it possible to tell Angular not to re-run this filter more than once? I do not need to observe the value of this filtered output in the code, so no need for Angular to watch the changes - even if a hardcoded "string" be used in place of an aString variable, the code behaves the same - 10 iterations reached...
And I know that I can put the randomizing logic in a controller and bind the result to a $scope.aString and it would just work - I'm trying to understand the Angular way of filters.
Cheers.
There is no way to use an non-idempotent filter in a watched expression without a hack. This the simplest one that I can think of, which will make the filter idempotent...
Use a memoizing function to ensure that subsequent calls to the filter passing the same arguments return the same result.
Example using Underscore:
myApp.filter('randomCase', function() {
return _.memoize(function (input) {
console.log("random");
return input.replace(/./g, function(c) {
if (Math.random() > 0.5) {
return c.toUpperCase();
} else {
return c;
}
});
});
});
Updated Fiddle
The filter itself will only run when an expression with the | operator (e.g. someVar | someFilter) is evaluated. It is Anuglar's dirty checking that causes the expression to be evaluated multiple times.
In short, Angular runs the expression aString | randomCase over and over until it doesn't change. At that point it knows what to put into the DOM. To prevent infinite looping when that value doesn't stop changing, it throws the infinite $digest error.
For this reason filters always run at least twice. Once to get the initial value, and then a second time to compare it against that first value.
By putting the randomizing logic in the controller, you would then have something like {{randomizedString}} in your HTML. The value of randomizedString wouldn't change from the first time it was evaluated, and thus would accomplish your end goal without hitting the infinite $digest error.

Angular filter works but causes "10 $digest iterations reached"

I receive data from my back end server structured like this:
{
name : "Mc Feast",
owner : "Mc Donalds"
},
{
name : "Royale with cheese",
owner : "Mc Donalds"
},
{
name : "Whopper",
owner : "Burger King"
}
For my view I would like to "invert" the list. I.e. I want to list each owner, and for that owner list all hamburgers. I can achieve this by using the underscorejs function groupBy in a filter which I then use in with the ng-repeat directive:
JS:
app.filter("ownerGrouping", function() {
return function(collection) {
return _.groupBy(collection, function(item) {
return item.owner;
});
}
});
HTML:
<li ng-repeat="(owner, hamburgerList) in hamburgers | ownerGrouping">
{{owner}}:
<ul>
<li ng-repeat="burger in hamburgerList | orderBy : 'name'">{{burger.name}}</li>
</ul>
</li>
This works as expected but I get an enormous error stack trace when the list is rendered with the error message "10 $digest iterations reached". I have a hard time seeing how my code creates an infinite loop which is implied by this message. Does any one know why?
Here is a link to a plunk with the code: http://plnkr.co/edit/8kbVuWhOMlMojp0E5Qbs?p=preview
This happens because _.groupBy returns a collection of new objects every time it runs. Angular's ngRepeat doesn't realize that those objects are equal because ngRepeat tracks them by identity. New object leads to new identity. This makes Angular think that something has changed since the last check, which means that Angular should run another check (aka digest). The next digest ends up getting yet another new set of objects, and so another digest is triggered. The repeats until Angular gives up.
One easy way to get rid of the error is to make sure your filter returns the same collection of objects every time (unless of course it has changed). You can do this very easily with underscore by using _.memoize. Just wrap the filter function in memoize:
app.filter("ownerGrouping", function() {
return _.memoize(function(collection, field) {
return _.groupBy(collection, function(item) {
return item.owner;
});
}, function resolver(collection, field) {
return collection.length + field;
})
});
A resolver function is required if you plan to use different field values for your filters. In the example above, the length of the array is used. A better be to reduce the collection to a unique md5 hash string.
See plunker fork here. Memoize will remember the result of a specific input and return the same object if the input is the same as before. If the values change frequently though then you should check if _.memoize discards old results to avoid a memory leak over time.
Investigating a bit further I see that ngRepeat supports an extended syntax ... track by EXPRESSION, which might be helpful somehow by allowing you to tell Angular to look at the owner of the restaurants instead of the identity of the objects. This would be an alternative to the memoization trick above, though I couldn't manage to test it in the plunker (possibly old version of Angular from before track by was implemented?).
Okay, I think I figured it out. Start by taking a look at the source code for ngRepeat. Notice line 199: This is where we set up watches on the array/object we are repeating over, so that if it or its elements change a digest cycle will be triggered:
$scope.$watchCollection(rhs, function ngRepeatAction(collection){
Now we need to find the definition of $watchCollection, which begins on line 360 of rootScope.js. This function is passed in our array or object expression, which in our case is hamburgers | ownerGrouping. On line 365 that string expression is turned into a function using the $parse service, a function which will be invoked later, and every time this watcher runs:
var objGetter = $parse(obj);
That new function, which will evaluate our filter and get the resulting array, is invoked just a few lines down:
newValue = objGetter(self);
So newValue holds the result of our filtered data, after groupBy has been applied.
Next scroll down to line 408 and take a look at this code:
// copy the items to oldValue and look for changes.
for (var i = 0; i < newLength; i++) {
if (oldValue[i] !== newValue[i]) {
changeDetected++;
oldValue[i] = newValue[i];
}
}
The first time running, oldValue is just an empty array (set up above as "internalArray"), so a change will be detected. However, each of its elements will be set to the corresponding element of newValue, so that we expect the next time it runs everything should match and no change will be detected. So when everything is working normally this code will be run twice. Once for the setup, which detects a change from the initial null state, and then once again, because the detected change forces a new digest cycle to run. In the normal case no changes will be detected during this 2nd run, because at that point (oldValue[i] !== newValue[i]) will be false for all i. This is why you were seeing 2 console.log outputs in your working example.
But in your failing case, your filter code is generating a new array with new elments every time it's run. While this new array's elments have the same value as the old array's elements (it's a perfect copy), they are not the same actual elements. That is, they refer to different objects in memory that simply happen to have the same properties and values. Hence in your case oldValue[i] !== newValue[i] will always be true, for the same reason that, eg, {x: 1} !== {x: 1} is always true. And a change will always be detected.
So the essential problem is that your filter is creating a new copy of the array every time it's run, consisting of new elements that are copies of the original array's elments. So the watcher setup by ngRepeat just gets stuck in what is essentially an infinite recursive loop, always detecting a change and triggering a new digest cycle.
Here's a simpler version of your code that recreates the same problem: http://plnkr.co/edit/KiU4v4V0iXmdOKesgy7t?p=preview
The problem vanishes if the filter stops creating a new array every time it's run.
New to AngularJS 1.2 is a "track-by" option for the ng-repeat directive. You can use it to help Angular recognize that different object instances should really be considered the same object.
ng-repeat="student in students track by student.id"
This will help unconfuse Angular in cases like yours where you're using Underscore to do heavyweight slicing and dicing, producing new objects instead of merely filtering them.
Thanks for the memoize solution, it works fine.
However, _.memoize uses the first passed parameter as the default key for its cache. This could not be handy, especially if the first parameter will always be the same reference. Hopefully, this behavior is configurable via the resolver parameter.
In the example below, the first parameter will always be the same array, and the second one a string representing on which field it should be grouped by:
return _.memoize(function(collection, field) {
return _.groupBy(collection, field);
}, function resolver(collection, field) {
return collection.length + field;
});
Pardon the brevity, but try ng-init="thing = (array | fn:arg)" and use thing in your ng-repeat. Works for me but this is a broad issue.
I am not sure why this error is coming but, logically the filter function gets called for each element for the array.
In your case the filter function that you have created returns a function which should only be called when the array is updated, not for each element of the array. The result returned by the function can then be bounded to html.
I have forked the plunker and have created my own implementation of it here http://plnkr.co/edit/KTlTfFyVUhWVCtX6igsn
It does not use any filter. The basic idea is to call the groupBy at the start and whenever an element is added
$scope.ownerHamburgers=_.groupBy(hamburgers, function(item) {
return item.owner;
});
$scope.addBurger = function() {
hamburgers.push({
name : "Mc Fish",
owner :"Mc Donalds"
});
$scope.ownerHamburgers=_.groupBy(hamburgers, function(item) {
return item.owner;
});
}
For what it's worth, to add one more example and solution, I had a simple filter like this:
.filter('paragraphs', function () {
return function (text) {
return text.split(/\n\n/g);
}
})
with:
<p ng-repeat="p in (description | paragraphs)">{{ p }}</p>
which caused the described infinite recursion in $digest. Was easily fixed with:
<p ng-repeat="(i, p) in (description | paragraphs) track by i">{{ p }}</p>
This is also necessary since ngRepeat paradoxically doesn't like repeaters, i.e. "foo\n\nfoo" would cause an error because of two identical paragraphs. This solution may not be appropriate if the contents of the paragraphs are actually changing and it's important that they keep getting digested, but in my case this isn't an issue.

AngularJS $watch inexplicably changing value of the watched value

I'm trying to implement an animdated version of ng-show and ng-hide; I originally tried to use jQueryUI.toggle('slide', …), but since $watch fires multiple times, my elements were toggling in and then immediately toggling out (sometimes more than once). But I saw in AngularJS's github issues that that is $watch's intended behaviour (dirty checking).
So I thought, okay this little eff-you be damned, I'll explicitly show or hide instead of the simple toggle: I broke it down to check the value of $watch's newValue like so:
scope.$watch(condition, function myShowHideAction(newValue,oldValue) {
if ( newValue !== oldValue ) {
if (newValue) {
elm.show("slide", { direction: direction }, "slow");
} else {
elm.hide("slide", { direction: direction }, "slow");
}
}
}
Fire it up, and what do I see? Toggling!
After putting in a few console logs, I discover somehow my condition's value is being changed during $watch's loop (which iterates like 6 times instead of just once, but that should be neither here nor there). However, the actual scope parameter's value does not change (as it shouldn't) half way thru like newValue's does.
What the heck is going on?
Plunker
The reason this happens is because all your infoboxes share the same scope. If you put a console.log(scope) statement in your mySlide directives linking function, you'll see that it's created several times with the same scope. So you have multiple watches for the same condition in the same scope.
Your code isn't the easiest to follow I'm afraid, but the issue seems to be inside my-map.js on the line 87. Instead of doing $compile(elm.contents())(scope);, you should probably do $compile(elm.contents())(scope.$new()); to create an isolated scope for that info box.

Resources