Angular Lists

Angular gives you two ways to render collections in a template: the classic *ngFor structural directive and the newer built-in @for control flow block, and knowing when and how to track items keeps your lists fast.

Rendering Collections in Angular

Almost every real application displays a list of something — products, comments, search results. Angular supports two syntaxes for repeating a template over an array: the structural directive *ngFor, and the newer @for block that ships as part of Angular's built-in control flow. Both walk an iterable and stamp out one copy of a template per item, but they differ in syntax, performance defaults, and how they handle empty state.

The Legacy *ngFor Directive

*ngFor is applied as an attribute on an element and desugars to an <ng-template> behind the scenes. You bind it to an expression of the form 'let item of items', and Angular exposes contextual variables like index, first, last, even, and odd that you can alias with the 'as' keyword. Performance-sensitive lists should also supply a trackBy function so Angular can identify which DOM nodes correspond to which data items across re-renders.

Using *ngFor with an index and trackBy

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { bootstrapApplication } from '@angular/platform-browser';

interface Task {
  id: number;
  title: string;
  done: boolean;
}

@Component({
  selector: 'app-task-list',
  standalone: true,
  imports: [CommonModule],
  template: `
    <ul>
      <li *ngFor="let task of tasks; index as i; trackBy: trackByTaskId">
        {{ i + 1 }}. {{ task.title }} - {{ task.done ? 'Done' : 'Pending' }}
      </li>
    </ul>
  `
})
export class TaskListComponent {
  tasks: Task[] = [
    { id: 1, title: 'Learn Angular', done: false },
    { id: 2, title: 'Build a component', done: false },
    { id: 3, title: 'Write tests', done: true }
  ];

  trackByTaskId(index: number, task: Task): number {
    return task.id;
  }
}

bootstrapApplication(TaskListComponent).catch((err) => console.error(err));

The Modern @for Control Flow Block

Since Angular 17, templates support a built-in control flow syntax that doesn't require importing CommonModule at all. The @for block reads more like plain TypeScript, requires an explicit track expression on every loop (there is no untracked fallback), and pairs naturally with an @empty block that renders when the collection has zero items — something *ngFor cannot do on its own.

Using @for with track and @empty

import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';

interface Task {
  id: number;
  title: string;
  done: boolean;
}

@Component({
  selector: 'app-task-list',
  standalone: true,
  template: `
    <ul>
      @for (task of tasks; track task.id; let i = $index, count = $count) {
        <li>{{ i + 1 }} of {{ count }}: {{ task.title }}</li>
      } @empty {
        <li>No tasks yet. Add one to get started.</li>
      }
    </ul>
  `
})
export class TaskListComponent {
  tasks: Task[] = [];
}

bootstrapApplication(TaskListComponent).catch((err) => console.error(err));
  • $index — zero-based position of the current item
  • $count — total number of items in the collection
  • $first / $last — true for the first or last item
  • $even / $odd — true when $index is even or odd
  • track — the required expression Angular uses to identify each item across re-renders
Aspect*ngFor@for
Import requiredCommonModuleNone — built into templates
TrackingOptional trackBy functionMandatory track expression
Empty stateNeeds a separate *ngIf checkBuilt-in @empty block
Syntax styleAttribute string expressionBlock syntax, closer to TypeScript

A filterable product list using signals and @for

import { Component, signal } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';

interface Product {
  id: number;
  name: string;
  price: number;
}

@Component({
  selector: 'app-product-list',
  standalone: true,
  template: `
    <input
      type="text"
      placeholder="Filter products..."
      (input)="filterTerm.set($any($event.target).value)"
    />

    @for (product of filteredProducts(); track product.id) {
      <div class="product-row">
        {{ product.name }} - Price: {{ product.price }}
      </div>
    } @empty {
      <p>No products match "{{ filterTerm() }}".</p>
    }
  `
})
export class ProductListComponent {
  private allProducts: Product[] = [
    { id: 1, name: 'Keyboard', price: 49 },
    { id: 2, name: 'Mouse', price: 25 },
    { id: 3, name: 'Monitor', price: 199 }
  ];

  filterTerm = signal('');

  filteredProducts() {
    const term = this.filterTerm().toLowerCase();
    return this.allProducts.filter(p => p.name.toLowerCase().includes(term));
  }
}

bootstrapApplication(ProductListComponent).catch((err) => console.error(err));
Note: Always track by a stable, unique identifier such as a database id — never by array index. Tracking by index makes Angular think items changed identity whenever the array is reordered or filtered, which throws away and rebuilds DOM nodes (and any component state or focus inside them) unnecessarily.
Note: Unlike *ngFor, where trackBy is optional and easy to forget, @for will fail to compile if you omit track entirely. This is intentional — the Angular team made tracking mandatory in the new syntax specifically because untracked lists were a common source of subtle performance bugs.

Exercise: Angular Lists

Which syntax repeats a template element for each array item?