Using Track-By With ngRepeat In AngularJS 1.2
With the release of AngularJS 1.2 earlier this week, I have to say that the feature about which I am most excited is the "track by" augmentation for ngRepeat. This feature allows you to associate a JavaScript object with an ngRepeat DOM (Document Object Model) node using a unique identifier. With this association in place, AngularJS will not $destroy and re-create DOM nodes unnecessarily. This can have a huge performance and user experience benefit.
View this demo in my JavaScript-Demos project on GitHub.
As I've blogged about before, when AngularJS renders an ngRepeat list, it injects an expando property - $$hashKey - into your JavaScript objects. It then uses this $$hashKey to associate your objects with the rendered DOM nodes. In the past, I've the hashKeyCopier library to manually manage these $$hashKey's such that I could updated the rendered collection with live data without causing unnecessary (and sometimes harmful) DOM changes.
But, no more! With the new "track by" syntax, I can now tell AngularJS which object property (or property path) should be used to associate a JavaScript object with a DOM node. This means that I can swap out JavaScript objects without destroying DOM nodes so long as the "track by" association still works.
To see this in action, we're going to render and then re-render two different ngRepeat lists. The first list will use the vanilla ngRepeat syntax. The second list will use the "track by" syntax. Then, each list item will use a tracking directive that will log DOM node creation.
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Using Track-By With ngRepeat In AngularJS
</title>
<style type="text/css">
a[ ng-click ] {
cursor: pointer ;
text-decoration: underline ;
}
</style>
</head>
<body ng-controller="AppController">
<h1>
Using Track-By With ngRepeat In AngularJS
</h1>
<h2>
Without Track-By
</h2>
<ul>
<li
ng-repeat="friend in friendsOne"
bn-log-dom-creation="Without">
{{ friend.id }} — {{ friend.name }}
</li>
</ul>
<h2>
With Track-By
</h2>
<!--
This time, we're going to use the same data structure;
however, we're going to use the "track by" syntax to tell
AngularJS how to map the objects to the DOM node.
--
NOTE: You can also use a $scope-based function like:
... track by identifier( item )
-->
<ul>
<li
ng-repeat="friend in friendsTwo track by friend.id"
bn-log-dom-creation="With">
{{ friend.id }} — {{ friend.name }}
</li>
</ul>
<p>
<a ng-click="rebuildFriends()">Rebuild Friends</a>
</p>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/jquery/jquery-2.0.3.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs/angular-1.2.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the root of the application.
app.controller(
"AppController",
function( $scope ) {
// Set up the initial collections.
$scope.friendsOne = getFriends();
$scope.friendsTwo = getFriends();
// ---
// PUBLIC METHODS.
// ---
// I rebuild the collections, creating completely new
// arrays and Friend object instances.
$scope.rebuildFriends = function() {
console.log( "Rebuilding..." );
$scope.friendsOne = getFriends();
$scope.friendsTwo = getFriends();
// Log the friends collection so we can see how
// AngularJS updates the objects.
setTimeout(
function() {
console.dir( $scope.friendsOne );
console.dir( $scope.friendsTwo );
},
50
);
};
// ---
// PRIVATE METHODS.
// ---
// I create a new collection of friends.
function getFriends() {
return([
{
id: 1,
name: "Sarah"
},
{
id: 2,
name: "Tricia"
},
{
id: 3,
name: "Joanna"
}
]);
}
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I simply log the creation / linking of a DOM node to
// illustrate the way the DOM nodes are created with the
// various tracking approaches.
app.directive(
"bnLogDomCreation",
function() {
// I bind the UI to the $scope.
function link( $scope, element, attributes ) {
console.log(
attributes.bnLogDomCreation,
$scope.$index
);
}
// Return the directive configuration.
return({
link: link
});
}
);
</script>
</body>
</html>
As you can see, all we have to do is tell the ngRepeat directive to use the "id" property to associate each Friend instance with a rendered DOM node. We can see the difference in behavior in the console output:
As you can see, during the "rebuild" action, in which we replaced the rendered collections, the vanilla ngRepeat list caused new DOM node creation with each rebuild. The "track by" ngRepeat list, on the other hand, caused no subsequent DOM activity, even when the underlying collection was replaced. This is because AngularJS knew how to associate each item with the corresponding DOM node.
Furthermore, you can see that an ngRepeat list with a "track by" clause doesn't inject the $$hashKey expando property.
I'm sure that people are frothing at the mouth to get "animation" in AngularJS 1.2. But, for me, I think this "track by" behavior is going to be the big win. When your applications get bigger and you start to deal with caching and mixing live data with cached data, the ease with which you can associate JavaScript objects with DOM nodes is going to have a huge performance payoff. Honestly, I would upgrade just for this feature.
Want to use code from this post? Check out the license.
Reader Comments
Great post, it's definitely a great feature of the new version.
One thing I realized when playing with it though is that when using filter in the repeat expression (item in items | filter:search track by item.id) it seems that elements are still destroyed and recreated in the DOM. so back to your previous post (discussing the use of ng-show instead of filtering) it is probably wise to use ng-show together with "track by".
Thanks
Another very important thing I just realized:
When using Angular Resources (resource.query(...)), the return value is actually a "future" and not the data itself, so in case the ng-repeat is bound to a property in the $scope that is actually a future, when replacing that future with a new one (by reloading the data for example) all the items will be destroyed and recreated even when using the "track by" mechanism. my guess is that it's because the future object is basically empty initially and so the track by mechanism can't find any item until it's actually filled.
As a workaround I use the success callback of the resource.query method and just set the actual result back to the property in the $scope.
something like:
myResource.query({}, function (data) {
$scope.divisionItems = data;
});
@Hadas,
Yeah, I think I remember reading that as one of the "breaking changes" in the 1.2 release (going from an empty object/array to a Promise for the resource return). To be honest, this won't affect me personally (in my current app) as we typically don't render data until is loaded. We usually have some sort of loading view:
That said, we DO deal a lot with locally-cached data and that's where I'm very interested to see if ngRepeat/track-by play nicely, which I suspect they will.
That said, my app is actually still running on AngualrJS 1.0.3 :( It makes me sad but we haven't had time to update yet.
Thanks for this article, I actually had a problem where updates were affecting the object in drag & drop (the DOM element was deleted / recreated, and the dragging element was disappearing), and the "track by" fixed it ! :)
Make sure that you put the "track by" syntax after any filters. https://github.com/tastejs/todomvc/pull/726#issuecomment-28817720
@Olivier,
Awesome! Glad to hear that (and to be on the lookout for those kinds of issues).
@Bret,
Thanks for the tip. Filters (and filter expressions) are still something that trip me up. There's something that doesn't feel natural enough for it to be committed to my brain, for some reason.
I was hoping that this will work with pagination using ng-repeat.
My use case scenario is that I have an images object.
images[0] = [im1,im2,im3] // First Page
images[1] = [im4, im5, im6] // Second page
I am displaying images in the table with pagination.
When currentPage variable is changed in scope, ng-repeat re renders the DOM using the new value of currentPage.
So my app has multiple pages.
When i move from page 1 to page 2 it creates a new DOM elements but when I move back to page 1 I want old DOM to be retained which doesn't happen.
ng-repeat again creates the DOM elements for page 1 even though I am using track by.
Can you please tell me what's wrong with my code.
While I am using track by, and changed the value of specific key after render. It doesn't reflect the changes in UI. Not sure if its a bug or I have to write some code for that ? Can you please clearify.
@Hadas,
Got the same problem and i can't manage to get it working...
messagesService.post({'content':msgContent,'email':'email@somewhere.com'},function(data) {
$scope.messages.push(data);
});
Unfortunaly data is an array for me... so i can't push it to the already fetched messages.
I mean the json response from the server is splitted into an array for each character ...
0=>"{",1=>"i",2=>"d" ... and so on instead of { "id"
any idea ?
The track by feature is a great idea, it's too bad that it doesn't works good with $resource as 90% of apps are pulling data from an API ( rest or other )
I have 600 records and in first go I am trying to use limit(10) along with ng-repeat. Plus there can be duplicates so I used 'track by $index' too.
I observed it works perfect for <50 records but slows down for 600+.
Along with this I can see many line functions of angularjs created in debugger as below.
var p;
if(s == null) return undefined;
s=((k&&k.hasOwnProperty("selectall"))?k:s)["selectall"];
return s;
I really worries if ng-repeat is performance hit and why so many inline functions are created as we go on increasing use of directives.
As a workaround I am limiting max records to 100, but it would be helpful to know performance considerations here.
Great post ...
Aside from ng-repeat performance improvement -- which I guess is the main point here -- when not destroying and creating scopes, this allows our scopes to keep track of previous states and implement state machines via watches, which just isn't possible with vanilla ng-repeat.
Great Post,it is very much helpfull..I have implemented it and i am facing one weird issue tougth...i am displaying a list of rows...and input in each rows has ng-maxlength set..when i slice the row...row gets deleted but not the validation message for that row .However,with out track by index it validation message disappears.
Great Post,it is very much helpfull..I have implemented it and i am facing one weird issue tougth...i am displaying a list of rows...and input in each rows has ng-maxlength set..when i slice the row...row gets deleted but not the validation message for that row .However,with out track by index validation message disappears.
Wonderful! Thanks for the article.
Hi Ben,
I guess the issue with filtering is worth being pointed out in the mail article because most of the time ng-repeat is used along with some sort of filtering. It's worth mentioning that 'track by' does not prevent DOM regeneration when a filter is applied.
Thanks again,
Ehsan
Hello,
Very clear demonstration, thank you very much !
How about nested ng-repeats? Suppose you display posts (using track by to their ID) and inside these posts you also display last 5 comments (using track by comment ID).
Posts themselves don't change but comments do. How would this work? Would top ng-repeat prevent updating inner comments because post ID is already present?
Hi Ben,
Just a small note on template caching using track by.
If you use $index the template get cached on the index
- causing templates to switch if e.g. deleting an item.
Example:
http://codepen.io/jakob-e/pen/MaWgPG
Thanks a million for all your great posts :-)