Skip to main content
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Ryan Anklam and Jim Walker
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Ryan Anklam Jim Walker

Using A Compound Track-By Expression With ngRepeat In AngularJS

By
Published in Comments (7)

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.

Run this demo in my JavaScript Demos project on GitHub.

If you look at the Regular Expression that AngularJS uses when parsing the ngRepeat directive value, the track-by portion looks like this:

([\s\S]+?)

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:

<!doctype html>
<html ng-app="Demo">
<head>
	<meta charset="utf-8" />

	<title>
		Using A Compound Track-By Expression With ngRepeat In AngularJS
	</title>
</head>
<body ng-controller="AppController">

	<h1>
		Using A Compound Track-By Expression With ngRepeat In AngularJS
	</h1>

	<h2>
		You have {{ $scope.friends.length }} friends
		and {{ $scope.enemies.length }} enemies.
	</h2>

	<ul>
		<!--
			When using the ngRepeat, we have to output a mixed collection of items
			that has non-unique IDs. If all we did was track-by the "id", we'd get
			the [ngRepeat:dupes] error. However, we can create a unique tracking value
			by employing a compound track-by expression that combines two different
			fields from the iteration object.
		-->
		<li
			ng-repeat="person in people track by ( person.type + person.id )"
			ng-switch="person.type">

			{{ person.name }}
			&mdash;
			( {{ person.id }} )
			<span ng-switch-when="friend">friend, yay!</span>
			<span ng-switch-when="enemy">enemy, meh.</span>

		</li>
	</ul>


	<!-- Load scripts. -->
	<script type="text/javascript" src="../../vendor/angularjs/angular-1.4.5.min.js"></script>
	<script type="text/javascript">

		// Create an application module for our demo.
		angular.module( "Demo", [] );


		// --------------------------------------------------------------------------- //
		// --------------------------------------------------------------------------- //


		// I control the root of the application.
		angular.module( "Demo" ).controller(
			"AppController",
			function AppController( $scope ) {

				$scope.friends = [
					{
						id: 1,
						name: "Kim",
						type: "friend"
					},
					{
						id: 2,
						name: "Sarah",
						type: "friend"
					},
					{
						id: 3,
						name: "Tricia",
						type: "friend"
					}
				];

				$scope.enemies = [
					{
						id: 1,
						name: "Tessa",
						type: "enemy"
					},
					{
						id: 2,
						name: "Helen",
						type: "enemy"
					}
				];

				// I hold the collection of people that we are going to output. Notice
				// that our collection contains two sets of objects that have overlapping
				// ID values, but have distinct TYPE values.
				$scope.people = $scope.friends.concat( $scope.enemies );

			}
		);

	</script>

</body>
</html>

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:

Using a compound track-by expression with ngRepat in an AngularJS application.

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.

Want to use code from this post? Check out the license.

Reader Comments

1 Comments

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.

15,902 Comments

@Ben G.,

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.

1 Comments

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.

Thanks!

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel