--- name: angular-20-control-flow description: Angular 20 built-in control flow syntax (@if, @for, @switch, @defer) for modern template programming. Use when writing templates with conditional rendering, loops, switch statements, or lazy loading components. Replaces *ngIf, *ngFor, *ngSwitch with new block syntax for better performance and type safety. license: MIT --- # 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:** ```typescript @Component({ template: ` @if (isLoggedIn()) {
Welcome back, {{ username() }}!
} ` }) export class WelcomeComponent { isLoggedIn = signal(false); username = signal('User'); } ``` **@if with @else:** ```typescript @Component({ template: ` @if (user()) { } @else { } ` }) export class AppComponent { user = signal(null); } ``` **@if with @else if:** ```typescript @Component({ template: ` @if (status() === 'loading') { } @else if (status() === 'error') { } @else if (status() === 'success') { } @else { } ` }) export class DataComponent { status = signal<'loading' | 'error' | 'success' | 'idle'>('idle'); errorMessage = signal(''); data = signal([]); } ``` **Type Narrowing:** ```typescript @Component({ template: ` @if (item(); as currentItem) {
{{ currentItem.name }}
{{ currentItem.description }}
} ` }) export class ItemComponent { item = signal(null); } ``` #### 2. @for - List Iteration **Basic @for Loop:** ```typescript @Component({ template: `
    @for (item of items(); track item.id) {
  • {{ item.name }}
  • }
` }) 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:** ```typescript @Component({ template: `
@for (item of items(); track item.id; let idx = $index, first = $first, last = $last) {
{{ idx + 1 }}. {{ item.name }}
}
` }) export class IndexedListComponent { items = signal([]); } ``` **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:** ```typescript @Component({ template: `
@for (product of products(); track product.id) { } @empty {

No products available

}
` }) export class ProductListComponent { products = signal([]); } ``` **Track By Best Practices:** ```typescript // ✅ Good - Track by unique ID @for (user of users(); track user.id) { } // ✅ Good - Track by index for static lists @for (tab of tabs; track $index) { } // ❌ Bad - Track by object reference (will cause unnecessary re-renders) @for (item of items(); track item) {
{{ item.name }}
} ``` #### 3. @switch - Multi-branch Conditionals **Basic @switch:** ```typescript @Component({ template: ` @switch (userRole()) { @case ('admin') { } @case ('moderator') { } @case ('user') { } @default { } } ` }) export class RoleBasedComponent { userRole = signal<'admin' | 'moderator' | 'user' | 'guest'>('guest'); } ``` **@switch with Complex Conditions:** ```typescript @Component({ template: ` @switch (connectionStatus()) { @case ('connected') {
check_circle Connected
} @case ('connecting') {
Connecting...
} @case ('disconnected') {
error Disconnected
} @case ('error') {
warning Connection Error
} @default {
Unknown Status
} } ` }) export class ConnectionStatusComponent { connectionStatus = signal<'connected' | 'connecting' | 'disconnected' | 'error'>('disconnected'); } ``` #### 4. @defer - Lazy Loading and Code Splitting **Basic Deferred Loading:** ```typescript @Component({ template: ` @defer { } @placeholder {
Loading...
} ` }) export class DeferredComponent {} ``` **Defer with Loading State:** ```typescript @Component({ template: ` @defer { } @loading (minimum 500ms) {

Loading video player...

} @placeholder {
play_circle
} @error {

Failed to load video player

} ` }) export class VideoComponent { videoUrl = signal('https://example.com/video.mp4'); } ``` **Defer Triggers:** ```typescript // Viewport trigger - Load when visible @defer (on viewport) { } // Interaction trigger - Load on click @defer (on interaction) { } // Idle trigger - Load when browser is idle @defer (on idle) { } // Immediate trigger - Load immediately @defer (on immediate) { } // Timer trigger - Load after delay @defer (on timer(5s)) { } // Hover trigger - Load on hover @defer (on hover) { } // Combined triggers @defer (on viewport; on idle) { } ``` **Prefetching:** ```typescript // Prefetch when idle @defer (on viewport; prefetch on idle) { } // Prefetch on hover @defer (on interaction; prefetch on hover) { } ``` **Defer with Minimum Loading Time:** ```typescript @Component({ template: ` @defer (on viewport) { } @loading (minimum 1s) {
} @placeholder (minimum 500ms) {
} ` }) export class ChartComponent { chartData = signal([]); } ``` ### Migration from Old Syntax #### ngIf → @if ```typescript // Before (Angular 19 and earlier)
Content
{{ user.name }}
// After (Angular 20+) @if (isVisible()) {
Content
} @if (user(); as currentUser) {
{{ currentUser.name }}
} @else { } ``` #### ngFor → @for ```typescript // Before
  • {{ item.name }}
  • // After @for (item of items(); track item.id) {
  • {{ item.name }}
  • } ``` #### ngSwitch → @switch ```typescript // Before
    Success!
    Error!
    Loading...
    // After @switch (status()) { @case ('success') {
    Success!
    } @case ('error') {
    Error!
    } @default {
    Loading...
    } } ``` ### Best Practices #### 1. Use Signals with Control Flow ```typescript // ✅ Good - Reactive with signals export class Component { items = signal([]); isLoading = signal(false); } @Component({ template: ` @if (isLoading()) { } @else { @for (item of items(); track item.id) { } } ` }) ``` #### 2. Always Use track in @for ```typescript // ✅ Good - Proper tracking @for (user of users(); track user.id) { } // ❌ Bad - Missing track (will cause error) @for (user of users()) { } ``` ### 3. Leverage @defer for Performance ```typescript // ✅ Good - Defer heavy components @defer (on viewport) { } @placeholder {
    } // ✅ Good - Defer analytics @defer (on idle) { } ``` ### 4. Use @empty for Better UX ```typescript // ✅ Good - Handle empty state @for (item of items(); track item.id) { } @empty { } ``` ### 5. Type Narrowing with @if ```typescript // ✅ Good - Type narrowing @if (user(); as currentUser) {
    {{ currentUser.email }}
    } ``` ## 🔧 Advanced Patterns ### Nested Control Flow ```typescript @Component({ template: ` @if (data(); as currentData) { @for (category of currentData.categories; track category.id) {

    {{ category.name }}

    @for (item of category.items; track item.id) {
    {{ item.title }}
    } @empty {

    No items in this category

    }
    } } @else { } ` }) ``` ### Conditional Deferred Loading ```typescript @Component({ template: ` @if (shouldLoadHeavyComponent()) { @defer (on viewport) { } @loading { } } ` }) ``` ## 🐛 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](https://angular.dev/guide/templates/control-flow) - [Angular Defer Guide](https://angular.dev/guide/defer) - [Migration Guide](https://angular.dev/reference/migrations/control-flow) --- ## 📂 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.