If you have written a production-ready Angular application before, you probably know that when displaying a list of data — at least somewhat large lists — you should be using Angular’s
trackBy feature which looks something like
More often than not, we get the data from the backend and the information uniquely identifying an item is always the same, such as a property named
id . Unfortunately, Angular forces us to write a tracking function in each component in which we want to make use of
Wouldn’t it be nice if we could just handle this entirely in the template by passing a property like this?
Or — better yet — have something like the following so that we don’t even need to duplicate the
'id' in each and every component?
Hint: If you’d just like to see the result, you can see it in action here
So how could we make the above work? First of all, it’s important to understand how this syntax even works so that we can find a way to extend it. If you’re not much interested in this part, feel free to jump ahead to the next section.
The syntax used in the
ngFor directive is microsyntax, which is briefly (but for our purposes sufficiently) documented:
The microsyntax parser takes
trackBy, title-cases them […], and prefixes them with the directive’s attribute name (
ngFor), yielding the names
ngForTrackBy. Those are the names of two
In other words, the following two snippets are equivalent and in fact this is how Angular will translate the structural directive initially:
Hint: This is also how the »of« keyword works, by transforming it to the
ngForOfdirective. »let … of …« isn’t some built-in Angular language, but instead follows the same rules!
We can now formulate a plan on how to implement our own »keyword«: we simply have to create a directive with a
Unfortunately, we are forced to prefix the name with »ngFor«, which we’d otherwise really like to avoid. Initially we might think that we could just opt for a slightly different syntax such as
but this won’t work as our directive will be placed on the wrong element when the microsyntax is desuggarized:
Another question to to ask here is which selector our directive should use. Initially, it might sound like a good idea to do it like this:
Hint: From here on out, we use the
Tgeneric to refer to a type which has an
The selectors used here are the same as
NgForOf uses itself, and we activate our directive by waiting for our input to be set. However, down the road this won’t work as expected because the input will only be set after those of
NgForOf . This poses a problem because — as of Angular 6.0.4 —
NgForOf doesn’t handle changes to the tracking function. The problem is actually still not fatal (without going into further detail here), but it’s just not ideal.
We can instead simply use
[ngTrackById] as our selector; afterall, we have just seen that Angular simply translates the microsyntax into this directive. This gives us the following skeleton to work with:
What we know by now is that somehow we want to overwrite the tracking function of the
NgForOf programmatically from within our own directive. For the reasons I briefly outlined above, we can’t use any lifecycle hooks for this, but instead rely on the constructor as it will run before the
ngForTrackBy input on the
NgForOf directive is processed.
The way we gain access to the
NgForOf directive is by simply injecting it. We also add the
@Host() decorator as we’re only interested on the host element:
Now all that is left to do is to simply overwrite the tracking function here:
And, for a first version, that’s it! Let’s see this in action.
Putting It To The Test
In the following example, the items flash briefly upon creation and also display their age in seconds (please excuse the crude styling — I’m not a designer).
As we can see, clicking the button lights up all the items and resets their age as we’re missing any tracking function. Now let’s introduce our directive and compare:
As we can see, pressing the button no longer recreates the items (even though we overwrite the list of items); et voilá — the tracking function is correctly applied!
Making It More Flexible
ngForTrackById directive is nice and useful, provided all your data has an
id field. But that might not always be the case and instead you’d like to just specify a property name.
Hint: Use caution when using this version of the directive. Specifying property names as strings breaks the type safety we get when using an actual tracking function.
We simply adapt our solution (and rename the directive in the process) to utilize a property passed in via an input.
Note that since it’ll take some time for the input to be set and our function might be called before this happens, we fall back to returning the entire item. This seems like it might cause an unnecessary round-trip, but that’s not true: it’ll only happen the first time the data is rendered, and at this point in time Angular will have to create all items anyway.
All code in this story is released under the MIT license, the same as Angular itself.