Before Angular 17, templates used directives with a star: *ngIf, *ngFor and [ngSwitch]. Millions of lines of code still use them, and many tutorials teach them. In this lesson you learn to read them, and how to move to the modern @if, @for and @switch blocks from the earlier lessons.
What you will learn
- The legacy syntax side by side with the new blocks
- What
trackBywas and what replaced it - How to migrate automatically
Angular marks NgIf and NgFor as deprecated since version 20, and recommends the new control flow. Use the old syntax only to maintain existing code.
ngIf
// src/app/legacy/legacy-if.ts
import { Component } from '@angular/core';
import { NgIf } from '@angular/common';
@Component({
selector: 'app-legacy-if',
imports: [NgIf],
template: `
<!-- Old -->
<p *ngIf="loggedIn; else guest">Welcome back!</p>
<ng-template #guest><p>Please sign in.</p></ng-template>
<!-- New -->
@if (loggedIn) {
<p>Welcome back!</p>
} @else {
<p>Please sign in.</p>
}
`,
})
export class LegacyIf {
protected loggedIn = false;
}
The star *ngIf is shorthand for wrapping the element in an <ng-template>. That is why else needed a separate named template. The @if block needs neither.
ngFor and trackBy
// src/app/legacy/legacy-for.ts
import { Component } from '@angular/core';
import { NgFor } from '@angular/common';
interface Item {
id: number;
name: string;
}
@Component({
selector: 'app-legacy-for',
imports: [NgFor],
template: `
<!-- Old -->
<ul>
<li *ngFor="let item of items; let i = index; trackBy: trackById">{{ i + 1 }}. {{ item.name }}</li>
</ul>
<!-- New -->
<ul>
@for (item of items; track item.id; let i = $index) {
<li>{{ i + 1 }}. {{ item.name }}</li>
} @empty {
<li>No items</li>
}
</ul>
`,
})
export class LegacyFor {
protected items: Item[] = [
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
];
// Old: a function that tells Angular how to identify an item
protected trackById(index: number, item: Item): number {
return item.id;
}
}
Why trackBy exists. When the list changes, Angular must decide which DOM elements to keep, move, add or remove. Without a key it may destroy and recreate every row, which is slow and loses things such as focus and typed text. trackBy returned a unique id for each item. In the new syntax, track item.id is required, so you cannot forget it. Use a stable id, not the array index, unless the list never reorders.
Old context variables map to new ones: index is $index, first is $first, last is $last, even is $even, odd is $odd, count is $count.
ngSwitch
// src/app/legacy/legacy-switch.ts
import { Component } from '@angular/core';
import { NgSwitch, NgSwitchCase, NgSwitchDefault } from '@angular/common';
@Component({
selector: 'app-legacy-switch',
imports: [NgSwitch, NgSwitchCase, NgSwitchDefault],
template: `
<!-- Old -->
<div [ngSwitch]="role">
<p *ngSwitchCase="'admin'">Admin panel</p>
<p *ngSwitchCase="'editor'">Editor tools</p>
<p *ngSwitchDefault>Read only</p>
</div>
<!-- New -->
@switch (role) {
@case ('admin') { <p>Admin panel</p> }
@case ('editor') { <p>Editor tools</p> }
@default { <p>Read only</p> }
}
`,
})
export class LegacySwitch {
protected role = 'editor';
}
Put the three on a page
// src/app/app.ts
import { Component } from '@angular/core';
import { LegacyIf } from './legacy/legacy-if';
import { LegacyFor } from './legacy/legacy-for';
import { LegacySwitch } from './legacy/legacy-switch';
@Component({
imports: [LegacyIf, LegacyFor, LegacySwitch],
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App {}
<!-- src/app/app.html -->
<main>
<app-legacy-if />
<app-legacy-for />
<app-legacy-switch />
</main>
The page shows both syntaxes together, so you can compare them directly.
Migrate automatically
Angular has a schematic that rewrites templates for you:
ng generate @angular/core:control-flow
It converts *ngIf, *ngFor and ngSwitch to the new blocks. Review the changes in Git, run your tests, and then remove the now unused NgIf, NgFor imports (the cleanup-unused-imports schematic helps). Run migrations on a clean Git branch.
Quick comparison
- **
*ngIf="x; else y"**: Modern:@if (x) { } @else { } - **
*ngFor="let i of list; trackBy: fn"**: Modern:@for (i of list; track i.id) { } - **
[ngSwitch]with*ngSwitchCase**: Modern:@switchwith@case - Import `NgIf`, `NgFor`: Modern: No imports needed
Try it yourself
- Convert an
*ngForwithlet odd = oddinto@forusing$odd. - Run the control-flow migration on a copy of your Task Manager project and read the diff.
- Find a list where the id changes between loads and think about what
trackshould be.
Quick check
- What does the
*in*ngIfstand for? - What did
trackBydo? - Which command migrates templates to the new control flow?
Next, continue with Angular ngClass, ngStyle, Class Binding and Style Binding Explained.