From ng-repeat in Angular 1.x to ngFor in Angular 2

This guide was written for Angular 2 version: 2.0.0-rc.4

The ng-repeat directive in Angular 1.x allows us to iterate over a collection of data and print out DOM nodes that respond to that data. If the data changes, the DOM changes as well. In this guide we'll be converting an Angular 1.x ng-repeat directive across to Angular 2's ngFor directive.

Table of contents

Angular 1.x

In Angular 1.x, using ng-repeat is pretty simple, we pass the directive some data and it automagically renders out for us. Let's take a look!

Using ng-repeat

Before we can get the ng-repeat directive working, we need some data inside a controller bound to the component:

const app = {
  controller() {
    this.groceries = [{
      id: 0, label: 'Butter'
    },{
      id: 1, label: 'Apples'
    },{
      id: 2, label: 'Paprika'
    },{
      id: 3, label: 'Potatoes'
    },{
      id: 4, label: 'Oatmeal'
    },{
      id: 5, label: 'Spaghetti'
    },{
      id: 6, label: 'Pears'
    },{
      id: 7, label: 'Bacon'
    }];
  }
};

angular
  .module('app')
  .component('app', app);

Next up, we can create some methods to the controller and assign the template with an unordered list to make way for our ng-repeat and upcoming click functions:

const app = {
  template: `
    <div>
      Grocery selected: {{ $ctrl.selectedGrocery.label }}
      <ul>
        <li>
          <a href=""></a>
        </li>
      </ul>
    </div>
  `,
  controller() {
    this.groceries = [{...}];
    this.selectGrocery = (grocery) => {
      this.selectedGrocery = grocery;
    };
    this.selectGrocery(this.groceries[0]);
  }
};

Then we need to assign ng-repeat to the <li> that serves as the template to be cloned for each item in the dataset, followed by an ng-click to pass each grocery into the selectGrocery method:

const app = {
  template: `
    <div>
      Grocery selected: {{ $ctrl.selectedGrocery.label }}
      <ul>
        <li ng-repeat="grocery in $ctrl.groceries">
          <a href="" ng-click="$ctrl.selectGrocery(grocery);">
            {{ grocery.label }}
          </a>
        </li>
      </ul>
    </div>
  `,
  ...
};

That's it for rendering with ng-repeat. Let's take a look at $index and the track by expression.

Using $index and track by

The $index property is automatically provided to us on each ng-repeat's $scope object. We can print out each index for the collection with ease:

const app = {
  template: `
    ...
        <li ng-repeat="grocery in $ctrl.groceries">
          <a href="" ng-click="$ctrl.selectGrocery(grocery);">
            {{ grocery.label }} {{ $index }}
          </a>
        </li>
    ...
  `,
  ...
};

If you've noted already, each object inside the this.groceries array has an id property, which, in this case, indicates that these are unique properties sent back from the server. These unique keys allow us to use the track by clause inside an ng-repeat to prevent Angular re-rendering an entire collection.

What it does instead is cleverly only re-render the DOM nodes that require rendering again, rather than destroying and recreating the DOM tree each time. It's simple to use and works as an extension to ng-repeat's value:

const app = {
  template: `
    ...
        <li ng-repeat="grocery in $ctrl.groceries track by grocery.id">
          <a href="" ng-click="$ctrl.selectGrocery(grocery);">
            {{ grocery.label }} {{ $index }}
          </a>
        </li>
    ...
  `,
  ...
};

So you can see here that we've added track by grocery.id at the end of the repeat syntax. We can also use track by $index as well. The ng-repeat directive also exposes $first, $middle, $last, $even and $odd properties – see the documentation for more.

You can also pass in a tracking function:

const app = {
  template: `
    ...
        <li ng-repeat="grocery in $ctrl.groceries track by trackByGrocery(grocery)">
          <a href="" ng-click="$ctrl.selectGrocery(grocery);">
            {{ grocery.label }} {{ $index }}
          </a>
        </li>
    ...
  `,
  ...
};

Final 1.x code

Angular 2

The Angular 2 implementation of the ng-repeat is called ngFor, purposely in camelCase. The syntax is pretty similar, whereby we can iterate over a collection. Angular 2 uses of instead of in with ngFor to align with the ES2015 for...of loop.

Using ngFor

Assuming we use the same data as in the Angular 1.x example, we can declare this.groceries in the class constructor:

interface Grocery {
  id: number;
  label: string;
}

export default class App {
  public groceries: Grocery[];
  constructor() {
    this.groceries = [{
      id: 0, label: 'Butter'
    },{
      id: 1, label: 'Apples'
    },{
      id: 2, label: 'Paprika'
    },{
      id: 3, label: 'Potatoes'
    },{
      id: 4, label: 'Oatmeal'
    },{
      id: 5, label: 'Spaghetti'
    },{
      id: 6, label: 'Pears'
    },{
      id: 7, label: 'Bacon'
    }];
    this.selectGrocery(this.groceries[0]);
  }
  selectGrocery(grocery: Grocery) {
    this.selectedGrocery = grocery;
  }
}

Then bind ngFor as follows, declaring block scoping with let:

@Component({
  selector: 'my-app',
  template: `
    <div>
      Grocery selected: {{ selectedGrocery.label }}
      <ul>
        <li *ngFor="let grocery of groceries;">
          <a href="#" (click)="selectGrocery(grocery);">
            {{ grocery.label }}
          </a>
        </li>
      </ul>
    </div>
  `
})
export default class App {...}

Nice and easy. What is the leading * infront of *ngFor you might ask? It's essentially sugar syntax for using <template> elements. Check out this section of the documentation for more details.

Using index and trackBy

Instead of $index (in Angular 1.x) being readily available in the template, we need to actually assign it a variable before we use it:

@Component({
  selector: 'my-app',
  template: `
    <div>
      Grocery selected: {{ selectedGrocery.label }}
      <ul>
        <li *ngFor="let grocery of groceries; let i = index;">
          <a href="#" (click)="selectGrocery(grocery);">
            {{ grocery.label }} {{ i }}
          </a>
        </li>
      </ul>
    </div>
  `
})
export default class App {...}

There's a change from Angular 1.x whereby using an object form with track by X is no longer allowed – it must be a function. So we'll add trackByGrocery to the App class (arguments are automatically provided):

@Component({
  selector: 'my-app',
  template: `
    <div>
      Grocery selected: {{ selectedGrocery.label }}
      <ul>
        <li *ngFor="let grocery of groceries; let i = index; trackBy: trackByGrocery;">
          <a href="#" (click)="selectGrocery(grocery);">
            {{ grocery.label }} {{ i }}
          </a>
        </li>
      </ul>
    </div>
  `
})
export default class App {
  ...
  trackByGrocery: (index: number, grocery: Grocery): number => grocery.id;
  ...
}

Altogether now:

import {Component} from '@angular/core';

interface Grocery {
  id: number;
  label: string;
}

@Component({
  selector: 'my-app',
  template: `
    <div>
      Grocery selected: {{ selectedGrocery.label }}
      <ul>
        <li *ngFor="let grocery of groceries; let i = index; trackBy: trackByGrocery;">
          <a href="#" (click)="selectGrocery(grocery);">
            {{ grocery.label }} {{ i }}
          </a>
        </li>
      </ul>
    </div>
  `
})
export default class App {
  public groceries: Grocery[];
  constructor() {
    this.groceries = [{
      id: 0, label: 'Butter'
    },{
      id: 1, label: 'Apples'
    },{
      id: 2, label: 'Paprika'
    },{
      id: 3, label: 'Potatoes'
    },{
      id: 4, label: 'Oatmeal'
    },{
      id: 5, label: 'Spaghetti'
    },{
      id: 6, label: 'Pears'
    },{
      id: 7, label: 'Bacon'
    }];
    this.selectGrocery(this.groceries[0]);
  }
  selectGrocery(grocery: Grocery) {
    this.selectedGrocery = grocery;
  }
  trackByGrocery: (index: number, grocery: Grocery): number => grocery.id;
}

Final 2 code

Comments