Control Flow and @defer
|
This section documents modern, standalone Angular — signals, the built-in This content was generated with the assistance of AI and should be verified against angular.dev before being relied on in production. Angular ships a major release roughly every six months and its APIs continue to evolve: the examples here target the current major release; where a consulted source disagrees with the current documentation, the documentation wins and the difference is noted. This section’s bibliography lists the reference material consulted while preparing these pages. |
Angular templates have keyword control-flow blocks — @if, @for, @switch — built into the compiler, plus
@defer for loading part of a template lazily. None of them needs an import.
@if, @for, @switch
@if (user(); as u) {
<p>Welcome, {{ u.name }}</p>
} @else if (loading()) {
<app-spinner />
} @else {
<a routerLink="/login">Sign in</a>
}
<ul>
@for (item of items(); track item.id) {
<li [class.first]="$first" [class.last]="$last">{{ $index + 1 }}. {{ item.label }}</li>
} @empty {
<li>No items ({{ $count }} total)</li>
}
</ul>
@switch (status()) {
@case ('active') { <span class="ok">Active</span> }
@case ('paused') { <span class="warn">Paused</span> }
@default { <span>Unknown</span> }
}
Inside @for, the contextual variables are $index, $first, $last, $even, $odd, and $count. The
track expression is required: it tells Angular how to identify a row across renders so it can move DOM
nodes instead of destroying and recreating them. Use a stable unique key (item.id); use track $index only
for lists of primitives that never reorder. A bad track causes lost focus, restarted animations, and wasted
DOM work. See control flow.
Migrating from *ngIf / *ngFor / *ngSwitch
The old structural directives still work, but the schematic rewrites them automatically:
ng generate @angular/core:control-flow
Before — the structural directives:
<p *ngIf="user as u; else guest">
Hi {{ u.name }}
</p>
<ng-template #guest><a>Sign in</a></ng-template>
<li *ngFor="let t of todos; trackBy: byId">
{{ t.text }}
</li>
<div [ngSwitch]="status">
<p *ngSwitchCase="'active'">Active</p>
<p *ngSwitchDefault>Unknown</p>
</div>
After — the built-in control flow:
@if (user(); as u) {
<p>Hi {{ u.name }}</p>
} @else {
<a>Sign in</a>
}
@for (t of todos(); track t.id) {
<li>{{ t.text }}</li>
}
@switch (status()) {
@case ('active') { <p>Active</p> }
@default { <p>Unknown</p> }
}
The built-in blocks need no CommonModule import, give better type-narrowing, and make track mandatory
rather than optional. See control flow and
the control-flow migration.
@defer blocks
A @defer block does not render (and its dependencies are not bundled into the initial JavaScript) until a
trigger fires. Companion blocks cover the other states:
-
@placeholder— shown before the trigger; can declare aminimumdisplay time. -
@loading— shown while the deferred chunk downloads; supportsminimumandafter. -
@error— shown if loading fails.
@defer (on viewport; prefetch on idle) {
<app-comments [postId]="postId()" />
} @placeholder (minimum 500ms) {
<p>Comments</p>
} @loading (after 100ms; minimum 1s) {
<app-spinner />
} @error {
<p>Could not load comments.</p>
}
Triggers: on idle (default), on viewport, on interaction, on hover, on timer(2s), on immediate,
and when <expression>. on viewport, interaction, and hover may name a template reference variable to
watch a different element. prefetch on <trigger> downloads the chunk early without rendering it yet. See
deferrable views.