| |

Angular 9 ๐Ÿ…ฐ๏ธ Modern Template Control Flow โ€” @if, @for, @switch, @let

Angular 17 introduced a block syntax for control flow โ€” @if, @for, @switch, and @let โ€” that replaces the old *ngIf, *ngFor, and *ngSwitch directives. The new syntax is built into the template language itself, so it doesn’t need CommonModule imports, it’s faster at runtime, and it reads more like code. Everything the structural directives did, the block syntax does โ€” cleaner, more consistently, and with better type-checking.

Key point: @if, @for, and @switch are language features, not directives. They’re parsed by Angular’s template compiler directly, which means no imports, no <ng-template> expansion to reason about, and better error messages. @let is a bonus โ€” a way to declare a local template variable from an expression. Together they’re the modern way to shape a view.


Why the new syntax exists

The old structural directives worked, but they carried historical baggage.

Problems with *ngIf / *ngFor / *ngSwitch:

  • Required CommonModule in every standalone component
  • The asterisk hid a <ng-template> expansion that confused newcomers
  • You couldn’t stack two on one element
  • *ngIf="x; else y" and *ngFor="let a of b; trackBy: fn" had awkward micro-syntax
  • Error messages pointed at the expanded template, not your source
  • No equivalent of else if โ€” chaining required nested <ng-template> blocks

The block syntax fixes all of these. It’s not a wrapper around directives โ€” it’s a first-class part of the template language.

Side by side:

<!-- Old -->
<div *ngIf="user; else noUser">Hello, {{ user.name }}</div>
<ng-template #noUser>No user.</ng-template>

<!-- New -->
@if (user) {
  <div>Hello, {{ user.name }}</div>
} @else {
  <div>No user.</div>
}

The new form is closer to actual code, has no asterisk, and doesn’t need a template reference for the else branch.

Why a language change: Angular’s team could have kept improving the directives, but the template language was the right place. Control flow is syntax, not behavior โ€” it belongs in the parser, not in a library. That decision also unlocked runtime performance wins and better tooling.


@if โ€” conditional rendering

@if renders a block when a condition is truthy. It supports @else if chains and @else fallbacks.

export class UserComponent {
  user: { name: string; role: 'admin' | 'user' } | null = null;
  loading = false;
  error: string | null = null;
}
@if (loading) {
  <p>Loading...</p>
} @else if (error) {
  <p>Error: {{ error }}</p>
} @else if (user) {
  <p>Welcome, {{ user.name }}</p>
} @else {
  <p>Please log in.</p>
}

Exactly one branch renders. The conditions are evaluated top to bottom โ€” the first truthy one wins.

No asterisk, no template reference, no CommonModule. The syntax is a plain expression in parentheses, followed by a block in braces.

What changed from *ngIf:

OldNew
*ngIf="cond"@if (cond) { ... }
*ngIf="cond; else tpl"@if (cond) { ... } @else { ... }
nested *ngIf chains@else if (cond) { ... }
<ng-template #ref> for else@else { ... } inline

Truthy/falsy: Same rules as *ngIf โ€” any truthy value renders the block, any falsy value skips it. 0, '', null, undefined, NaN, false are all falsy.

Narrowing: Inside the block, TypeScript narrows the condition. If you write @if (user), then user is non-null inside โ€” no ?. needed.

@if (user) {
  <p>{{ user.name }}</p>  <!-- user is User, not User | null -->
}

That’s a real improvement over *ngIf โ€” the template type-checker understands the control flow.

Why narrowing matters: Without it, every property access on a possibly-null value needs ?. or a pipe. With @if, the template checker knows the block only runs when the condition was truthy, so it tightens the type.


@for โ€” repeating elements

@for repeats a block for each item in a collection. Its syntax is different from *ngFor โ€” the track clause is required, not optional.

export class ListComponent {
  users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
    { id: 3, name: 'Carol' }
  ];
}
<ul>
  @for (user of users; track user.id) {
    <li>{{ user.name }}</li>
  }
</ul>

Two things to notice. First, the shape: @for (item of collection; track expression) { ... }. Second, track is mandatory โ€” Angular refuses to compile @for without it.

Why track is required: Angular’s team decided that the performance cost of forgetting trackBy was too high. Making it mandatory forces developers to think about identity every time. It’s a deliberate choice.

The track expression: Usually a unique identifier โ€” track user.id, track item.sku, track $index for simple arrays.

@for (item of items; track $index) {
  <li>{{ item }}</li>
}

$index is fine for static lists. For dynamic lists, use a real ID.

Implicit variables: @for exposes the same loop variables as *ngFor, but with $ prefixes:

VariableMeaning
$indexZero-based position
$countTotal number of items
$firstFirst item?
$lastLast item?
$oddOdd index?
$evenEven index?
@for (user of users; track user.id; let i = $index, isLast = $last) {
  <li>{{ i + 1 }}. {{ user.name }} {{ isLast ? '(last)' : '' }}</li>
}

@empty block: @for has a built-in empty state โ€” no more *ngIf="items.length; else empty" dance.

@for (user of users; track user.id) {
  <li>{{ user.name }}</li>
} @empty {
  <li>No users found.</li>
}

If users is empty, the @empty block renders. If not, the loop renders.

Why @empty exists: The “list or empty state” pattern was so common that Angular baked it in. It removes a wrapper element and a template reference from every list in every app.


@switch โ€” picking one branch

@switch renders exactly one branch based on a value.

export class StatusComponent {
  status: 'loading' | 'success' | 'error' = 'loading';
}
@switch (status) {
  @case ('loading') {
    <p>Loading...</p>
  }
  @case ('success') {
    <p>Done!</p>
  }
  @case ('error') {
    <p>Something went wrong.</p>
  }
  @default {
    <p>Unknown status.</p>
  }
}

The shape is close to a switch statement in JavaScript. No [ngSwitch] binding on a container, no asterisks on the branches โ€” the whole thing is one block.

Differences from *ngSwitch:

OldNew
[ngSwitch]="x" on container@switch (x) { ... }
*ngSwitchCase="'a'"@case ('a') { ... }
*ngSwitchDefault@default { ... }

Comparison: @switch uses === for case matching. Strings, numbers, and booleans work cleanly. Objects and arrays won’t match unless they’re the same reference โ€” the same rule as a JS switch.

When to use @switch vs @if:

  • @if โ€” two branches
  • @if/else if chain โ€” three to four branches
  • @switch โ€” five or more branches, or when a single value picks the outcome

@switch is clearer when one value drives the choice.

Why no fallthrough: Angular’s @switch doesn’t fall through like JS switch. Each @case is independent โ€” there’s no break to forget. That removes a classic source of bugs.


@let โ€” declaring a local template variable

@let declares a local variable inside the template, bound to an expression.

export class UserComponent {
  user = { firstName: 'Alice', lastName: 'Johnson', age: 30 };
}
@let fullName = user.firstName + ' ' + user.lastName;
@let isAdult = user.age >= 18;

<p>{{ fullName }}</p>
<p>{{ isAdult ? 'Adult' : 'Minor' }}</p>

Each @let creates a variable usable anywhere below it in the template. The expression is evaluated on every change detection cycle โ€” same as interpolation.

Why @let is useful:

  • Avoid repeating expressions. Write user.firstName + ' ' + user.lastName once, use fullName multiple times.
  • Narrow types. @let u = user! or use @if narrowing to declare a non-null local.
  • Split complex expressions. Break a long interpolation into readable steps.
  • Cache slightly-expensive computations. The value is captured, so it’s not recomputed for each use.

Scope: A @let is visible from its declaration to the end of the enclosing block. Inside an @if, it’s only visible inside that @if.

@if (user) {
  @let name = user.name;
  <p>{{ name }}</p>
} @else {
  <!-- name is not visible here -->
  <p>No user.</p>
}

Not the same as a property: @let doesn’t create component state. It’s a template-local alias. It can’t be assigned to โ€” it’s read-only.

Example with narrowing:

@let u = user;
@if (u) {
  <p>{{ u.name }}</p>
}

Or, more usefully, right inside a branch:

@if (user) {
  @let displayName = user.nickname ?? user.name;
  <h2>{{ displayName }}</h2>
  <p>{{ displayName }} has {{ user.followers }} followers.</p>
}

displayName is computed once and reused.

Why @let was added: The template expression language is expression-only โ€” no statements, no assignments. That’s a safety feature, but it means you can’t write let x = expr the way you would in code. @let gives you the missing piece: a way to name an intermediate value without adding a component property.


Migrating from *ngIf / *ngFor / *ngSwitch

Angular ships an automated migration that converts the old directives to the new syntax.

ng generate @angular/core:control-flow

The schematic walks every template and rewrites:

<!-- Before -->
<div *ngIf="user">...</div>
<div *ngIf="user; else noUser">...</div>
<ng-template #noUser>...</ng-template>
<li *ngFor="let u of users; trackBy: trackById">...</li>
<div [ngSwitch]="status">
  <p *ngSwitchCase="'a'">A</p>
  <p *ngSwitchDefault>B</p>
</div>
<!-- After -->
@if (user) { ... }
@if (user) { ... } @else { ... }
@for (u of users; track u.id) { <li>...</li> }
@switch (status) {
  @case ('a') { <p>A</p> }
  @default { <p>B</p> }
}

The schematic handles trackBy by inlining the function body or asking you to provide a track expression. It doesn’t delete CommonModule imports automatically โ€” do that yourself once nothing else needs them.

Why migrate: The new syntax is faster at runtime, easier to read, and eliminates the CommonModule import for control-flow-only components. New code should use blocks; old code should be migrated when convenient.


A full example

A component that uses all four features.

The class:

import { Component } from '@angular/core';

interface User {
  id: number;
  name: string;
  nickname: string | null;
  role: 'admin' | 'user' | 'guest';
  active: boolean;
}

@Component({
  selector: 'app-dashboard',
  standalone: true,
  templateUrl: './dashboard.component.html',
  styleUrl: './dashboard.component.css'
})
export class DashboardComponent {
  loading = false;
  error: string | null = null;

  users: User[] = [
    { id: 1, name: 'Alice', nickname: 'Al', role: 'admin', active: true },
    { id: 2, name: 'Bob', nickname: null, role: 'user', active: false },
    { id: 3, name: 'Carol', nickname: 'C', role: 'guest', active: true }
  ];

  currentStatus: 'loading' | 'ready' | 'error' = 'ready';
}

The template:

<h1>Dashboard</h1>

@switch (currentStatus) {
  @case ('loading') {
    <p>Loading users...</p>
  }
  @case ('error') {
    <p>Error: {{ error }}</p>
  }
  @default {
    <ul>
      @for (user of users; track user.id; let i = $index) {
        @let displayName = user.nickname ?? user.name;
        @let label = user.role === 'admin' ? 'Administrator' : 'Member';
        <li [class.inactive]="!user.active">
          {{ i + 1 }}. {{ displayName }} โ€” {{ label }}
          @if (!user.active) {
            <span>(inactive)</span>
          }
        </li>
      } @empty {
        <li>No users found.</li>
      }
    </ul>
  }
}

The styles:

.inactive {
  color: gray;
}

Notes on the template:

  • @switch picks the top-level state
  • @for iterates with track user.id
  • $index gives the position
  • @let declares displayName and label inside the loop
  • @if inside the loop shows a marker for inactive users
  • @empty handles the no-users case

Each block is self-contained. No imports. No template references. No asterisks.

Why this shape: The new syntax is designed to compose. @switch at the top, @for inside, @let for local aliases, @if for per-item conditionals. The result reads top to bottom without <ng-template> interruptions.


Complete Example Session

# ============================================
# PART 1: GENERATE A COMPONENT
# ============================================

ng generate component dashboard
# [ CREATE src/app/dashboard/dashboard.component.ts ]
# [ CREATE src/app/dashboard/dashboard.component.html ]
# [ CREATE src/app/dashboard/dashboard.component.css ]

# ============================================
# PART 2: WRITE THE CLASS
# ============================================

cat > src/app/dashboard/dashboard.component.ts << 'EOF'
import { Component } from '@angular/core';

interface User {
  id: number;
  name: string;
  nickname: string | null;
  role: 'admin' | 'user';
  active: boolean;
}

@Component({
  selector: 'app-dashboard',
  standalone: true,
  templateUrl: './dashboard.component.html',
  styleUrl: './dashboard.component.css'
})
export class DashboardComponent {
  users: User[] = [
    { id: 1, name: 'Alice', nickname: 'Al', role: 'admin', active: true },
    { id: 2, name: 'Bob', nickname: null, role: 'user', active: false },
    { id: 3, name: 'Carol', nickname: 'C', role: 'user', active: true }
  ];
}
EOF

# ============================================
# PART 3: WRITE THE TEMPLATE
# ============================================

cat > src/app/dashboard/dashboard.component.html << 'EOF'
<h1>Users</h1>

@if (users.length === 0) {
  <p>No users yet.</p>
} @else {
  <ul>
    @for (user of users; track user.id; let i = $index) {
      @let displayName = user.nickname ?? user.name;
      <li [class.inactive]="!user.active">
        {{ i + 1 }}. {{ displayName }}
        @if (!user.active) {
          <span>(inactive)</span>
        }
      </li>
    } @empty {
      <li>Empty.</li>
    }
  </ul>
}
EOF

# ============================================
# PART 4: ADD A SWITCH
# ============================================

cat > src/app/status/status.component.ts << 'EOF'
import { Component } from '@angular/core';

@Component({
  selector: 'app-status',
  standalone: true,
  template: `
    @switch (status) {
      @case ('loading') { <p>Loading...</p> }
      @case ('success') { <p>Done!</p> }
      @case ('error') { <p>Error!</p> }
      @default { <p>Unknown.</p> }
    }
  `
})
export class StatusComponent {
  status: 'loading' | 'success' | 'error' | 'idle' = 'loading';
}
EOF

# ============================================
# PART 5: SERVE
# ============================================

ng serve
# [ Local:   http://localhost:4200/ ]

Quick Reference

Block Syntax Summary

BlockPurpose
@if (cond) { }Conditional
@else if (cond) { }Additional branch
@else { }Fallback
@for (item of items; track expr) { }Loop
@empty { }Empty-state for @for
@switch (value) { }Multi-branch
@case (val) { }One case
@default { }Fallback case
@let name = expr;Local template variable

@if Forms

FormMeaning
@if (cond) { ... }Render when truthy
@else if (cond) { ... }Additional condition
@else { ... }Fallback

@for Loop Variables

VariableMeaning
$indexZero-based position
$countTotal items
$firstFirst item?
$lastLast item?
$oddOdd index?
$evenEven index?

@for Syntax

PartRequired
item of itemsโœ…
track exprโœ… (mandatory)
let i = $indexOptional
@empty { }Optional

@switch Forms

FormMeaning
@switch (value) { }Container
@case (val) { }One branch
@default { }Fallback

@let Rules

RuleValue
ScopeFrom declaration to end of block
AssignableโŒ read-only
RecomputedEvery change detection cycle
Narrowingโœ… works with @if

Old vs New

OldNew
*ngIf="c"@if (c) { }
*ngIf="c; else t"@if (c) { } @else { }
*ngFor="let x of xs; trackBy: f"@for (x of xs; track x.id) { }
[ngSwitch]="v"@switch (v) { }
*ngSwitchCase="v"@case (v) { }
*ngSwitchDefault@default { }

Migration Command

CommandPurpose
ng generate @angular/core:control-flowAuto-migrate old syntax

Best Practices

โœ… Do This:

<!-- Use @if for conditions -->
@if (user) { <p>{{ user.name }}</p> }                       <!-- โœ… -->

<!-- Use @else if for chains -->
@if (a) { } @else if (b) { } @else { }                       <!-- โœ… -->

<!-- Always provide track -->
@for (u of users; track u.id) { <li>{{ u.name }}</li> }      <!-- โœ… -->

<!-- Use @empty for empty state -->
@for (u of users; track u.id) { } @empty { <p>None.</p> }    <!-- โœ… -->

<!-- Use @let for repeated expressions -->
@let name = user.nickname ?? user.name;                      <!-- โœ… -->

<!-- Use $index for position -->
@for (x of xs; track $index; let i = $index) { }             <!-- โœ… -->

<!-- Use @switch for many branches -->
@switch (status) { @case ('a') { } @default { } }            <!-- โœ… -->

<!-- Rely on narrowing -->
@if (user) { <p>{{ user.name }}</p> }                        <!-- โœ… no ?. -->

โŒ Don’t Do This:

<!-- Don't forget track -->
@for (u of users) { }                                        <!-- โŒ compile error -->

<!-- Don't mix old and new syntax -->
<div *ngIf="x">@if (y) { }</div>                             <!-- โš ๏ธ pick one -->

<!-- Don't import CommonModule just for control flow -->
imports: [CommonModule]                                      <!-- โš ๏ธ no longer needed -->

<!-- Don't use @switch for two branches -->
@switch (x) { @case ('a') { } @default { } }                 <!-- โš ๏ธ use @if -->

<!-- Don't assign to @let -->
@let x = 1; @let x = 2;                                      <!-- โŒ -->

<!-- Don't use @let for component state -->
@let count = 0;  <!-- this isn't a component property -->    <!-- โš ๏ธ use a class field -->

<!-- Don't use $index as track for dynamic lists -->
@for (u of users; track $index) { }                          <!-- โš ๏ธ use u.id -->

Common Pitfalls

PitfallProblemSolution
Missing trackCompile errorAlways provide track
track $index on dynamic listFull re-renderTrack by ID
Using *ngIf alongside @ifConfusing mixMigrate to blocks
Importing CommonModule unnecessarilyDead importRemove once nothing uses it
@let used outside its scopeNot visibleDeclare at the right level
Assigning to @let variableRead-onlyUse a component property
@switch with === on objectsNever matchesMatch by primitive
@case without @defaultUnmatched value renders nothingAdd @default

Real-World Examples

1. Simple @if

@if (isLoggedIn) {
  <p>Welcome back!</p>
}

2. @if / @else

@if (user) {
  <p>Hello, {{ user.name }}</p>
} @else {
  <p>Please log in.</p>
}

3. @if / @else if chain

@if (status === 'loading') { <p>Loading...</p> }
@else if (status === 'error') { <p>Error.</p> }
@else { <p>Ready.</p> }

4. Basic @for

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

5. @for with index

@for (item of items; track item.id; let i = $index) {
  <li>{{ i + 1 }}. {{ item.name }}</li>
}

6. @for with @empty

@for (u of users; track u.id) {
  <li>{{ u.name }}</li>
} @empty {
  <li>No users.</li>
}

7. @for with $last

@for (u of users; track u.id; let last = $last) {
  <li>{{ u.name }}{{ last ? '' : ',' }}</li>
}

8. @switch on a status

@switch (status) {
  @case ('loading') { <p>Loading...</p> }
  @case ('ready') { <p>Ready.</p> }
  @default { <p>Unknown.</p> }
}

9. @let for a computed display name

@let displayName = user.nickname ?? user.name;
<p>{{ displayName }}</p>

10. @let inside @for

@for (u of users; track u.id) {
  @let label = u.role === 'admin' ? 'Admin' : 'Member';
  <li>{{ u.name }} โ€” {{ label }}</li>
}

11. Nested blocks

@if (users.length) {
  @for (u of users; track u.id) {
    <li>{{ u.name }}</li>
  }
} @else {
  <p>No users.</p>
}

12. @for with @if inside

@for (u of users; track u.id) {
  <li>
    {{ u.name }}
    @if (u.admin) { <span>(admin)</span> }
  </li>
}

13. @let with @if narrowing

@if (user) {
  @let name = user.name;
  <p>{{ name }}</p>
}

14. @switch inside @for

@for (item of items; track item.id) {
  @switch (item.type) {
    @case ('text') { <p>{{ item.value }}</p> }
    @case ('image') { <img [src]="item.value"> }
    @default { <p>?</p> }
  }
}

15. Run the migration

ng generate @angular/core:control-flow

Visual: Block Syntax Structure

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  @if (condition) {                           โ”‚
โ”‚    ...                                       โ”‚
โ”‚  } @else if (cond2) {                        โ”‚
โ”‚    ...                                       โ”‚
โ”‚  } @else {                                   โ”‚
โ”‚    ...                                       โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  @for (item of items; track item.id) {       โ”‚
โ”‚    ...                                       โ”‚
โ”‚  } @empty {                                  โ”‚
โ”‚    ...                                       โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  @switch (value) {                           โ”‚
โ”‚    @case ('a') { ... }                       โ”‚
โ”‚    @default { ... }                          โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  @let name = expression;                     โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: @for Loop Variables

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  @for (u of users; track u.id;               โ”‚
โ”‚        let i = $index,                       โ”‚
โ”‚            last = $last) {                   โ”‚
โ”‚    ...                                       โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  $index  โ†’ 0, 1, 2, ...                      โ”‚
โ”‚  $count  โ†’ total items                       โ”‚
โ”‚  $first  โ†’ true on index 0                   โ”‚
โ”‚  $last   โ†’ true on last item                 โ”‚
โ”‚  $odd    โ†’ true on odd indices               โ”‚
โ”‚  $even   โ†’ true on even indices              โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Old vs New โ€” Same Logic

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  OLD                                         โ”‚
โ”‚                                              โ”‚
โ”‚  <div *ngIf="user; else noUser">             โ”‚
โ”‚    Hello, {{ user.name }}                    โ”‚
โ”‚  </div>                                      โ”‚
โ”‚  <ng-template #noUser>                       โ”‚
โ”‚    No user.                                  โ”‚
โ”‚  </ng-template>                              โ”‚
โ”‚                                              โ”‚
โ”‚  Requires CommonModule import                โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  NEW                                         โ”‚
โ”‚                                              โ”‚
โ”‚  @if (user) {                                โ”‚
โ”‚    <div>Hello, {{ user.name }}</div>         โ”‚
โ”‚  } @else {                                   โ”‚
โ”‚    <div>No user.</div>                       โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  No imports needed                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: @let Scope

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Template                                    โ”‚
โ”‚                                              โ”‚
โ”‚  @let topLevel = 'visible everywhere below'; โ”‚
โ”‚                                              โ”‚
โ”‚  <p>{{ topLevel }}</p>          โœ…           โ”‚
โ”‚                                              โ”‚
โ”‚  @if (cond) {                                โ”‚
โ”‚    @let inner = 'only inside @if';           โ”‚
โ”‚    <p>{{ inner }}</p>            โœ…           โ”‚
โ”‚    <p>{{ topLevel }}</p>         โœ…           โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  <p>{{ inner }}</p>              โŒ           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Migration Path

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Step 1: Run the schematic                   โ”‚
โ”‚                                              โ”‚
โ”‚  ng generate @angular/core:control-flow      โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Step 2: Review the changes                  โ”‚
โ”‚                                              โ”‚
โ”‚  *ngIf โ†’ @if                                 โ”‚
โ”‚  *ngFor โ†’ @for with track                    โ”‚
โ”‚  *ngSwitch โ†’ @switch                         โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Step 3: Clean up imports                    โ”‚
โ”‚                                              โ”‚
โ”‚  Remove CommonModule if nothing else needs   โ”‚
โ”‚  it (pipes, ngClass, ngStyle, etc.)          โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

BlockPurposeReplaces
@ifConditional*ngIf
@else ifChained conditionnested *ngIf / <ng-template>
@elseFallback*ngIf="x; else t"
@forLoop*ngFor
@emptyEmpty-state*ngIf="xs.length; else t"
@switchMulti-branch[ngSwitch]
@caseOne branch*ngSwitchCase
@defaultFallback branch*ngSwitchDefault
@letLocal variable(no equivalent)

Key takeaways:

  • The new block syntax is part of the template language โ€” no CommonModule imports
  • @if supports @else if and @else โ€” cleaner than nested <ng-template>s
  • @for requires a track expression โ€” no more optional trackBy
  • @for exposes $index, $count, $first, $last, $odd, $even
  • @empty handles the empty-list case inline โ€” no template references
  • @switch matches with === and has no fallthrough โ€” no break to forget
  • @let declares a local template variable โ€” read-only, scoped, useful for aliases and narrowing
  • The template type-checker narrows inside @if blocks โ€” no ?. needed
  • Migrate automatically with ng generate @angular/core:control-flow
  • New Angular code should use blocks, not directives โ€” legacy syntax remains for compatibility

Remember: @if, @for, @switch, and @let are the modern way to shape an Angular view. They’re language features, not imports, and they read more like code. @for requires track, @if narrows types, @empty handles the list-or-nothing case, and @let gives you a way to name intermediate values without adding component state. Learn these blocks first, and treat *ngIf/*ngFor as history you’ll encounter in older codebases.


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!