Chapter 3: Control Flow (@if, @for, @switch)
Control Flow (@if, @for, @switch) — this is where Angular templates start feeling modern, clean, and way less cluttered compared to the old *ngIf, *ngFor, *ngSwitch days.
Think of the old way like writing emails with lots of attachments and footnotes. The new way (@if / @for / @switch) is like writing clean, readable prose — everything is right there in the template, no extra wrappers, no asterisks everywhere, and it plays beautifully with signals for automatic reactivity.
Introduced in Angular 17 (2023), stabilized and improved through 18–19–21+, this is now the recommended way in 2026. Almost all new code and tutorials use it.
Why the new syntax? (Quick teacher rant)
Old structural directives (*ngIf etc.):
- Required an extra <ng-template> for else/then
- Created hidden wrapper elements sometimes
- Harder to read nested conditions
- Less type narrowing in branches
- Change detection was zone.js-heavy
New block syntax:
- No directives → built into the template parser
- Cleaner, more JS-like
- Better performance (especially with track + signals)
- Full signal reactivity → change a signal → only the affected block updates
- Easier migration tools exist (ng update)
1. @if + @else if + @else (Conditional rendering)
Replaces *ngIf.
Basic syntax
|
0 1 2 3 4 5 6 7 8 |
@if (condition) { <div>Shown when true</div> } |
With else
|
0 1 2 3 4 5 6 7 8 9 10 11 |
@if (isLoggedIn()) { <p>Welcome back, {{ userName() }}!</p> } @else { <p>Please log in to continue.</p> } |
Chained else if (multiple branches)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@if (score >= 90) { <div class="grade">A - Excellent! 🎉</div> } @else if (score >= 80) { <div class="grade">B - Good job</div> } @else if (score >= 70) { <div class="grade">C - Keep going</div> } @else { <div class="grade">Needs improvement 😕</div> } |
Alias with as (super useful for observables/signals)
|
0 1 2 3 4 5 6 7 8 9 10 11 |
@if (user(); as currentUser) { <!-- user() is a signal --> <p>Hello {{ currentUser.name }}, age {{ currentUser.age }}</p> } @else { <p>No user loaded yet...</p> } |
→ Saves you from writing user()?.name everywhere inside the block.
Reactivity with signals
|
0 1 2 3 4 5 6 7 8 |
// In component showMessage = signal(true); toggle() { this.showMessage.update(v => !v); } |
|
0 1 2 3 4 5 6 7 8 9 10 |
<button (click)="toggle()">Toggle</button> @if (showMessage()) { <div>This appears/disappears instantly — no Zone.js magic needed!</div> } |
→ Because showMessage() is a signal read, Angular knows exactly when to re-evaluate this block.
2. @for + @empty (Looping / Repeating)
Replaces *ngFor.
Basic syntax — track is mandatory (and the most important part!)
|
0 1 2 3 4 5 6 7 8 |
@for (item of shoppingList(); track item.id) { <li>{{ item.name }} - ₹{{ item.price }}</li> } |
Why track?
- Tells Angular how to identify items between renders
- Prevents unnecessary DOM destroy/create → huge perf win
- Best: use unique ID (id, _id, uuid)
- Okay: $index (if list never reorders)
- Avoid: track item (object reference) unless list is immutable/static
@empty block (replaces showing “no items” manually)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
<ul> @for (hero of heroes(); track hero.id) { <li>{{ hero.name }} ({{ hero.power }})</li> } @empty { <li class="empty">No heroes recruited yet... 😢</li> } </ul> |
Context variables (like let-i = index in old ngFor)
Built-in: $index, $first, $last, $even, $odd, $count
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<ol> @for (color of colors(); track $index; let i = $index, isOdd = $odd) { <li [style.color]="isOdd ? 'purple' : 'navy'"> #{{ i + 1 }} — {{ color }} </li> } @empty { <li>No colors selected</li> } </ol> |
Aliasing outer variables in nested loops
|
0 1 2 3 4 5 6 7 8 9 10 11 |
@for (category of categories(); track category.id) { <h3>{{ category.name }}</h3> @for (product of category.products; track product.id; let outer = $outer) { <p>{{ outer.name }} > {{ product.name }}</p> <!-- access outer loop --> } } |
3. @switch + @case + @default (Multi-way branching)
Replaces *ngSwitch.
Great when you have one value with many possible states (user role, order status, theme, etc.).
Syntax
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@switch (userRole()) { @case ('admin') { <app-admin-panel /> <p>Full access granted</p> } @case ('editor') { <app-editor-dashboard /> } @case ('viewer') @case ('guest') { <!-- multiple cases → same block (new in later versions) --> <app-read-only-view /> <p>Limited access</p> } @default { <p>Unknown role — contact support</p> } } |
- Uses strict === comparison
- No fallthrough (no need for break)
- @default is optional — if no match and no default → nothing renders
With signals
|
0 1 2 3 4 5 6 |
theme = signal<'light' | 'dark' | 'auto'>('auto'); |
|
0 1 2 3 4 5 6 7 8 9 10 |
@switch (theme()) { @case ('light') { <body class="light-theme">...</body> } @case ('dark') { <body class="dark-theme">...</body> } @default { <body class="system-theme">...</body> } } |
Quick Comparison Table (Old vs New)
| Feature | Old (*ngIf / *ngFor / *ngSwitch) | New (@if / @for / @switch) |
|---|---|---|
| Syntax | *ngIf=”…” on element | Block @if { … } |
| Else | Needs <ng-template #else> | Just @else { … } |
| Empty list | Manual *ngIf=”!items?.length” | Built-in @empty { … } |
| Tracking | trackBy function | Inline track expr (simpler & faster) |
| Nested readability | Lots of nesting & wrappers | Clean, indented blocks |
| Signal integration | Works, but Zone.js | Native, fine-grained reactivity |
| Imports needed? | CommonModule | Nothing — built-in |
Mini Practice Task (try this in your app!)
In your app.component.ts, add these signals:
|
0 1 2 3 4 5 6 7 |
items = signal<string[]>(['Apple', 'Banana']); filter = signal<'all' | 'fruit' | 'empty'>('all'); |
Then in template:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<button (click)="items.set([...items(), 'Orange'])">Add fruit</button> <button (click)="filter.set('empty')">Show empty</button> @switch (filter()) { @case ('all') { @for (item of items(); track $index) { <div>{{ item }}</div> } @empty { <div>Nothing here yet — add something!</div> } } @case ('fruit') { <div>Only fruits: 🍎🍌🍊</div> } @default { <div>Empty list mode activated</div> } } |
Play with adding items, changing filter — watch how smoothly it updates.
This chapter unlocks 80% of what makes Angular templates feel modern in 2026.
Next up is usually outputs & events (child talking back to parent) — or do you want to deepen any part here (track gotchas, migration tips, nested examples)? Just say the word! 😊
