In AngularJS 1.2, the ngRepeat directive added the track-by expression which allows AngularJS to map DOM (Document Object Model) nodes onto view-model values. This was a boon for performance as it meant that DOM nodes didn't have to be destroyed and recreated whenever the underlying object reference changed. Typically, I use the primary-key / id of the given object as the track-by expression. But, when you're rendering a collection of mixed data, this can lead to the [ngRepeat:dupes] error. One way to get around this is by using a compound track-by expression that results in a set-wide unique value.
If you look at the Regular Expression that AngularJS uses when parsing the ngRepeat directive value, the track-by portion looks like this:
If you are not familiar with how Regular Expressions work, this basically matches anything. Which means, the track-by expression can contain just about anything in it and it will picked up by AngularJS. Under the hood, this match then gets passed through the $parse() service and is used to extract the track-by value from each object in the ngRepeat loop.
Ultimately, this means that any valid AngularJS expression can be used in the track-by portion of the ngRepeat. Which, in the case of mixed-data collections, means that we can produce a unique track-by value using an ngRepeat expression that combines multiple values.
To see this in action, I've created a demo in which we are rendering both Friends and Enemies in the same ngRepeat. Since this is an aggregation of two collections, we have overlapping ID values; but, each collection has a distinct "type". To create a set-wide unique value, for the ngRepeat, we can concat the type and the non-unique ID:
As you can see, the track-by expression in our demo is:
track by ( person.type + person.id )
While the person.id value is not unique, the compound expression is unique. This allows AngularJS to properly map the DOM node onto the underlying object. And, when we run this page, we get the appropriate output:
In previous posts, I've accomplished the same thing by calculating a "uid" and storing it in the collection itself. However, anytime we can stop storing something in the view-model, it feels like one less thing to keep track of (no pun intended). As such, compound track-by expressions feel like a nice win.
Thanks for the handy tip!
Have you considered using "track by $index"?
It will still give each repeated value a unique id (0...n), and still prevents storing data in the view model.
In this case the "$index" would definitely work since the collection isn't dynamic. But, technically speaking, since the collection isn't dynamic, you don't really even need to use the track-by expression anyway.
If the collection were ever shuffled, or if an item was added to the front (unshifted), though, the "$index" would not work as expected. It might still prevent some DOM-element destruction; but, I think you'd still get a lot of DOM manipulation and maybe some unexpected behavior.
That said, I think I could understand the use of "$index" a bit more. I think I should dig through the ngRepeat directive and see how it actually uses the track-by a bit more closely to fully understand the repercussions.
Nice article! I found an explanation here (and a solution) for a problem I didn't find elsewhere.
Awesome - glad I could help!
I just want to point out that I like your in-depth articles but the code snippets really bugs me out. The reason being the line height (vertical line spacing between lines) is way too much. What could have taken a single scroll to read entire code, now takes multiple scrolls, which is unnecessary in my view.