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;
    });
}
foiseworth
  • 951
  • 2
  • 11
  • 19

2 Answers2

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.

Kos Prov
  • 4,207
  • 1
  • 18
  • 14
  • 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: []"]] – foiseworth Nov 11 '13 at 08:56
  • Check it out [here](http://plnkr.co/edit/xEOe6Pg0akjsHiPrUySL?p=preview). 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? – Kos Prov Nov 11 '13 at 11:33
  • Check out [this version](http://plnkr.co/edit/RnO2n3EaW0IquBJzBCKC?p=preview) 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. – Kos Prov Nov 11 '13 at 11:41
  • 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 boolean – foiseworth Nov 11 '13 at 13:25
  • 1
    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. – Kos Prov Nov 11 '13 at 16:09
  • @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. – James Cazzetta Aug 18 '15 at 15:20
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

KayakDave
  • 24,636
  • 3
  • 65
  • 68
  • 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. – foiseworth Nov 11 '13 at 08:42
  • 1
    If think @KayakDave 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). – Kos Prov Nov 11 '13 at 16:34