Control flow: @if, @for, @switch

Angular · Essentials · 11 min

Every list and conditional in an Angular template uses these blocks; track decides list performance.

Angular templates have built-in control flow blocks (they replaced the older *ngIf and *ngFor directives).

`@if` with optional @else if and @else:

@if (user(); as u) { <p>Hi {{ u.name }}</p> } @else { <a>Log in</a> }

The as alias stores the condition's value, handy for signals and nullable values.

`@for` repeats a block, and a `track` expression is required:

@for (item of items(); track item.id; let i = $index, last = $last) { ... } @empty { <p>No items</p> }

track tells Angular how to identify items so it can move, add and remove DOM nodes instead of recreating everything when the array changes. Use a unique id; track $index is acceptable only for static lists. Implicit variables include $index, $count, $first, $last, $even and $odd. The @empty block renders for an empty collection.

`@switch` chooses one case by strict equality:

@switch (status()) { @case ('loading') { <spinner /> } @case ('error') { <p>Failed</p> } @default { <app-list /> } }

Blocks need no imports and work with TypeScript narrowing: inside @if (result.kind === 'ok'), the template type checker knows the narrowed type. Prefer computed signals for complex conditions so templates stay readable.

Example

import { Component, signal, computed } from '@angular/core';

type Todo = { id: number; title: string; done: boolean };

@Component({
  selector: 'app-todos',
  template: `
    @switch (filter()) {
      @case ('open') { <h2>Open tasks</h2> }
      @default { <h2>All tasks</h2> }
    }
    @for (t of visible(); track t.id; let i = $index) {
      <p [class.done]="t.done">{{ i + 1 }}. {{ t.title }}</p>
    } @empty { <p>Nothing to do</p> }
    @if (visible().length > 2) { <p>Busy day!</p> } @else { <p>Light day</p> }
  `,
})
export class Todos {
  protected filter = signal<'all' | 'open'>('open');
  protected todos = signal<Todo[]>([
    { id: 1, title: 'Learn @for', done: true }, { id: 2, title: 'Use track', done: false },
  ]);
  protected visible = computed(() =>
    this.filter() === 'all' ? this.todos() : this.todos().filter((t) => !t.done));
}

Remember

  • @if (cond; as x) {} @else {}, @switch / @case / @default
  • @for (x of list; track x.id) {} @empty {}: track is mandatory
  • Use $index, $first, $last etc. via let

Watch out for

  • Tracking by $index (or the object itself when data is refetched) in dynamic lists, causing every row to be recreated.

Check yourself

  1. Why does @for require a track expression?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: angular.dev/guide/templates/control-flow