Skip to content

Angular Structural Directives

Structural directives shape the DOM itself by adding, removing, or repeating elements based on data. This lesson covers the classic * directives and the modern built-in control flow syntax side by side.

What Are Structural Directives?

Structural directives change the structure of the DOM, unlike attribute directives, which only change an existing element's appearance or behavior. The most common examples are conditionally showing content and repeating content for each item in a list.

Classic structural directives are prefixed with an asterisk (*ngIf, *ngFor), which is syntactic sugar Angular expands into an <ng-template> behind the scenes. Since Angular 17, a built-in block syntax (@if, @for, @switch) provides the same capability more efficiently.

<!-- classic -->
<p *ngIf="user">Welcome, {{ user.name }}</p>

<!-- modern -->
@if (user) {
  <p>Welcome, {{ user.name }}</p>
}

Both render the paragraph only when user is truthy; the modern syntax needs no CommonModule import.

Structural Directive Syntax

*ngIf="condition"
*ngIf="condition; else elseBlock"
*ngFor="let item of items; trackBy: trackByFn"
*ngSwitch="value"

@if (condition) { ... } @else { ... }
@for (item of items; track item.id) { ... }
@switch (value) { @case (x) { ... } @default { ... } }
  • *ngIf can include an else template reference for the falsy branch.
  • *ngFor supports trackBy to optimize re-rendering when list items change identity.
  • @for requires a track expression, there is no optional/untracked form.
  • @switch/@case/@default replace *ngSwitch/*ngSwitchCase/*ngSwitchDefault.

Structural Directives Cheatsheet

Classic and modern syntax side by side for quick reference.

Purpose Classic Syntax Modern Syntax
Conditional *ngIf="cond" @if (cond) { ... }
Conditional + else *ngIf="cond; else t" @if (cond) { ... } @else { ... }
Loop *ngFor="let x of xs" @for (x of xs; track x.id) { ... }
Loop index *ngFor="let x of xs; let i = index" @for (x of xs; track x.id; let i = $index) { ... }
Switch *ngSwitch, *ngSwitchCase @switch, @case, @default
Empty list manual *ngIf="!xs.length" @empty { ... } block on @for

*ngIf With an Else Template

*ngIf can render an alternate template when the condition is falsy, avoiding the need for a second, separate conditional block.

<p *ngIf="user; else loading">{{ user.name }}</p>
<ng-template #loading>
  <p>Loading user...</p>
</ng-template>

<!-- modern equivalent -->
@if (user) {
  <p>{{ user.name }}</p>
} @else {
  <p>Loading user...</p>
}

Optimizing Loops With trackBy / track

By default, Angular tracks list items by reference identity. When a list is replaced with a new array of objects representing the same logical items (for example, after refetching from an API), Angular may destroy and recreate every DOM node unnecessarily. trackBy (classic) and track (modern, required) tell Angular how to recognize the "same" item across renders.

<!-- classic -->
<li *ngFor="let item of items; trackBy: trackById">{{ item.name }}</li>

trackById(index: number, item: Item) {
  return item.id;
}

<!-- modern: track is mandatory -->
@for (item of items; track item.id) {
  <li>{{ item.name }}</li>
}

Without proper tracking, list re-renders can lose focus state, animation state, or cause unnecessary performance cost.

Rendering One of Several Branches

<!-- classic -->
<div [ngSwitch]="status">
  <p *ngSwitchCase="'loading'">Loading...</p>
  <p *ngSwitchCase="'error'">Something went wrong.</p>
  <p *ngSwitchDefault>Ready.</p>
</div>

<!-- modern -->
@switch (status) {
  @case ('loading') { <p>Loading...</p> }
  @case ('error') { <p>Something went wrong.</p> }
  @default { <p>Ready.</p> }
}

Handling Empty Lists With @for's @empty

The modern @for block supports an @empty branch that renders automatically when the collection has zero items, removing the need for a separate *ngIf check.

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

Common Mistakes

  • Using @for without a track expression; unlike *ngFor's optional trackBy, track is mandatory in the modern syntax.
  • Tracking by array index when list items can be reordered, causing incorrect item identity across renders.
  • Nesting multiple *ngIf/*ngFor on the same element instead of using <ng-container> or the modern block syntax to avoid extra wrapper elements.
  • Forgetting CommonModule in a standalone component's imports when still using classic *ngIf/*ngFor.

Key Takeaways

  • Structural directives add, remove, or repeat DOM elements based on data.
  • *ngIf/*ngFor/*ngSwitch are the classic syntax; @if/@for/@switch are the modern, recommended replacement.
  • track/trackBy help Angular correctly identify list items across re-renders, improving performance and preserving UI state.
  • @for includes a built-in @empty branch for empty-collection UI.

Pro Tip

When migrating a codebase, the Angular CLI includes an automated schematic, ng generate @angular/core:control-flow, that rewrites *ngIf/*ngFor/*ngSwitch templates to the modern @if/@for/@switch syntax for you.