Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Function in ng-repeat with track by causes Infinite $digest-loop

Apparently I did not yet understand the mechanic behind ng-repeat, $$hashKeys and track by.

I am currently using AngularJS 1.6 in my project.

The Problem:

I got an array of complex objects which i want to use to render a list in my view. But to get the required result i need to modify (or map/enhance/change) these objects first:

const sourceArray = [{id: 1, name: 'Dave'}, {id:2, name: Steve}]

const persons = sourceArray.map((e) => ({enhancedName: e.name + e.id})) 

//Thus the content of persons is:
//[{enhancedName: 'Dave_1'}, {enhancedName: 'Steve_2'}]

Binding this to the view should be working like this:

<div ng-repeat="person in ctrl.getPersons()">
    {{person.enhancedName}}
</div>

However this obviously runs into a $digest()-loop, because .map returns new object-instances every time it is called. Since I bind this to ng-repeat via a function, it gets reevaluated in every $digest, the model does not stabilize and Angular keeps rerunning $digest-cycles because these objects are flagged as $dirty.

Why i am confused

Now this is not a new issue and there are several solutions for this:

In an Angular-Issue from 2012 Igor Minar himself suggested to set the $$hashKey-Property manually to tell angular that the generated objects are the same. This is his working fiddle, but since even this very trivial example still ran into a $digest-loop when i used it in my project, i tried upgrading the Angular-Version in the fiddle. For some reason it crashes.

Okay... since Angular 1.3 we have track by which should essentially solve this exact problem. However both

<div ng-repeat="person in ctrl.getPersons() track by $index">   

and

<div ng-repeat="person in ctrl.getPersons() track by person.enhancedName">   

crash with a $digest-loop. I was under the impression that the track by statement should let angular believe it works with the same objects, but apparently this is not the case since it just keeps checking them for changes. To be honest, i have no idea how i can properly debug the cause of this.

Question:

Is it possible to use a filtered/ modified array as data-source for ng-repeat?

I do not want to store the modified array on my controller, because i need to update its data constantly and would then have to maintain and refresh it manually in the controller instead of relying on databinding.

like image 555
H W Avatar asked Sep 26 '17 08:09

H W


Video Answer


1 Answers

The "it crashes" fiddle that you provided, did not produce an infinite digest for me. In fact: it did not even successfully bootstrap the Angular app (looks like bootstrapping cannot be done that way in latest Angular).

I rewrote it to use an Angular bootstrap mechanism that I understood. It reproduces the crash, like you said.

I found a way to make it successfully track by stringified JSON.

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.6/angular.min.js"></script>

<script>
angular.module('myApp',[])
.controller('Ctrl', ['$scope', function($scope) {
    angular.extend($scope, {
    stringify: function(x) { return JSON.stringify(x) },
    getList: function() {
      return [
        {name:'John', age:25},
        {name:'Mary', age:28}
      ];
    }
  });
}]);
</script>

<div ng-app="myApp">

<div ng-controller="Ctrl">
  I have {{getList().length}} friends. They are:
  <ul>
    <li ng-repeat="friend in getList() track by stringify(friend)">
      [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
    </li>
  </ul>
</div>

</div>

i.e. we provide a tracking function, stringify(). There's probably an Angular builtin for that, too.

track by $index worked as well — contrary to your finding. I think JsFiddle ruined the experimentation slightly*

*The following is anecdotal. I believe that I experienced some problems with JsFiddle itself. For example: my track by stringify() example didn't work until I forked the Fiddle and tried the same code again in a new browsing context. I believe that as soon as I ever got any infinite digest: that JsFiddle would always infinite digest. It seemed like there was some statefulness that was lingering from previous runs. So, I would advise that anything you saw fail in JsFiddle, you try again in a new JsFiddle.

As for why your $$hashKey trick led to an infinite digest — I think Angular does not expect $$hashKey to be a function. So probably instead of invoking your function, it did a reference comparison of the function assigned to $$hashKey.

Since you assign to $$hashKey a new instance of the comparator each time you invoke getList(): the references could never be equal on subsequent digests, and so it would keep trying the digest forever.

EDIT: updated StackOverflow embed and JsFiddle to use HTTPS CDN (to avoid falling afoul of mixed content security).

like image 199
Birchlabs Avatar answered Oct 07 '22 12:10

Birchlabs