Native Template Syntax for Conditions and Loops
New Control Flow — @if, @for, @switch
Modern Angular replaced *ngIf and *ngFor with built-in @-syntax that reads like normal control flow.
What you'll learn
- Use @if, @else, and @else if
- Use @for with mandatory track and @empty
- Use @switch with @case and @default
Angular 17 introduced built-in control-flow blocks, and v20 made them the recommended default. They are faster, easier to read, and don’t need any imports.
@if / @else if / @else
@if (loading()) {
<p>Loading…</p>
} @else if (error()) {
<p class="error">{{ error() }}</p>
} @else {
<p>{{ data() }}</p>
} You can chain as many @else if blocks as you like. The blocks read
exactly like JS conditionals because they are.
@for — Always With track
@for (todo of todos(); track todo.id) {
<li [class.done]="todo.done">{{ todo.text }}</li>
} @empty {
<li>No todos yet.</li>
} A track expression is required — it tells Angular which DOM nodes
correspond to which items so updates can reuse them. Use a stable id
when you can; the index works for static lists.
Inside the loop, helpful local variables are available too: $index,
$first, $last, $even, $odd.
@for (row of rows(); track row.id; let i = $index, even = $even) {
<tr [class.alt]="even">{{ i }}. {{ row.name }}</tr>
} @switch
@switch (status()) {
@case ('loading') { <p>Loading…</p> }
@case ('ready') { <p>Done!</p> }
@case ('error') { <p>Try again</p> }
@default { <p>Unknown</p> }
} Why the New Syntax
The old *ngIf and *ngFor were directives, which meant extra
generated code, harder type inference, and no built-in “else” or
“empty” cases. The new blocks are part of the template compiler, so
they’re smaller, faster, and ergonomic enough that the team is
encouraging everyone to migrate.