A look at Angular's ngFor directive

Published: January 22, 2017  •  Updated: December 05, 2017  •  angular, javascript

Angular knows 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 a 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.

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
Example

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.
Step1

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 indentity 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.
Step2

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.
Step3