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. The code creates a local variable actor
that 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 loop
- last is true when the current item is the last in the loop
- even is true when the index number is even
- odd 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
And here 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 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.