Built-in Control Flow (v17)
📚 What is Built-in Control Flow?
Angular 17 introduced new template syntax replacing structural directives:
- @if / @else: Replaces *ngIf
- @for with track: Replaces *ngFor (track is required!)
- @switch / @case / @default: Replaces *ngSwitch
- @empty: Shows content when @for array is empty
- Better performance: Optimized change detection
- Better type narrowing: TypeScript understands the types better
🎯 Interview Questions
- Q1: Why is track required in @for?
- A: Track identifies items for efficient DOM updates. Without it, Angular can't optimize re-renders. Use unique identifier.
- Q2: What are the advantages of new control flow over *ngIf/*ngFor?
- A: Better performance, cleaner syntax, built-in (no imports needed), better type narrowing, required track prevents bugs.
- Q3: What implicit variables are available in @for?
- A: $index, $first, $last, $even, $odd, $count
- Q4: Can you still use *ngIf and *ngFor?
- A: Yes, they still work but new syntax is recommended. You need to import CommonModule/NgIf/NgFor for old syntax.
🔧 @if / @else Demo
✅ Welcome back!
You are a regular user.
🔧 @for with track Demo
1. Learn Angular signals First
2. Master control flow
3. Build demo app Last
Total: 3 | Completed: 1
🔧 @switch / @case Demo
🟡 Medium Priority - Complete this week
📋 Old vs New Syntax
❌ Old (*ngIf, *ngFor)
<div *ngIf="isLoggedIn; else loggedOut">
Welcome!
</div>
<ng-template #loggedOut>
Please log in
</ng-template>
<li *ngFor="let item of items;
let i = index;
trackBy: trackById">
{{ i }}: {{ item.name }}
</li>
<div [ngSwitch]="status">
<p *ngSwitchCase="'active'">Active</p>
<p *ngSwitchDefault>Unknown</p>
</div>✅ New (@if, @for)
@if (isLoggedIn) {
<div>Welcome!</div>
} @else {
<div>Please log in</div>
}
@for (item of items; track item.id;
let i = $index) {
<li>{{ i }}: {{ item.name }}</li>
} @empty {
<li>No items</li>
}
@switch (status) {
@case ('active') {
<p>Active</p>
}
@default {
<p>Unknown</p>
}
}💻 Control Flow Code
// ===== @if / @else / @else if =====
@if (user) {
<p>Hello, {{ user.name }}!</p>
} @else if (loading) {
<p>Loading...</p>
} @else {
<p>Please log in</p>
}
// Type narrowing works!
@if (user) {
{{ user.email }} // TypeScript knows user is defined here
}
// ===== @for with track (REQUIRED!) =====
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
}
// Available variables:
// $index - zero-based index
// $first - true if first item
// $last - true if last item
// $even - true if even index
// $odd - true if odd index
// $count - total number of items
@for (user of users; track user.id;
let idx = $index,
let isFirst = $first,
let isLast = $last) {
<div [class.highlight]="isFirst">
{{ idx + 1 }}. {{ user.name }}
@if (isLast) { (Last one!) }
</div>
} @empty {
<p>No users found</p>
}
// track can be a property or expression
@for (item of items; track item.id) { }
@for (item of items; track item) { } // Track by reference
@for (item of items; track $index) { } // Track by index (use carefully)
// ===== @switch / @case / @default =====
@switch (status) {
@case ('pending') {
<span class="yellow">Pending...</span>
}
@case ('approved') {
<span class="green">Approved ✓</span>
}
@case ('rejected') {
<span class="red">Rejected ✗</span>
}
@default {
<span>Unknown status</span>
}
}
// ===== Nesting is clean =====
@for (category of categories; track category.id) {
<h2>{{ category.name }}</h2>
@for (item of category.items; track item.id) {
@if (item.inStock) {
<p>{{ item.name }} - Available</p>
}
} @empty {
<p>No items in this category</p>
}
}