Implementing ngRepeat Track-By Using A Directive In AngularJS 1.0.8
For me, one of the most exciting features of AngularJS 1.2 is the "track by" syntax that was added to ngRepeat. This feature allows ngRepeat collections to be updated without the costly overhead of DOM (Document Object Model) node destruction and recreation. Unfortunately, I'm still using a lot of AngularJS 1.0.8 in production. Last night, however, I was laying in bed, thinking about AngularJS, as one often does, and it occurred to me that I might be able to replicate some of the "track by" features using a custom trackBy directive in AngularJS 1.0.8.
Right now, in my AngularJS 1.0.8 applications, I use my hashKeyCopier project in an attempt to decrease the amount of DOM element generation during rendering (and re-rendering). But, this is a complex process and doesn't work in situations where the previous version of a collection isn't readily available. The beauty of the "track by" syntax is that it works independently of the way in which the collections are generated.
To implement this feature using a custom directive, I need to have access to two parts of the "digest": Before ngRepeat renders the collection and after ngRepeat renders the collection. I need pre-rendering access so I can try to copy over $$hashKey values; then, I need post-rendering access so that I can extract newly-assigned $$hashKey values.
I think I have something that is working, though it wasn't heavily tested. So, please think of this as a thought-experiment more than anything else.
The way it works is that the trackBy directive is linked with a higher priority than the ngRepeat directive. This allows it to bind watcher-handlers first, which means it can inspect the given collection before ngRepeat does. Then, it uses the $evalAsync() method to hook back into the $digest phase after ngRepeat has rendered the collection. At that point, I can extract the newly assigned $$hashKeys for use in the next $digest.
If I run this page and then start rebuilding the friends collection, we'll get the following console output:
What you can see here is that with each rebuilding of the Friends collection, the plain ngRepeat recreates all of the DOM nodes while the ngRepeat with the trackBy directive only builds the newest item; the already-existing items were simply updated, not recreated.
I can't wait to upgrade to AngularJS 1.2 (or 1.3 at this point). But, for now, this thought experiment has a lot of promise. I still need to integrate it in a real application and look at the performance considerations and make sure there aren't any hidden bugs that a white-page demo isn't exposing. But, if nothing else, this was a lot of fun to try to put together.
Want to use code from this post? Check out the license.
Why not to use filters? Could you please explain, possibly link related resources?
I was having trouble finding the video that I was looking for. I can't remember if this is the one - I don't think it is. But, in this one, Misko Hevery does say that you should create an intermediary model to hold the filtered data:
In this other post, I demonstrate how often filters fire... a lot!
Hope some of that helps.
Thanks a ton, Ben! Great stuff!
I love your articles Ben, and I especially loved this one. I have a question though. How would you remove an item from the repeater when using track by friend.id? Do you mind writing a short example? Thanks!
Thanks for the kind words! In order to remove an item from the repeater, you don't have to do anything special. If you just remove it from the "friends" collection, in this case, the core AngularJS directive ngRepeat will automatically remove it form the DOM. The "Track By" isn't actually doing any of the DOM manipulation - it's just trying to copy the $$hashKey from one digest to the next.
TypeError: Cannot read property 'length' of undefined
Hi ben i'm getting above error because
var collection = collectionAccessor( scope );
Its unable to initialize collection.
Can you help ??
Is it possible that you are not using an array in the ngRepeat directive? When I wrote this, I think I only ever expected the "collection" to be an array, as in:
ng-repeat="item in ARRAY"
From the error, it sounds like it the collection doesn't have a "length" property, which makes me think it's not an array. But, that is just my best guess.