https://toddmotto.com/angular-ngfor-template-element#lttemplategt-element
Angular ngFor,<template> and the compiler
Angular ngFor
is a built-in Directive that allows us to iterate over a collection. This collection is typically an array,however can be “array-like”. To demonstrate this,we’ll be usingRx.Observable.of()
to initialise our collection with an Observable instead of a static array.
We’ll also be exploring some other under-the-hood properties of ngFor
,as well as looking at how Angular expands our ngFor
to a <template>
element and composes our view.
Table of contents
Using the ngFor directive
In this section,we’ll be taking a look at the featureset ngFor
provides,as well as some use case examples.
NgModule import
First of all,to use ngFor
,you need to import theCommonModule
. However,if you’re using theBrowserModule
in the root module,you won’t need to import it,as theBrowserModule
exposes the CommonModule
for us - so you only need to import the CommonModule
when creating further modules with @NgModule
.
Our @NgModule
will look like this:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
bootstrap: [AppComponent]
})
export class AppModule {}
For this article,we’ll be including a further ContactCardComponent
component in our @NgModule
:
// ...
import { ContactCardComponent } from './contact-card.component';
@NgModule({
declarations: [
AppComponent,
ContactCardComponent
],
// ...
})
export class AppModule {}
Our ContactCardComponent
simply looks like this,taking a single@Input
of contact
:
import { Component, Input } from '@angular/core';
import { Contact } from './models/contact.interface';
@Component({
selector: 'contact-card',
template: `
<div class="contact-card">
<p>{{ contact.name }} ( {{ contact.age }} )</p> <p>{{ contact.email }}</p> </div> `
})
export class ContactCardComponent {
@Input()
contact: Contact;
}
So now we’re all setup,what’s next?
Iterating collections
Now that our ContactCardComponent
is included in our module,we can setup ourAppComponent
to use this dataset:
@Component({...})
export class AppComponent implements OnInit {
contacts: Observable<Contact[]>;
ngOnInit() {
this.contacts = Observable.of([
{
"id": 1,
"name": "Laura",
"email": "lbutler0@latimes.com",
"age": 47
},
{
"id": 2,
"name": "Walter",
"email": "wkelley1@goodreads.com",
"age": 37
},
{
"id": 3,
"email": "wgutierrez2@smugmug.com",
"age": 49
},
{
"id": 4,
"name": "Jesse",
"email": "jarnold3@com.com",
{
"id": 5,
"name": "Irene",
"email": "iduncan4@oakley.com",
"age": 33
}
]);
}
}
As mentioned in the introduction,I’m using Observable.of
here from RxJS to give me an Observable stream from the results,this is a nice way to mimic an Observable response,such as when using the AngularHttp
module to return data from your API.
Using ngFor
Now we’re setup,we can look into our AppComponent
template:
@Component({
selector: 'app-root',
template: `
<div class="app">
<ul>
<li>
<contact-card></contact-card> </li> </ul> </div> `
})
You can see I’m declaring <contact-card>
inside of here,as we want to iterate our dataset and populate each contact via the@Input
setup inside our ContactCardComponent
.
One way we could do this is using ngFor
on the component itself,however for simplicity we’ll use the unordered list. Let’s addngFor
:
<ul>
<li *ngFor="let contact of contacts">
<contact-card></contact-card>
</li>
</ul>
There are a few things happening here,the first you’ll notice a *
character at the beginning of the ngFor
,we’ll come onto what this means in the next section when we look at the<template>
element. Secondly,we’re creating a context calledcontact
,using a “for of” loop - just like in ES6.
The ngFor
Directive will clone the <li>
and the child nodes. In this case,the <contact-card>
is a child node,and a card will be “stamped out” in the DOM for each particular item inside ourcontacts
collection.
So,now we have contact
available as an individual Object,we can pass the individualcontact
into the <contact-card>
:
<ul>
<li *ngFor="let contact of contacts">
<contact-card [contact]="contact"></contact-card>
</li>
</ul>
If you’re using a static array,or binding the result of an Observable to the template,you can leave the template as it currently is. However,we can optionally bind the Observable directly to the template,which means we’ll need theasync
pipe here to finish things off:
<ul>
<li *ngFor="let contact of contacts | async">
<contact-card [contact]="contact"></contact-card>
</li>
</ul>
You can check a live output of what we’ve covered so far here:
Using trackBy for keys
If you’re coming from an Angular 1.x background,you’ll have likely seen “track by” when using anng-repeat
,and similarly in React land,usingkey
on a collection item.
So what do these do? They associate the objects,or keys,with the particular DOM nodes,so should anything change or need to be re-rendered,the framework can do this much more efficiently. Angular’sngFor
defaults to using object identity checking for you,which is fast,but can befaster!
This is where trackBy
comes into play,let’s add some more code then explain:
<ul>
<li *ngFor="let contact of contacts | async; trackBy: trackById;">
<contact-card [contact]="contact"></contact-card>
</li>
</ul>
Here we’ve added trackBy
,then given it a value oftrackById
. This is a function that we’ll add in the component class:
trackById(index, contact) {
return contact.id;
}
All this function does is use a custom tracking solution for our collection. Instead of using object identity,we’re telling Angular here to use the uniqueid
property that each contact
object contains. Optionally,we can use the index
(which is the index in the collection of each item,i.e. 0,1,2,3,4).
If your API returns unique data,then using that would be a preferable solution overindex
- as the index
may be subject to change if you reorder your collection. Using a unique identifier allows Angular to locate that DOM node associated with the object much faster,and it will reuse the component in the DOM should it need to be updated - instead of destroying it and rebuilding it.
You can check a live output of what we’ve covered here:
Capturing “index” and “count”
The ngFor
directive doesn’t just stop at iteration,it also provides us a few other niceties. Let’s exploreindex
and count
,two public properties exposed to us on each ngFor
iteration.
Let’s create another variable called i
,which we’ll assign the value ofindex
to. Angular exposes these values under-the-hood for us,and when we look at the next section with the<template>
element,we can see how they are composed.
To log out the index,we can simply interpolate i
:
<ul>
<li *ngFor="let contact of contacts | async; let i = index;">
Index: {{ i }}
<contact-card [contact]="contact"></contact-card>
</li>
</ul>
This will give us every index,starting from 0
,for each item in our collection. Let’s also exposecount
:
<ul>
<li *ngFor="let contact of contacts | async; let i = index; let c = count;">
Index: {{ i }}
Count: {{ c }}
<contact-card [contact]="contact"></contact-card>
</li>
</ul>
The count
will return a live collection length,equivalent ofcontacts.length
. These can optionally be bound and passed into each<contact-card>
,for example you may wish to log out the total length of your collection somewhere,and also pass theindex
of the particular contact into a function@Output
:
<ul>
<li *ngFor="let contact of contacts | async; let i = index; let c = count;">
<contact-card
[contact]="contact"
[collectionLength]="c"
(update)="onUpdate($event, i)">
</contact-card>
</li>
</ul>
You can check a live output of what we’ve covered here:
Accessing first,even
Four more properties exposed by ngFor
(well,actually underneath it usesNgForRow
,a class which generates each ngFor
item internally). Let’s quickly look at the source code for this:
export class NgForRow {
constructor(public $implicit: any, public index: number, public count: number) {}
get first(): boolean { return this.index === 0; }
get last(): boolean { return this.index === this.count - 1; }
get even(): boolean { return this.index % 2 === 0; }
get odd(): boolean { return !this.even; }
}
As I mentioned above,the NgForRow
is what constructs ourngFor
items,and you can see in the constructor
we’ve already taken a look at index
and count
! The last things we need to look at are the getters,which we can explain from the source code above:
-
first: returns
true
for the first item in the collection,matches the index with zero -
last: returns
true
for the last item in the collection,matches the index with the total count,minus one to shift the “count” down one to cater for zero-based indexes -
even: returns
true
for even items (e.g. 2,4) in the collection,uses%
modulus operator to calculate based off index -
odd: returns
true
for odd items (e.g. 1,3),simply invertsthis.even
result
Using this,we can add conditionally apply things such as styling,or hook into thelast
property to know when the collection has finished rendering.
For this quick demo,we’ll use ngClass
to add some styles to each<li>
(note how we create more variables,just likeindex
):
<ul>
<li
*ngFor="let contact of contacts | async; let o = odd; let e = even;"
[ngClass]="{
'odd-active': o,
'even-active': e
}">
<contact-card
[contact]="contact"
(update)="onUpdate($event, index)">
</contact-card>
</li>
</ul>
And some styles:
@Component({
selector: 'app-root',
styles: [`
.odd-active { background: purple; color: #fff; }
.even-active { background: red; color: #fff; }
`],
template: `
<div class="app">
<ul>
<li
*ngFor="let contact of contacts | async; let o = odd; let e = even;"
[ngClass]="{ 'odd-active': o,'even-active': e }">
<contact-card
[contact]="contact"
(update)="onUpdate($event,index)">
</contact-card> </li> </ul> </div> `
})
We won’t demonstrate first
and last
,as it’s fairly obvIoUs from the above how we can hook those up!
You can check a live output of what we’ve covered here:
<template> element
We mentioned earlier in this article that we’d look at understanding what the *
meant in our templates. This also shares the same Syntax as *ngIf
,which you’ve likely also seen before.
So in this next section,we’ll take a deeper dive on ngFor
,*
and the <template>
element to explain in more detail what’s really happening here.
When using an asterisk (*
) in our templates,we are informing Angular we’re using a structural directive,which is also sugar Syntax (a nice short hand) for using the<template>
element.
<template> and Web Components
So,what is the <template>
element? First,let’s take a step back. We’ll roll back to showing some Angular 1.x code here,perhaps you’ve done this before or done something similar in another framework/library:
<script id="myTemplate" type="text/ng-template">
<div>
My awesome template!
</div> </script>
This overrides the type
on the <script>
tag,which prevents the JavaScript engine from parsing the contents of the<script>
tag. This allows us,or a framework such as Angular 1.x,to fetch the contents of the script tag and use it as some form of HTML template.
Web Components introduced something similar to this idea,called the <template>
:
<template id="myTemplate">
<div>
My awesome template!
</div>
</template>
To grab our above template and instantiate it,we’d do this in plain JavaScript:
<div id="host"></div> <script>
let template = document.querySelector('#myTemplate');
let clone = document.importNode(template.content, true);
let host = document.querySelector('#host');
host.appendChild(clone);
</script>
Note how we have id=host
,which is our “host” Node for the template to be injected into.
You may have seen this term floating around Angular in a few ways,such as _nghost
prefixes on Nodes (ng-host) or the host
property in directives.
ngFor and <template>
So how does the above <template>
explanation tell us more aboutngFor
and the *
? The asterisk is shorthand Syntax for using the <template>
element.
Let’s start from the basic ngFor
example:
<ul>
<li *ngFor="let contact of contacts | async">
<contact-card [contact]="contact"></contact-card>
</li>
</ul>
And demonstrate the <template>
equivalent:
<template ngFor let-contact [ngForOf]="contacts | async">
<li>
<contact-card [contact]="contact"></contact-card>
</li>
</template>
That’s a lot different! What’s happening here?
When we use *ngFor
,we’re telling Angular to essentially treat the element the*
is bound to as a template.
Note: Angular’s
<template>
element is not a true Web Component,it merely mirrors the concepts behind it to allow you to use<template>
as it’s intended in the spec. When we Ahead-of-Time compile,we will see no<template>
elements. However,this doesn’t mean we can’t use things like Shadow DOM,as they are stillcompletely possible.
Let’s continue,and understand what ngFor
,let-contact
and ngForOf
are doing above.
ngFor and embedded view templates
First thing’s first,ngFor
is a directive! Let’s check some of the source code:
@Directive({selector: '[ngFor][ngForOf]'})
export class NgFor implements DoCheck, OnChanges {...}
Here,Angular is using attribute selectors as the value of selector
to tell the @Directive
decorator what attributes to look for (don’t confuse this with property binding such as<input [value]="foo">
).
The directive uses [ngFor][ngForOf]
,which implies there are two attributes as a chained selector. So,how doesngFor
work if we’re not using ngForOf
?
Angular’s compiler transforms any <template>
elements and directives used with a asterisk (*
) into views that are separate from the root component view. This is so each view can be created multiple times.
During the compile phase,it will take
let contact of contacts
and capitalise theof
,and create a custom key to createngForOf
.
In our case,Angular will construct a view that creates everything from the <li>
tag inwards:
<!-- view -->
<li>
<contact-card [contact]="contact"></contact-card>
</li>
<!-- /view -->
It also creates an invisible view container to contain all instances of the template,acting as a placeholder for content. The view container Angular has created essentially wraps the “views”,in our case this is just inside the<ul>
tags. This houses all the templates that are created byngFor
(one for each row).
A pseudo-output might look like this:
<ul>
<!-- view container -->
<!-- view -->
<li>
<contact-card [contact]="contact"></contact-card>
</li>
<!-- /view -->
<!-- view -->
<li>
<contact-card [contact]="contact"></contact-card>
</li>
<!-- /view -->
<!-- view -->
<li>
<contact-card [contact]="contact"></contact-card>
</li>
<!-- /view -->
<!-- /view container -->
</ul>
ngFor
creates an “embedded view” for each row,passing through the view it has created and the context of the row (the index and the row data). This embedded view is then inserted into the view container. When the data changes,it tracks the items to see if they have moved. If they’ve moved,instead of recreating the embedded views,it moves them around to be in the correct position,or destroys them if they no longer exist.
Context and passing variables
The next step is understanding how Angular passes the context to each <template>
:
<template ngFor let-contact [ngForOf]="contacts | async">
<li>
<contact-card [contact]="contact"></contact-card>
</li>
</template>
So now we’ve understood ngFor
and ngForOf
,how does Angular associate let-contact
with the individualcontact
that we then property bind to?
Because let-contact
has no value,it’s merely an attribute,this is where Angular provides an “implied” value,or$implicit
as it’s called under-the-hood.
Whilst Angular is creating each ngFor
item,it uses anNgForRow
class alongside an EmbeddedViewRef
,and passes these properties in dynamically. Here’s a small snippet from the source code:
changes.forEachIdentityChange((record: any) => {
const viewRef = <EmbeddedViewRef<NgForRow>>this._viewContainer.get(record.currentIndex);
viewRef.context.$implicit = record.item;
});
Alongside this section of code,we can also see how our aforementioned index
and count
properties are kept updated:
for (let i = 0, ilen = this._viewContainer.length; i < ilen; i++) {
const viewRef = <EmbeddedViewRef<NgForRow>>this._viewContainer.get(i);
viewRef.context.index = i;
viewRef.context.count = ilen;
}
You can dig through the directive source code in more detail here.
This is how we can then access the index
andcontext
like this:
<ul>
<template ngFor let-i="index" let-c="count" let-contact [ngForOf]="contacts | async">
<li>
<contact-card [contact]="contact"></contact-card>
</li>
</template>
</ul>
Note how we’re supplying let-i
and let-c
values which are exposed from the NgForRow
instance,unlike let-contact
.
You can check out the <template>
version of the things we’ve covered here:
Award-winning Angular 1.x and Angular 2 courses
Become an expert with my comprehensive Angular 1.5+ and Angular 2 courses