On this page
Templates and native control flow
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
- Render a list with @for: Create an array of at least 5 objects (e.g., tasks) and render them using
@forwithtrackbyid. Add an@emptyblock that displays a message when the list is empty. - Add filters with @if and @switch: Implement filter buttons that show or hide items using
@if. Use@switchto 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.
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);
}
}
Sign in to track your progress