New Control Flow Syntax
Overview
Angular 17+ introduces new built-in control flow syntax (@if, @for, @switch, @defer) to replace structural directives.
Benefits
- ✅ Better TypeScript integration
- ✅ Improved performance
- ✅ Cleaner syntax
- ✅ Built-in optimizations
- ✅ No need to import CommonModule
@if - Conditional Rendering
Basic Usage
Old way:
<div *ngIf="isLoggedIn">Welcome back!</div>
New way:
@if (isLoggedIn) {
<div>Welcome back!</div>
}
With Else Block
Old way:
<div *ngIf="isLoggedIn; else loginPrompt">Welcome back!</div>
<ng-template #loginPrompt>
<div>Please log in</div>
</ng-template>
New way:
@if (isLoggedIn) {
<div>Welcome back!</div>
} @else {
<div>Please log in</div>
}
Else If
@if (status === 'loading') {
<div>Loading...</div>
} @else if (status === 'error') {
<div>Error occurred!</div>
} @else if (status === 'success') {
<div>Success!</div>
} @else {
<div>Unknown status</div>
}
@for - List Rendering
Basic Loop
Old way:
<div *ngFor="let item of items; trackBy: trackById">{{ item.name }}</div>
New way:
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
}
Note: track is required (was optional in *ngFor)
With Index and Context Variables
@for (item of items; track item.id; let idx = $index) {
<div>{{ idx + 1 }}. {{ item.name }}</div>
}
Available context variables:
$index- Current index$first- Is first item$last- Is last item$even- Is even index$odd- Is odd index$count- Total items
Empty State
@for (user of users; track user.id) {
<div class="user-card">{{ user.name }}</div>
} @empty {
<div class="no-users">No users found</div>
}
@switch - Multi-way Branching
Old way:
<div [ngSwitch]="role">
<div *ngSwitchCase="'admin'">Admin Panel</div>
<div *ngSwitchCase="'user'">User Dashboard</div>
<div *ngSwitchDefault>Guest View</div>
</div>
New way:
@switch (role) { @case ('admin') {
<div>Admin Panel</div>
} @case ('user') {
<div>User Dashboard</div>
} @default {
<div>Guest View</div>
} }
@defer - Lazy Loading
Basic Deferred Loading
@defer {
<heavy-component />
} @placeholder {
<div>Loading...</div>
}
With Loading State
@defer {
<analytics-dashboard />
} @loading {
<div class="spinner">Loading analytics...</div>
} @placeholder {
<div>Click to load analytics</div>
} @error {
<div>Failed to load analytics</div>
}
Trigger Conditions
<!-- Trigger on viewport -->
@defer (on viewport) {
<image-gallery />
} @placeholder {
<div>Scroll to load gallery</div>
}
<!-- Trigger on interaction -->
@defer (on interaction) {
<comments-section />
} @placeholder {
<div>Click to load comments</div>
}
<!-- Trigger on idle -->
@defer (on idle) {
<recommendations />
}
<!-- Trigger on timer -->
@defer (on timer(5s)) {
<advertisement />
}
<!-- Trigger on hover -->
@defer (on hover) {
<tooltip-content />
}
<!-- Multiple triggers -->
@defer (on viewport; on timer(10s)) {
<widget />
}
Prefetching
@defer (on interaction; prefetch on idle) {
<video-player />
} @placeholder {
<div>Click to play video</div>
}
Real-World Examples
Product List with Loading States
import { Component, signal } from "@angular/core";
@Component({
selector: "app-product-list",
standalone: true,
template: `
<div class="products">
<h2>Products ({{ products().length }})</h2>
@if (isLoading()) {
<div class="loading">Loading products...</div>
} @else if (error()) {
<div class="error">{{ error() }}</div>
} @else { @for (product of products(); track product.id) {
<div class="product-card">
<h3>{{ product.name }}</h3>
<p>\${{ product.price }}</p>
@if (product.inStock) {
<span class="badge success">In Stock</span>
} @else {
<span class="badge danger">Out of Stock</span>
}
</div>
} @empty {
<div class="no-products">No products available</div>
} }
</div>
`,
})
export class ProductListComponent {
products = signal([
{ id: 1, name: "Product 1", price: 29.99, inStock: true },
{ id: 2, name: "Product 2", price: 49.99, inStock: false },
]);
isLoading = signal(false);
error = signal("");
}
Dashboard with Deferred Widgets
@Component({
selector: "app-dashboard",
standalone: true,
template: `
<div class="dashboard">
<!-- Critical content loads immediately -->
<div class="header">
<h1>Dashboard</h1>
</div>
<!-- Analytics loads on viewport -->
@defer (on viewport) {
<analytics-widget />
} @placeholder {
<div class="widget-placeholder">Analytics will load when visible</div>
} @loading (minimum 1s) {
<div class="spinner">Loading...</div>
}
<!-- Charts load on idle -->
@defer (on idle) {
<charts-widget />
} @placeholder {
<div class="widget-placeholder">Charts</div>
}
<!-- Heavy table loads on interaction -->
<div class="data-section">
@defer (on interaction; prefetch on idle) {
<data-table />
} @placeholder {
<button>Load Data Table</button>
}
</div>
</div>
`,
})
export class DashboardComponent {}
User Profile with Role-Based Content
@Component({
selector: "app-user-profile",
standalone: true,
template: `
<div class="profile">
@switch (userRole()) { @case ('admin') {
<admin-panel />
<user-management />
<system-settings />
} @case ('moderator') {
<moderation-tools />
<content-review />
} @case ('user') {
<user-dashboard />
<personal-settings />
} @default {
<guest-view />
} }
</div>
`,
})
export class UserProfileComponent {
userRole = signal<"admin" | "moderator" | "user" | "guest">("user");
}
Migration Guide
Automated Migration
# Angular CLI migration schematic
ng generate @angular/core:control-flow
Manual Migration Checklist
@if migration:
- Replace
*ngIfwith@if - Replace
ng-template #refwith@else - Update
elsereferences
@for migration:
- Replace
*ngForwith@for - Add required
trackexpression - Update context variable syntax
- Replace empty checks with
@empty
@switch migration:
- Replace
[ngSwitch]with@switch - Replace
*ngSwitchCasewith@case - Replace
*ngSwitchDefaultwith@default
Performance Benefits
- Better tree-shaking: Built-in syntax optimizes better
- Faster rendering: No directive overhead
- Type safety: Better TypeScript integration
- Lazy loading:
@deferenables granular code splitting
Interview Questions
Q: Why introduce new control flow instead of keeping directives? A: Better performance, improved type safety, cleaner syntax, and built-in optimizations that weren't possible with structural directives.
Q: What's the most powerful feature of @defer?
A: Declarative lazy loading with multiple triggers (viewport, interaction, idle) and built-in loading states.
Q: Is the track expression required in @for?
A: Yes, unlike trackBy in *ngFor, the track expression is required in @for for better default performance.