Skip to main content

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 *ngIf with @if
  • Replace ng-template #ref with @else
  • Update else references

@for migration:

  • Replace *ngFor with @for
  • Add required track expression
  • Update context variable syntax
  • Replace empty checks with @empty

@switch migration:

  • Replace [ngSwitch] with @switch
  • Replace *ngSwitchCase with @case
  • Replace *ngSwitchDefault with @default

Performance Benefits

  • Better tree-shaking: Built-in syntax optimizes better
  • Faster rendering: No directive overhead
  • Type safety: Better TypeScript integration
  • Lazy loading: @defer enables 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.