angularjs infinite $digest Loop when no scope changes
Asked Answered
C

2

11

I'm getting the below error in my angular code. I'm struggling to understand why the function getDrawWithResults would cause a digest cycle as there don't seem to be any side effects? It just returns items from a list that have a property set to true.

The error only occurs when the first use of getDrawWithResults is on the page, if I remove, the error stops.

Uncaught Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: [["getDrawsWithResults(selectedLottery.draws); newVal: []; oldVal: []"],["getDrawsWithResults(selectedLottery.draws); newVal: []; oldVal: []"],["getDrawsWithResults(selectedLottery.draws); newVal: []; oldVal: []"],["getDrawsWithResults(selectedLottery.draws); newVal: []; oldVal: []"],["getDrawsWithResults(selectedLottery.draws); newVal: []; oldVal: []"]]

This is my code:

HTML

<h4 ng-cloak ng-hide="getDrawsWithResults(selectedLottery.draws)">Results of Selected Lottery</h4>

<div class="con" ng-repeat="draw in getDrawsWithResults(selectedLottery.draws)" ng-cloak>
    <h5 class="con__header">[[ draw.date|date:'EEEE d MMMM yyyy - H:mm' ]]</h5>
    <div class="balls-list__outer con__row">
        <div class="balls-list">
            <div class="balls-list__ball__outer" ng-repeat="b in draw.results">
                <button class="balls-list__ball btn btn-con">[[ b ]]</button>
            </div>

        </div>
    </div>
</div>

JS

// return list of draws with results
$scope.getDrawsWithResults = function(draws) {
    return _.filter(draws, function(draw){
        return draw.results && draw.results.length > 0;
    });
}
Conduct answered 8/11, 2013 at 16:23 Comment(0)
U
14

I assume _.filter returns a new array instance everytime it is run. This causes angular's implicit $watches like:

ng-hide="getDrawsWithResults(selectedLottery.draws)"

and

ng-repeat="draw in getDrawsWithResults(selectedLottery.draws)"

to think that the model has changed so it needs to digest again.

I would implement a filter

app.filter('withResults', function() {
    return function(draws) {
        return _.filter(draws, function(draw){
            return draw.results && draw.results.length > 0;
        });
    }
})

and apply it like that (see EDIT below):

ng-hide="selectedLottery.draws | withResults"

and

ng-repeat="draw in selectedLottery.draws | withresults"

EDITED after discussion in comments

The actual problem is this binding:

ng-hide="getDrawsWithResults(selectedLottery.draws)"

ng-hide registers a watch which will fire forever since the reference of the filtered array always changes. It can be changed to:

ng-hide="getDrawsWithResults(selectedLottery.draws).length > 0"

and the corresponding filter:

ng-hide="(selectedLottery.draws | withResults).length > 0"

ng-repeat does not have the same problem because it registers a $watchCollection.

Udela answered 8/11, 2013 at 17:12 Comment(6)
This still seems to cause the same issue? Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting! Watchers fired in the last 5 iterations: [["selectedLottery.draws|withResults; newVal: []; oldVal: []"],["selectedLottery.draws|withResults; newVal: []; oldVal: []"],["selectedLottery.draws|withResults; newVal: []; oldVal: []"],["selectedLottery.draws|withResults; newVal: []; oldVal: []"],["selectedLottery.draws|withResults; newVal: []; oldVal: []"]]Conduct
Check it out here. Then, comment out the first 2 draws and see that ng-hide also works. As expected, withResults filter is called 2 times per binding. Can you make a similar plunker that reproduces the problem?Udela
Check out this version that uses getDrawsWithResults (your original code). Apparently it also works, so my assumption that the new array messes up Angular's digest was not correct. I think the problem is somewhere else. Maybe selectedLotterry or selectedLotterry.draws somehow change during the digest.Udela
I ended up fixing it by just writing a new funtion that return a boolean. I thing I could have also fixed it by doing !!getDrawsWithResults(selectedLottery.draws).length which would have also returned a booleanConduct
Ok... Now I see the bug. ng-repeat registers a $scope.$watchCollection over the selectedLottery.draws array. This means that it monitors it's elements not the actual reference. On the other hand, ng-hide registers a $scope.$watch and accepts something that evaluates to true by some heuristic tests (a private function called toBoolean. That watch will fire everytime the array reference changes, which is always so the digest fails. I will also edit my answer.Udela
@KosProv please note that there is 1.) a missing comma after app.filter('withResults'. and 2.) a missing closing bracket at the end of that code block. S.O. wont allow an edit of only 2 chars.Duodenal
O
7

This implies $scope.getDrawsWithResults() is not idempotent. Given the same input and state it doesn't consistently return the same result. And ng-hide requires an idempotent function (as do all function that Angular has a watch on).

In short, you may be better off using a function that returns a single boolean result instead of _.filter which returns an array. Perhaps _.all?

The reason idempotence matters here is because of the way Angular's $digest cycle works. Because of your ng-hide Angular places a watch on the results of your $scope.getDrawsWithResults(). This way it can be alerted whenever it should re-evaluate that ng-hide. Your ng-repeat is not affected because it's results don't need to be watched by Angular.

So every time a $digest happens (which is many times a second) $scope.getDrawsWithResults() is called to see if it's results changed from the previous $digest cycle and thus whether it should change ng-hide. If the result has changed Angular knows that could also mean some other function it's watching (which possibly uses a result from your function) could have changed. So it needs to re-run $digest (letting the change propagate through the system if need be).

And so $digest keeps running until the results of all functions it's watching stop changing. Or until there's been 10 $digest cycles. Angular assumes that if the system isn't stable after 10 cycles it probably will never stabilise. And so it gives up and throws the error message you got.

You can dive into this all in more depth here if you'd like: http://teropa.info/blog/2013/11/03/make-your-own-angular-part-1-scopes-and-digest.html

Overshine answered 8/11, 2013 at 17:2 Comment(2)
Are you stating it is not idempotent because it does not return the same array each time? Even though the array has the same contents. That makes a lot of sense to me. I'm giving Kos the answer but only because he suggests how to resolve the issue.Conduct
If think @Overshine should get the answer since he spotted that the problem was with ng-hide in particular. I thought the problem was generally the concept of binding to function calls which return different values (it's something I learned hard to avoid no matter what).Udela

© 2022 - 2024 — McMap. All rights reserved.