angular-20-control-flow

Angular 20 Control Flow Skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "angular-20-control-flow" with this command: npx skills add 7spade/black-tortoise/7spade-black-tortoise-angular-20-control-flow

Angular 20 Control Flow Skill

Rules

Control Flow Syntax

  • Use @if / @else / @else if for conditional rendering

  • Use @for with mandatory track expression for list iteration

  • Use @switch / @case / @default for multi-branch conditionals

  • Use @defer for lazy loading and code splitting

  • MUST NOT use structural directives: *ngIf , *ngFor , *ngSwitch

@for Track Expression

  • Every @for loop MUST include a track expression

  • Track by unique ID: track item.id

  • Track by index for static lists: track $index

  • MUST NOT track by object reference

@defer Loading States

  • Use appropriate trigger: on viewport , on interaction , on idle , on immediate , on timer(Xs) , on hover

  • Use @loading (minimum Xms) to prevent UI flashing

  • Use @placeholder (minimum Xms) for minimum display time

Signal Integration

  • Control flow conditions MUST use signal invocation: @if (signal())

  • MUST NOT use plain properties without signal invocation

Context Variables

  • Available in @for : $index , $first , $last , $even , $odd , $count

Context

Purpose

This skill provides comprehensive guidance on Angular 20's built-in control flow syntax, which introduces new template syntax (@if, @for, @switch, @defer) that replaces structural directives with better performance, type safety, and developer experience.

What is Angular Control Flow?

Angular 20 introduces new built-in control flow syntax:

  • @if / @else: Conditional rendering (replaces *ngIf)

  • @for: List iteration with tracking (replaces *ngFor)

  • @switch / @case: Multi-branch conditionals (replaces *ngSwitch)

  • @defer: Lazy loading and code splitting (new feature)

  • @empty: Fallback for empty collections

  • @placeholder / @loading / @error: Defer states

When to Use This Skill

Use Angular 20 Control Flow when:

  • Writing templates with conditional rendering

  • Iterating over lists or arrays

  • Implementing switch/case logic in templates

  • Lazy loading components or content blocks

  • Handling loading states and error boundaries

  • Optimizing bundle size with deferred loading

  • Migrating from *ngIf, *ngFor, *ngSwitch to modern syntax

Core Control Flow Blocks

  1. @if - Conditional Rendering

Basic Usage:

@Component({ template: @if (isLoggedIn()) { <div>Welcome back, {{ username() }}!</div> } }) export class WelcomeComponent { isLoggedIn = signal(false); username = signal('User'); }

@if with @else:

@Component({ template: @if (user()) { &#x3C;app-dashboard [user]="user()" /> } @else { &#x3C;app-login /> } }) export class AppComponent { user = signal<User | null>(null); }

@if with @else if:

@Component({ template: @if (status() === 'loading') { &#x3C;app-spinner /> } @else if (status() === 'error') { &#x3C;app-error [message]="errorMessage()" /> } @else if (status() === 'success') { &#x3C;app-content [data]="data()" /> } @else { &#x3C;app-empty-state /> } }) export class DataComponent { status = signal<'loading' | 'error' | 'success' | 'idle'>('idle'); errorMessage = signal(''); data = signal<any[]>([]); }

Type Narrowing:

@Component({ template: @if (item(); as currentItem) { &#x3C;!-- currentItem is type-narrowed here --> &#x3C;div>{{ currentItem.name }}&#x3C;/div> &#x3C;div>{{ currentItem.description }}&#x3C;/div> } }) export class ItemComponent { item = signal<Item | null>(null); }

  1. @for - List Iteration

Basic @for Loop:

@Component({ template: &#x3C;ul> @for (item of items(); track item.id) { &#x3C;li>{{ item.name }}&#x3C;/li> } &#x3C;/ul> }) export class ListComponent { items = signal([ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }, { id: 3, name: 'Item 3' } ]); }

@for with Index and Context:

@Component({ template: &#x3C;div class="items"> @for (item of items(); track item.id; let idx = $index, first = $first, last = $last) { &#x3C;div class="item" [class.first]="first" [class.last]="last"> &#x3C;span class="index">{{ idx + 1 }}.&#x3C;/span> &#x3C;span class="name">{{ item.name }}&#x3C;/span> &#x3C;/div> } &#x3C;/div> }) export class IndexedListComponent { items = signal<Item[]>([]); }

Available Context Variables:

  • $index

  • Current index (0-based)

  • $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 with @empty:

@Component({ template: &#x3C;div class="product-list"> @for (product of products(); track product.id) { &#x3C;app-product-card [product]="product" /> } @empty { &#x3C;div class="empty-state"> &#x3C;p>No products available&#x3C;/p> &#x3C;button (click)="loadProducts()">Refresh&#x3C;/button> &#x3C;/div> } &#x3C;/div> }) export class ProductListComponent { products = signal<Product[]>([]); }

Track By Best Practices:

// ✅ Good - Track by unique ID @for (user of users(); track user.id) { <app-user-card [user]="user" /> }

// ✅ Good - Track by index for static lists @for (tab of tabs; track $index) { <button>{{ tab }}</button> }

// ❌ Bad - Track by object reference (will cause unnecessary re-renders) @for (item of items(); track item) { <div>{{ item.name }}</div> }

  1. @switch - Multi-branch Conditionals

Basic @switch:

@Component({ template: @switch (userRole()) { @case ('admin') { &#x3C;app-admin-panel /> } @case ('moderator') { &#x3C;app-moderator-panel /> } @case ('user') { &#x3C;app-user-panel /> } @default { &#x3C;app-guest-panel /> } } }) export class RoleBasedComponent { userRole = signal<'admin' | 'moderator' | 'user' | 'guest'>('guest'); }

@switch with Complex Conditions:

@Component({ template: @switch (connectionStatus()) { @case ('connected') { &#x3C;div class="status online"> &#x3C;mat-icon>check_circle&#x3C;/mat-icon> Connected &#x3C;/div> } @case ('connecting') { &#x3C;div class="status pending"> &#x3C;mat-spinner diameter="20">&#x3C;/mat-spinner> Connecting... &#x3C;/div> } @case ('disconnected') { &#x3C;div class="status offline"> &#x3C;mat-icon>error&#x3C;/mat-icon> Disconnected &#x3C;/div> } @case ('error') { &#x3C;div class="status error"> &#x3C;mat-icon>warning&#x3C;/mat-icon> Connection Error &#x3C;/div> } @default { &#x3C;div class="status unknown">Unknown Status&#x3C;/div> } } }) export class ConnectionStatusComponent { connectionStatus = signal<'connected' | 'connecting' | 'disconnected' | 'error'>('disconnected'); }

  1. @defer - Lazy Loading and Code Splitting

Basic Deferred Loading:

@Component({ template: @defer { &#x3C;app-heavy-component /> } @placeholder { &#x3C;div class="skeleton">Loading...&#x3C;/div> } }) export class DeferredComponent {}

Defer with Loading State:

@Component({ template: @defer { &#x3C;app-video-player [src]="videoUrl" /> } @loading (minimum 500ms) { &#x3C;div class="loading-spinner"> &#x3C;mat-spinner>&#x3C;/mat-spinner> &#x3C;p>Loading video player...&#x3C;/p> &#x3C;/div> } @placeholder { &#x3C;div class="video-placeholder"> &#x3C;mat-icon>play_circle&#x3C;/mat-icon> &#x3C;/div> } @error { &#x3C;div class="error-state"> &#x3C;p>Failed to load video player&#x3C;/p> &#x3C;button (click)="retry()">Retry&#x3C;/button> &#x3C;/div> } }) export class VideoComponent { videoUrl = signal('https://example.com/video.mp4'); }

Defer Triggers:

// Viewport trigger - Load when visible @defer (on viewport) { <app-below-fold-content /> }

// Interaction trigger - Load on click @defer (on interaction) { <app-modal-content /> }

// Idle trigger - Load when browser is idle @defer (on idle) { <app-analytics-widget /> }

// Immediate trigger - Load immediately @defer (on immediate) { <app-critical-content /> }

// Timer trigger - Load after delay @defer (on timer(5s)) { <app-delayed-content /> }

// Hover trigger - Load on hover @defer (on hover) { <app-tooltip-content /> }

// Combined triggers @defer (on viewport; on idle) { <app-content /> }

Prefetching:

// Prefetch when idle @defer (on viewport; prefetch on idle) { <app-article-content /> }

// Prefetch on hover @defer (on interaction; prefetch on hover) { <app-modal /> }

Defer with Minimum Loading Time:

@Component({ template: @defer (on viewport) { &#x3C;app-chart [data]="chartData()" /> } @loading (minimum 1s) { &#x3C;!-- Show loading for at least 1 second to avoid flashing --> &#x3C;div class="chart-skeleton">&#x3C;/div> } @placeholder (minimum 500ms) { &#x3C;!-- Show placeholder for at least 500ms --> &#x3C;div class="chart-placeholder">&#x3C;/div> } }) export class ChartComponent { chartData = signal<ChartData[]>([]); }

Migration from Old Syntax

ngIf → @if

// Before (Angular 19 and earlier) <div *ngIf="isVisible">Content</div> <div *ngIf="user; else loading">{{ user.name }}</div>

// After (Angular 20+) @if (isVisible()) { <div>Content</div> }

@if (user(); as currentUser) { <div>{{ currentUser.name }}</div> } @else { <ng-container [ngTemplateOutlet]="loading" /> }

ngFor → @for

// Before <li *ngFor="let item of items; trackBy: trackById">{{ item.name }}</li>

// After @for (item of items(); track item.id) { <li>{{ item.name }}</li> }

ngSwitch → @switch

// Before <div [ngSwitch]="status"> <div *ngSwitchCase="'success'">Success!</div> <div *ngSwitchCase="'error'">Error!</div> <div *ngSwitchDefault>Loading...</div> </div>

// After @switch (status()) { @case ('success') { <div>Success!</div> } @case ('error') { <div>Error!</div> } @default { <div>Loading...</div> } }

Best Practices

  1. Use Signals with Control Flow

// ✅ Good - Reactive with signals export class Component { items = signal<Item[]>([]); isLoading = signal(false); }

@Component({ template: @if (isLoading()) { &#x3C;spinner /> } @else { @for (item of items(); track item.id) { &#x3C;item-card [item]="item" /> } } })

  1. Always Use track in @for

// ✅ Good - Proper tracking @for (user of users(); track user.id) { <user-card [user]="user" /> }

// ❌ Bad - Missing track (will cause error) @for (user of users()) { <user-card [user]="user" /> }

  1. Leverage @defer for Performance

// ✅ Good - Defer heavy components @defer (on viewport) { <app-complex-chart /> } @placeholder { <div class="chart-skeleton"></div> }

// ✅ Good - Defer analytics @defer (on idle) { <app-analytics-tracker /> }

  1. Use @empty for Better UX

// ✅ Good - Handle empty state @for (item of items(); track item.id) { <item-card [item]="item" /> } @empty { <empty-state message="No items found" /> }

  1. Type Narrowing with @if

// ✅ Good - Type narrowing @if (user(); as currentUser) { <!-- currentUser is guaranteed non-null here --> <div>{{ currentUser.email }}</div> }

🔧 Advanced Patterns

Nested Control Flow

@Component({ template: @if (data(); as currentData) { @for (category of currentData.categories; track category.id) { &#x3C;div class="category"> &#x3C;h3>{{ category.name }}&#x3C;/h3> @for (item of category.items; track item.id) { &#x3C;div class="item">{{ item.title }}&#x3C;/div> } @empty { &#x3C;p>No items in this category&#x3C;/p> } &#x3C;/div> } } @else { &#x3C;app-loading /> } })

Conditional Deferred Loading

@Component({ template: @if (shouldLoadHeavyComponent()) { @defer (on viewport) { &#x3C;app-heavy-component [config]="config()" /> } @loading { &#x3C;skeleton-loader /> } } })

🐛 Troubleshooting

Issue Solution

Syntax error with @ blocks Ensure Angular 20+ and update compiler

@for without track error Always add track expression to @for

@defer not lazy loading Check bundle config and verify component is in separate chunk

Type errors with @if Use as alias for type narrowing

@empty not showing Ensure collection signal returns empty array, not undefined

📖 References

  • Angular Control Flow Guide

  • Angular Defer Guide

  • Migration Guide

📂 Recommended Placement

Project-level skill:

/.github/skills/angular-20-control-flow/SKILL.md

Copilot will load this when working with Angular 20 control flow syntax.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

material-design-3

No summary provided by upstream source.

Repository SourceNeeds Review
General

architecture-ddd

No summary provided by upstream source.

Repository SourceNeeds Review
General

webapp-testing

No summary provided by upstream source.

Repository SourceNeeds Review
General

angular-material-cdk-animations

No summary provided by upstream source.

Repository SourceNeeds Review