Angular supports three kinds of directives: Components, attribute directives, and structural directives.
ngIf
, ngSwitch
, and ngFor
are built-in structural directives.
A structural directive adds and removes elements in the DOM tree. ngFor
adds DOM elements for each item from an array.
A simple ngFor
example looks like this (with Ionic):
<ion-list>
<ion-item *ngFor="let actor of bondActors">
{{actor.firstName}} {{actor.lastName}}
</ion-item>
</ion-list>
This example creates an ion-item
component for each element in the array and adds it to the DOM. In this example, a local variable actor
is created, which can be referenced inside the ngFor
loop. The bondActors
array is a public instance variable in the TypeScript code. Each element is an object with a firstName
and lastName
property.
export class HomePage {
bondActors: Actor[];
constructor() {
this.bondActors = [
new Actor("Sean", "Connery"),
new Actor("David", "Niven"),
new Actor("George", "Lazenby"),
new Actor("Roger", "Moore"),
new Actor("Timothy", "Dalton"),
new Actor("Pierce", "Brosnan"),
new Actor("Daniel", "Craig")
];
}
}
https://github.com/ralscha/blog/blob/master/ngfor/home.ts
export class Actor {
constructor(public firstName: string, public lastName: string) {
}
}
Star syntax ¶
The star syntax *ngFor
is syntactic sugar. Under the hood, Angular transforms this:
<ion-item *ngFor="let actor of bondActors">
{{actor.firstName}} {{actor.lastName}}
</ion-item>
into this:
<ng-template ngFor let-actor [ngForOf]="bondActors">
<ion-item>
{{actor.firstName}} {{actor.lastName}}
</ion-item>
</ng-template>
Angular creates an ng-template
element, moves the ngFor
directive to this element, and moves the element, where the *ngFor
was located, into the ng-template
element as a child. The two styles are equivalent and result in the same DOM tree. You could always write your ngFor
with ng-template
, but the *ngFor
syntax looks a bit simpler to write and read.
Exported values ¶
ngFor
exports several values that can be assigned to local variables:
index
represents the number of the current loop iteration (the first iteration is 0)first
is true when the current item is the first in the looplast
is true when the current item is the last in the loopeven
is true when the index number is evenodd
is true when the index number is odd
The following example assigns all 5 values to local variables. [ngClass]
adds CSS classes dynamically to the element when the local variable is true (i.e., when isOdd
is true, it adds the CSS class odd
). The local variable i
contains the current index.
<ion-item
*ngFor="let actor of bondActors; index as i; even as isEven; odd as isOdd; first as isFirst; last as isLast"
[ngClass]="{ odd: isOdd, even: isEven, first: isFirst, last: isLast }">
{{i+1}} {{actor.firstName}} {{actor.lastName}}
</ion-item>
https://github.com/ralscha/blog/blob/master/ngfor/home.html
With the following CSS class definitions:
.odd {
background-color: lightblue;
}
.even {
background-color: azure;
}
.first {
font-style: italic;
}
.last {
font-weight: bold;
}
The output looks like this:
Here is the same example but written with the ng-template
syntax:
<ng-template ngFor let-actor [ngForOf]="bondActors"
let-i="index"
let-isEven="even" let-isOdd="odd"
let-isFirst="first" let-isLast="last">
<ion-item [ngClass]="{ odd: isOdd, even: isEven, first: isFirst, last: isLast }">
{{i+1}} {{actor.firstName}} {{actor.lastName}}
</ion-item>
</ng-template>
With the as
keyword, you can also assign the collection to a local variable. This is useful when you are working with Observables and Promises and need access to the resolved collection object.
In the following example, the async
pipe waits until the observable returns a result and assigns it to the local variable users
. You can then reference this variable inside the ngFor
block.
<li *ngFor="let user of userObservable | async as users; index as i; first as isFirst">
{{i}}/{{users.length}}. {{user}} <span *ngIf="isFirst">default</span>
</li>
trackBy
¶
The NgForOf
directive automatically keeps track of the changes that are made to the referenced collection. When the application adds, removes, or reorders the items in the collection, NgForOf
makes the corresponding changes to the DOM.
Angular uses, by default, the object identity of the items to track the changes. This could be a problem when your application, for example, fetches data from the server and gets a new result object. Because the reference changes, NgForOf
has to destroy all DOM elements and recreate them, even if the data might not have changed.
For this use case, NgForOf
supports an additional option: trackBy
. This option takes a function with two arguments: the current index and the item. Angular then tracks changes with the return value of this function.
To demonstrate the implication of trackBy
, we create a simple list with 500 objects and update the
property d
every 50 milliseconds.
items = [];
constructor() {
const newItems = [];
let d = Date.now();
for (let i = 0; i < 500; i++) {
newItems.push({id: i, name: i, d: d++});
}
this.items = newItems;
setInterval(() => {
let d = Date.now();
for (let i = 0; i < 500; i++) {
this.items[i].d = d++;
}
}, 50);
}
<ion-content padding>
<ion-list>
<ion-item
*ngFor="let item of items">
{{item.name}} {{item.d}}
</ion-item>
</ion-list>
</ion-content>
This works fine because the application does not change the reference of the items
object.
I get a frame rate of about 16 fps on my computer.
Now we change the setInterval
function and create a new array and assign that to the items
instance variable.
Angular has to destroy and recreate the DOM elements because the object identity changes.
setInterval(()=>{
const newItems = [];
let d = Date.now();
for (let i = 0; i < 500; i++) {
newItems.push({id: i, name: i, d: d++});
}
this.items = newItems;
}, 50);
The performance is now significantly worse, and the frame rate drops to 3 fps.
To solve that, we add a trackBy
function and tell Angular to use the id
field to track changes.
The return value should be something that uniquely identifies the record, for instance, a primary key from a database.
trackById(index: number, data: any): number {
return data.id;
}
and in the template, we set the trackBy
option:
<ion-item
*ngFor="let item of items; trackBy: trackById">
{{item.name}} {{item.d}}
</ion-item>
When we observe the frames per second, we see that we are back to 16 fps.