Skip to content

Control Flow (@if, @for, @switch) πŸ”„

Angular’s new control flow syntax is like upgrading from a flip phone to a smartphone - everything becomes cleaner, more intuitive, and more powerful. Think of @if, @for, and @switch as your new best friends for creating dynamic, responsive templates.

The new control flow syntax replaces structural directives with a more readable and performant approach. It’s like replacing complex assembly language with modern programming - same power, better experience.

Key Benefits:

  • ~45% Better Performance - Direct internal API usage vs structural directives
  • Improved Readability - Cleaner, more intuitive syntax
  • Type Safety - Better TypeScript integration
  • Less Boilerplate - No more *ngIf and template references
  • Enhanced Developer Experience - Better IDE support and autocomplete

How it works internally:

  • Old way (*ngIf): Uses ViewContainerRef API β†’ property() instruction β†’ checks value changes β†’ creates/destroys views
  • New way (@if): Uses direct internal APIs β†’ conditional() instruction β†’ directly creates/destroys views

This eliminates the extra steps of setting properties and checking for changes, making it significantly faster.

Migration Tip: Run ng g @angular/core:control-flow to automatically migrate your existing structural directives to the new syntax!

Think of @if like a smart bouncer - it only lets content through when conditions are met.

@Component({
template: `
<!-- Simple conditional rendering -->
@if (user()) {
<div class="user-info">
<h4>Welcome, {{ user()?.name }}!</h4>
<p>Email: {{ user()?.email }}</p>
</div>
}
<!-- Loading state -->
@if (isLoading()) {
<div class="loading">Loading user profile...</div>
}
<!-- Error state -->
@if (error()) {
<div class="error">
<p>{{ error() }}</p>
<button (click)="retry()">Try Again</button>
</div>
}
`
})
export class UserProfileComponent {
user = signal<User | null>(null);
isLoading = signal(false);
error = signal<string | null>(null);
retry() {
this.loadUser();
}
}

Think of @if/@else like a smart traffic light - it shows different content based on conditions.

@Component({
template: `
<div class="shopping-cart">
<h3>Shopping Cart</h3>
@if (cartItems().length > 0) {
<div class="cart-content">
<h4>{{ cartItems().length }} items in your cart</h4>
@for (item of cartItems(); track item.id) {
<div class="cart-item">
<span>{{ item.name }} - ${{ item.price }}</span>
<button (click)="removeItem(item.id)">Remove</button>
</div>
}
<div class="total">
<strong>Total: ${{ calculateTotal() }}</strong>
</div>
</div>
} @else {
<div class="empty-cart">
<h4>πŸ›’ Your cart is empty</h4>
<p>Add some items to get started!</p>
</div>
}
</div>
`
})
export class ShoppingCartComponent {
cartItems = signal<CartItem[]>([]);
calculateTotal(): number {
return this.cartItems().reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
}
removeItem(id: string) {
this.cartItems.update(items => items.filter(item => item.id !== id));
}
}

Think of @for like a smart assembly line - it efficiently creates components for each item in your list.

@Component({
template: `
<div class="task-list">
<h3>My Tasks</h3>
<!-- List with @for and track -->
@for (task of tasks(); track task.id) {
<div class="task-item">
<input
type="checkbox"
[checked]="task.completed"
(change)="toggleTask(task.id)"
/>
<span [class.completed]="task.completed">
{{ task.title }}
</span>
<button (click)="deleteTask(task.id)">Delete</button>
</div>
} @empty {
<div class="no-tasks">
<h4>πŸŽ‰ No tasks yet!</h4>
<p>Create your first task to get started.</p>
</div>
}
</div>
`
})
export class TaskListComponent {
tasks = signal<Task[]>([
{ id: '1', title: 'Learn Angular Control Flow', completed: false },
{ id: '2', title: 'Build a todo app', completed: true }
]);
toggleTask(taskId: string) {
this.tasks.update(tasks =>
tasks.map(task =>
task.id === taskId
? { ...task, completed: !task.completed }
: task
)
);
}
deleteTask(taskId: string) {
this.tasks.update(tasks => tasks.filter(task => task.id !== taskId));
}
}

Think of @switch like a smart receptionist - it directs you to the right place based on your needs.

@Component({
template: `
<div class="dashboard">
<h3>User Dashboard</h3>
<!-- Navigation tabs -->
<div class="tabs">
<button (click)="setActiveTab('profile')">Profile</button>
<button (click)="setActiveTab('settings')">Settings</button>
<button (click)="setActiveTab('notifications')">Notifications</button>
</div>
<!-- Dynamic content based on active tab -->
@switch (activeTab()) {
@case ('profile') {
<div class="profile-section">
<h4>Profile Information</h4>
<input
type="text"
[value]="userProfile().name"
placeholder="Name"
/>
<input
type="email"
[value]="userProfile().email"
placeholder="Email"
/>
</div>
}
@case ('settings') {
<div class="settings-section">
<h4>Application Settings</h4>
<label>
<input
type="checkbox"
[checked]="settings().darkMode"
/>
Dark Mode
</label>
<label>
<input
type="checkbox"
[checked]="settings().notifications"
/>
Email Notifications
</label>
</div>
}
@case ('notifications') {
<div class="notifications-section">
<h4>Recent Notifications</h4>
@for (notification of notifications(); track notification.id) {
<div class="notification">
@switch (notification.type) {
@case ('success') { βœ… }
@case ('warning') { ⚠️ }
@case ('error') { ❌ }
}
<span>{{ notification.message }}</span>
</div>
}
</div>
}
@default {
<div>Welcome! Select a tab above.</div>
}
}
</div>
`
})
export class DashboardComponent {
activeTab = signal<string>('profile');
userProfile = signal({
name: 'John Doe',
email: 'john@example.com'
});
settings = signal({
darkMode: false,
notifications: true
});
notifications = signal([
{ id: '1', type: 'success', message: 'Profile updated successfully!' },
{ id: '2', type: 'warning', message: 'Password expires in 7 days' }
]);
setActiveTab(tab: string) {
this.activeTab.set(tab);
}
}
// βœ… Good - Always use track for @for
@for (item of items(); track item.id) {
<div>{{ item.name }}</div>
}
// ❌ Avoid - Missing track function
@for (item of items()) {
<div>{{ item.name }}</div>
}
// βœ… Good - Handle empty states
@for (item of items(); track item.id) {
<div>{{ item.name }}</div>
} @empty {
<div class="empty-state">No items found</div>
}
// ❌ Avoid - No empty state handling
@for (item of items(); track item.id) {
<div>{{ item.name }}</div>
}
// βœ… Good - Clear, readable conditions
@if (user() && user()!.isActive) {
<div>Welcome back!</div>
}
// ❌ Avoid - Complex inline logic
@if (user() && user()!.status === 'active' && user()!.lastLogin > Date.now() - 86400000) {
<div>Welcome back!</div>
}
  • Use @if for conditional rendering
  • Use @for with track functions for lists
  • Use @switch for multiple conditions
  • Handle empty states with @empty
  • Use @else for alternative content
  • Keep conditions simple and readable
  • Leverage computed signals for complex logic
  • Test all control flow branches
  1. Zoneless Angular - Performance optimization
  2. New Template Syntax - @let and more
  3. Signal Forms - Modern form handling

Remember: The new control flow syntax is like having a smart assistant that makes your templates cleaner and more performant. Use @if, @for, and @switch to create dynamic, responsive user interfaces! πŸ”„