Lesson 6 of 30
Angular's templates finally speak plain conditionals — @if, @for, and @switch are built into the compiler, not directives you import.
14 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
For years, conditional rendering in an Angular template meant reaching for a structural directive: *ngIf, *ngFor, *ngSwitch. They worked, but they were directives — importable, patchable, and built on a slightly awkward <ng-template> desugaring that most developers never actually looked at.
The new control-flow blocks — @if, @for, @switch — aren't directives at all. They're template syntax the Angular compiler understands natively, the same way if and for are syntax the JavaScript engine understands natively. No import, no module, no CommonModule in your standalone component's imports array. You write @if (condition) { ... } directly in the template and the compiler does the rest. This has been stable since Angular v18 and is the default scaffolded syntax since v19 — if you open a modern Angular project today, this is what you'll see, not *ngIf.
The old structural-directive syntax still works and you'll run into it constantly in existing codebases — treat it as legacy: readable, not what you reach for when writing new code.
import { Component, signal } from '@angular/core';
interface Task {
id: number;
title: string;
done: boolean;
}
@Component({
selector: 'app-task-list',
standalone: true,
template: `
@if (tasks().length === 0) {
<p class="empty">No tasks yet — add one above.</p>
} @else {
<ul>
@for (task of tasks(); track task.id) {
<li [class.done]="task.done">
{{ task.title }}
@switch (task.priority) {
@case ('high') { <span class="tag tag-high">Urgent</span> }
@case ('low') { <span class="tag tag-low">Someday</span> }
@default { <span class="tag">Normal</span> }
}
</li>
} @empty {
<li>Nothing matches the current filter.</li>
}
</ul>
}
`,
})
export class TaskListComponent {
tasks = signal<(Task & { priority: 'high' | 'low' | 'normal' })[]>([
{ id: 1, title: 'Write signals lesson', done: false, priority: 'high' },
{ id: 2, title: 'Refill coffee', done: true, priority: 'low' },
]);
}
Three things to notice. First, @if / @else reads like real conditional syntax — no <ng-template #elseBlock> gymnastics. Second, @for requires a track expression — there's no optional third parameter here, and no trackBy function form like the old *ngFor; trackBy: fn. You write track task.id directly in the block header, and the compiler uses it to match array items across re-renders so it can patch the DOM in place instead of tearing down and rebuilding every <li>. Third, @for has a built-in @empty block for the empty-array case — something *ngFor never had natively; you used to hand-roll it with a separate *ngIf="items.length === 0".
@for (item of items; track item.id) renders 3 items as boxes — each carries a stable id: a, b, c.
track doesn't silently fall back to something safe. Angular requires an explicit track expression at compile time — leaving it out is a template compile error, not a runtime warning. That's a deliberate design choice: it forces you to think about item identity up front instead of discovering the bug later.@myIf. If you need custom reusable conditional logic, you still write a directive or just compose @if/@for yourself in the template.@switch has no fallthrough. Each @case is self-contained — there's no accidental fall-through into the next case like raw JavaScript switch can produce. @default is optional but recommended so an unmatched value doesn't just render nothing silently.This mirrors the same shift JSX represents in React: instead of calling a helper function or component to express a conditional, the framework's own template/syntax layer understands branching and iteration directly, which is faster to compile and easier to statically analyze. It also depends on the same "stable identity for list items" idea that React's list-and-keys lesson covers — track and key are solving the identical reconciliation problem from two different frameworks.