Handling duplicate elements in an ng-repeat


I am building an app which features a kind of "playlist". This is represented an ng-repeated custom directive with ng-repeat = "element in playlist"

Because I want to allow a user to re-use the same element twice in the playlist, I tried using the track by $index addition.

Now, what's confusing is: when I came to remove an element from the playlist (I have a function removeElement(index) which essentially contains something like this:

$scope.removeElement = function(index){
  $scope.playlist.splice(index, 1);

Something weird happened: the element was removed correctly from $scope.playlist, but for some reason the view didn't update properly. The last element appeared to be removed.

Furthermore, I couldn't properly re-order the elements in the array either.

When I removed track by $index this problem disappears, so I assume this is because when you remove an item from the array, if you're only looking at the indices, then it appears you've just deleted the last one.

The behaviour is odd though, because transcluded content is correctly removed -- see this plunker

EDIT: The above link has been modified to show the problem better and also show the answer I settled on.

The question has also been slightly edited, to make it clearer what I was getting at. KayakDave's answer below is still correct, but is more suited to an array of primitives (which my original plunker featured).

TL;DR: How do you put duplicate elements in an ng-repeat without sacrificing the ability to control their position, or remove elements correctly?

Problem courtesy of: Ed Hinchliffe


I'd like to add another answer to this question, because I discovered a simpler solution.

There's an important section of the documentation for ng-repeat which is easy to miss, specifically on the dupes error.

It states:

By default, collections are keyed by reference

After reading this, the solution was obvious - as I wasn't dealing with primitives (yes, the plunker is, but that was an over-simplification) I needed to copy the duplicate object and add its copy to the array. This means everything works as expected when you remove track by $index and just let the default behaviour take over.

Angular makes this especially easy because jqlite has a .copy. method.

Here's what I'm saying demonstrated in a plunker.

Solution courtesy of: Ed Hinchliffe


One of the big performance advantages of using track by is Angular doesn't touch any DOM element whose tracking expression hasn't changed. This is a huge performance improvement for long ng-repeat lists and one of the reasons for the addition of track by.

That performance optimization is at the root of what you're seeing.

When you use $index in track by you're telling ng-repeat to tie each DOM element it creates to it's position ($index) on the first run of ng-repeat.

So the element with color style red is tagged 0, orange 1, yellow 2 ... and finally indigo 5.

When you delete a color Angular looks at the indexes you told it to track and sees that you longer have an index #5 (since your list is one shorter than before). Therefore it removes the DOM element tagged 5- which has a color style of "indigo". You still have an index #2 so the element with the color yellow stays.

What makes it confusing is that, due to data binding, the text inside the DOM element does get updated. Thus when you delete "yellow" the DOM element with the color yellow gets the text "green".

In short what you're seeing is ng-repeat leaving the DOM element styled with yellow untouched because it's tracking value (2) still exists but data binding has updated the text inside that element.

In order to add multiple entries with the same color you need to add your a unique identifier to each entry that you use for the track by. One approach is to use key-value pairs for each entry where the key is your unique identifier. Like so:

$scope.colorlist = {1:'red', 2:'orange',3:'yellow',4:'green',5:'blue',6:'indigo',7:'yellow'};

Then track by the key as follows:

<color-block ng-repeat="(key, color) in colorlist track by key" color="{{color}}" ng-transclude>

And make sure to use that key for your delete select:

<option value="{{key}}" ng-repeat="(key,color) in colorlist">{{color}}</option>

Now the DOM element with the color styling yellow is tied to the key you specified for the "yellow" array element. So when you delete "yellow" the ng-repeat will remove the correct DOM element and everything works.

You can see it work here: http://plnkr.co/edit/cFaU8WIjliRjPI6LInZ0?p=preview

Discussion courtesy of: KayakDave

This recipe can be found in it's original form on Stack Over Flow.