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,$lastetc. vialet
Watch out for
- Tracking by
$index(or the object itself when data is refetched) in dynamic lists, causing every row to be recreated.