Classic Structural Directives
Structural directives change the DOM layout by adding or removing elements. The classic ones are *ngIf, *ngFor, and *ngSwitch.
*ngIf — Conditional Rendering
@Component({
selector: 'app-user-profile',
template: `
<div *ngIf="isLoggedIn; else loggedOut">
<h2>Welcome, {{ user.name }}</h2>
<button (click)="logout()">Log out</button>
</div>
<ng-template #loggedOut>
<p>Please log in</p>
<button (click)="login()">Log in</button>
</ng-template>
`,
})
export class UserProfileComponent {
isLoggedIn = true;
user = { name: 'Alice' };
login(): void { this.isLoggedIn = true; }
logout(): void { this.isLoggedIn = false; }
}
*ngFor — List Rendering
@Component({
selector: 'app-todo-list',
template: `
<ul>
<li *ngFor="let todo of todos; let i = index; let first = first; let last = last; let odd = odd; let even = even"
[class.first]="first"
[class.last]="last"
[class.odd]="odd"
[class.even]="even">
{{ i + 1 }}. {{ todo.text }} <span *ngIf="todo.done">(done)</span>
</li>
</ul>
`,
})
export class TodoListComponent {
todos = [
{ text: 'Learn Angular', done: true },
{ text: 'Build app', done: false },
{ text: 'Deploy', done: false },
];
}
*ngFor with trackBy
@Component({
selector: 'app-user-list',
template: `
<div *ngFor="let user of users; trackBy: trackByUserId">
{{ user.name }}
</div>
`,
})
export class UserListComponent {
users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
trackByUserId(index: number, user: any): number {
return user.id;
}
}
*ngSwitch — Multiple Branches
@Component({
selector: 'app-status',
template: `
<div [ngSwitch]="status">
<p *ngSwitchCase="'loading'">Loading...</p>
<p *ngSwitchCase="'success'">Data loaded!</p>
<p *ngSwitchCase="'error'">Error occurred</p>
<p *ngSwitchDefault>Unknown status</p>
</div>
`,
})
export class StatusComponent {
status = 'loading';
}
New Control Flow: @if, @for, @switch
Angular 17+ introduces built-in control flow blocks that replace *ngIf, *ngFor, and *ngSwitch with cleaner syntax and better performance.
@if — Conditional Rendering
@Component({
selector: 'app-user-profile',
template: `
@if (isLoggedIn) {
<h2>Welcome, {{ user.name }}</h2>
<button (click)="logout()">Log out</button>
} @else {
<p>Please log in</p>
<button (click)="login()">Log in</button>
}
`,
})
export class UserProfileComponent {
isLoggedIn = true;
user = { name: 'Alice' };
login(): void { this.isLoggedIn = true; }
logout(): void { this.isLoggedIn = false; }
}
@for — List Rendering
@Component({
selector: 'app-todo-list',
template: `
@for (todo of todos; track todo.id; let i = $index, first = $first, last = $last, odd = $odd, even = $even) {
<li [class.first]="first" [class.last]="last" [class.odd]="odd" [class.even]="even">
{{ i + 1 }}. {{ todo.text }} @if (todo.done) { <span>(done)</span> }
</li>
} @empty {
<li>No todos yet</li>
}
`,
})
export class TodoListComponent {
todos = [
{ id: 1, text: 'Learn Angular', done: true },
{ id: 2, text: 'Build app', done: false },
];
}
@switch — Multiple Branches
@Component({
selector: 'app-status',
template: `
@switch (status) {
@case ('loading') { <p>Loading...</p> }
@case ('success') { <p>Data loaded!</p> }
@case ('error') { <p>Error occurred</p> }
@default { <p>Unknown status</p> }
}
`,
})
export class StatusComponent {
status = 'loading';
}
Old vs New Syntax
| Feature | Old (*ngIf) | New (@if) |
|---|---|---|
| Syntax | *ngIf="condition" | @if (condition) { } |
| Else | *ngIf="condition; else elseBlock" | @else { } |
| Else-if | Multiple *ngIf | @else if (condition) { } |
| Local variable | *ngIf="condition as var" | @if (condition; as var) { } |
| Performance | Good | Better (compiled to efficient JS) |
| Feature | Old (*ngFor) | New (@for) |
|---|---|---|
| Syntax | *ngFor="let item of items" | @for (item of items; track item.id) |
| Index | let i = $index | $index (built-in) |
| Empty | No built-in | @empty { } |
| TrackBy | trackBy: fn | track expression (required) |
Why the New Control Flow Is Better
- Required trackBy — @for enforces trackBy, preventing performance bugs
- @empty block — Built-in empty state handling
- Better type checking — Stricter template type inference
- No imports needed — Built into the compiler, no NgIf/NgFor imports
- Cleaner syntax — No asterisk prefix, more readable
Quiz
1. What is the new control flow syntax for conditional rendering in Angular 17+?
2. What is required in @for that was optional in *ngFor?
3. What is the @empty block used for?
Flashcards
Question
What are the new control flow blocks?
Click to reveal answer
Answer
@if, @for, @switch — built into Angular 17+ compiler, no imports needed.
Question
What is required in @for?
Click to reveal answer
Answer
track expression — e.g., @for (item of items; track item.id). Enforces performance best practices.
Question
What does @empty do?
Click to reveal answer
Answer
Displays content when the @for iterable has no items. Built-in empty state handling.
Revision Notes
Key Takeaways
- 1. @if, @for, @switch replace *ngIf, *ngFor, *ngSwitch
- 2. @for requires a track expression for performance
- 3. @empty provides built-in empty state handling
- 4. New control flow has no import requirements and better type checking
Interview Tips
- • Explain why the new control flow is better than the old syntax
- • Be ready to describe the performance benefits of required trackBy
- • Mention that old syntax still works but new syntax is preferred
Cheat Sheet
Structural Directives Cheat Sheet
New control flow (Angular 17+):
@if (condition) { } @else { }
@for (item of items; track item.id) {
{{ item.name }}
} @empty {
<p>No items</p>
}
@switch (status) {
@case ('loading') { }
@default { }
}
Old syntax (still works):
<div *ngIf="condition">
<li *ngFor="let item of items; trackBy: fn">
<div [ngSwitch]="status">