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
CommonModulein 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:
| Old | New |
|---|---|
*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:
| Variable | Meaning |
|---|---|
$index | Zero-based position |
$count | Total number of items |
$first | First item? |
$last | Last item? |
$odd | Odd index? |
$even | Even 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
@emptyexists: 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:
| Old | New |
|---|---|
[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 ifchain โ 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
@switchdoesn’t fall through like JSswitch. Each@caseis independent โ there’s nobreakto 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.lastNameonce, usefullNamemultiple times. - Narrow types.
@let u = user!or use@ifnarrowing 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
@letwas added: The template expression language is expression-only โ no statements, no assignments. That’s a safety feature, but it means you can’t writelet x = exprthe way you would in code.@letgives 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
CommonModuleimport 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:
@switchpicks the top-level state@foriterates withtrack user.id$indexgives the position@letdeclaresdisplayNameandlabelinside the loop@ifinside the loop shows a marker for inactive users@emptyhandles 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.
@switchat the top,@forinside,@letfor local aliases,@iffor 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
| Block | Purpose |
|---|---|
@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
| Form | Meaning |
|---|---|
@if (cond) { ... } | Render when truthy |
@else if (cond) { ... } | Additional condition |
@else { ... } | Fallback |
@for Loop Variables
| Variable | Meaning |
|---|---|
$index | Zero-based position |
$count | Total items |
$first | First item? |
$last | Last item? |
$odd | Odd index? |
$even | Even index? |
@for Syntax
| Part | Required |
|---|---|
item of items | โ |
track expr | โ (mandatory) |
let i = $index | Optional |
@empty { } | Optional |
@switch Forms
| Form | Meaning |
|---|---|
@switch (value) { } | Container |
@case (val) { } | One branch |
@default { } | Fallback |
@let Rules
| Rule | Value |
|---|---|
| Scope | From declaration to end of block |
| Assignable | โ read-only |
| Recomputed | Every change detection cycle |
| Narrowing | โ
works with @if |
Old vs New
| Old | New |
|---|---|
*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
| Command | Purpose |
|---|---|
ng generate @angular/core:control-flow | Auto-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
| Pitfall | Problem | Solution |
|---|---|---|
Missing track | Compile error | Always provide track |
track $index on dynamic list | Full re-render | Track by ID |
Using *ngIf alongside @if | Confusing mix | Migrate to blocks |
Importing CommonModule unnecessarily | Dead import | Remove once nothing uses it |
@let used outside its scope | Not visible | Declare at the right level |
Assigning to @let variable | Read-only | Use a component property |
@switch with === on objects | Never matches | Match by primitive |
@case without @default | Unmatched value renders nothing | Add @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
| Block | Purpose | Replaces |
|---|---|---|
@if | Conditional | *ngIf |
@else if | Chained condition | nested *ngIf / <ng-template> |
@else | Fallback | *ngIf="x; else t" |
@for | Loop | *ngFor |
@empty | Empty-state | *ngIf="xs.length; else t" |
@switch | Multi-branch | [ngSwitch] |
@case | One branch | *ngSwitchCase |
@default | Fallback branch | *ngSwitchDefault |
@let | Local variable | (no equivalent) |
Key takeaways:
- The new block syntax is part of the template language โ no
CommonModuleimports @ifsupports@else ifand@elseโ cleaner than nested<ng-template>s@forrequires atrackexpression โ no more optionaltrackBy@forexposes$index,$count,$first,$last,$odd,$even@emptyhandles the empty-list case inline โ no template references@switchmatches with===and has no fallthrough โ nobreakto forget@letdeclares a local template variable โ read-only, scoped, useful for aliases and narrowing- The template type-checker narrows inside
@ifblocks โ 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!