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.
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With