On this page

Templates and native control flow

12 min read TextCh. 1 — Fundamentals

Native control flow in Angular

Angular 21 uses native control flow blocks directly in templates. This syntax replaces the structural directives *ngIf, *ngFor, and *ngSwitch from previous versions.

@if / @else

Renders content conditionally:

@if (user()) {
  <p>Welcome, {{ user()!.name }}</p>
} @else if (loading()) {
  <p>Loading...</p>
} @else {
  <p>Sign in to continue</p>
}

Unlike *ngIf, you don't need to import any directive. The @else if block is native and doesn't require ng-template.

@for / @empty

Iterates over collections. The track expression is mandatory and must reference a unique property:

@for (item of items(); track item.id) {
  <div>{{ item.name }}</div>
} @empty {
  <div>No items to display.</div>
}

The @empty block renders when the collection is empty. Available context variables:

Variable Type Description
$index number Index of the current element
$first boolean Is the first element
$last boolean Is the last element
$even boolean Even index
$odd boolean Odd index
$count number Total number of elements

Example with context variables:

@for (step of steps(); track step.id; let i = $index) {
  <div [class.active]="i === currentStep()">
    Step {{ i + 1 }}: {{ step.title }}
  </div>
}

@switch

Evaluates an expression and renders the matching case:

@switch (status()) {
  @case ('active') {
    <span class="badge green">Active</span>
  }
  @case ('pending') {
    <span class="badge yellow">Pending</span>
  }
  @default {
    <span class="badge gray">Unknown</span>
  }
}

Interpolation and bindings

In addition to control flow, Angular templates use:

  • Interpolation: {{ expression }} to display values
  • Property binding: [property]="expression" to bind DOM properties
  • Event binding: (event)="handler()" to listen for events
  • Two-way binding: [(ngModel)]="signal" for forms (requires FormsModule)
<!-- Property binding -->
<img [src]="avatar()" [alt]="name()" />

<!-- Conditional class binding -->
<div [class.active]="isActive()">Content</div>

<!-- Style binding -->
<div [style.color]="isActive() ? 'green' : 'gray'">Status</div>

Pipes in templates

Pipes transform values for display:

<p>{{ price() | currency:'USD' }}</p>
<p>{{ date() | date:'longDate' }}</p>
<p>{{ name() | uppercase }}</p>

Practice

  1. Render a list with @for: Create an array of at least 5 objects (e.g., tasks) and render them using @for with track by id. Add an @empty block that displays a message when the list is empty.
  2. Add filters with @if and @switch: Implement filter buttons that show or hide items using @if. Use @switch to display a different badge based on an object property (e.g., priority or category).

In the next lesson, we will dive deeper into Signals, the core reactive primitive of Angular 21.

track is mandatory
In @for, the track expression is mandatory. Use a unique property like id. This allows Angular to track elements and optimize DOM updates.
Don't use ngIf or ngFor
The *ngIf, *ngFor, and *ngSwitch directives are deprecated in Angular 21. Always use native control flow: @if, @for, @switch.
import { Component, signal, computed, ChangeDetectionStrategy } from '@angular/core';

interface Product {
  id: number;
  name: string;
  price: number;
  category: 'electronics' | 'clothing' | 'home';
  available: boolean;
}

@Component({
  selector: 'app-catalog',
  changeDetection: ChangeDetectionStrategy.OnPush,
  templateUrl: './catalog.html',
})
export class Catalog {
  readonly products = signal<Product[]>([
    { id: 1, name: 'Laptop Pro', price: 1200, category: 'electronics', available: true },
    { id: 2, name: 'Angular T-Shirt', price: 25, category: 'clothing', available: true },
    { id: 3, name: 'LED Lamp', price: 45, category: 'home', available: false },
    { id: 4, name: 'Mechanical Keyboard', price: 89, category: 'electronics', available: true },
  ]);

  readonly filter = signal<string>('all');

  readonly filteredProducts = computed(() => {
    const f = this.filter();
    if (f === 'all') return this.products();
    return this.products().filter(p => p.category === f);
  });

  readonly total = computed(
    () => this.filteredProducts().reduce((sum, p) => sum + p.price, 0)
  );

  changeFilter(category: string): void {
    this.filter.set(category);
  }
}