| |

Angular 8 ๐Ÿ…ฐ๏ธ Structural Directives โ€” ngIf, ngFor, ngSwitch

Structural directives change the shape of the DOM โ€” they add, remove, or replace elements based on data. The three built-in ones are *ngIf (conditionally include), *ngFor (repeat for each item), and *ngSwitch (pick one of several branches). They’re called “structural” because they don’t just style an element โ€” they decide whether it exists at all. This chapter covers the legacy directive syntax; the modern @if/@for/@switch block syntax is covered in the next chapter and is what you should use in new Angular code.

Key point: Structural directives are sugar for <ng-template>. The asterisk (*) tells Angular to wrap the host element in a template and let the directive decide when to stamp it out. Understanding that expansion is the key to understanding everything else โ€” *ngIf, *ngFor, *ngSwitch, and custom structural directives.


What “structural” means

A structural directive changes the DOM’s structure โ€” it adds, removes, or replaces elements. Contrast this with an attribute directive (next chapter) that only changes an existing element’s appearance or behavior.

*ngIf="condition"     โ†’ add or remove
*ngFor="let x of xs"  โ†’ repeat
*ngSwitch             โ†’ pick one

The asterisk prefix is the giveaway. Any directive used with * is structural โ€” it wraps the host element in a template and controls whether that template is rendered.

Without a structural directive, the element is always in the DOM. With one, the element may be absent entirely โ€” not hidden with CSS, not display: none, actually removed.

Why this matters: *ngIf removes elements from the DOM. That means their components are destroyed, their ngOnDestroy runs, their subscriptions are torn down. That’s different from hiding with [hidden] or [style.display]="'none'" โ€” which keeps the element alive but invisible. Choosing the right one has real consequences for performance and lifecycle.


The asterisk and <ng-template>

The * in *ngIf is shorthand. Angular rewrites it before compiling.

You write:

<div *ngIf="isVisible">Hello</div>

Angular expands it to:

<ng-template [ngIf]="isVisible">
  <div>Hello</div>
</ng-template>

<ng-template> is an inert element โ€” it renders nothing by itself. It’s a placeholder that holds content until something decides to stamp it out. The ngIf directive lives on the template, not on the <div>. When isVisible is true, the template is rendered; when false, it’s removed.

Why this matters:

  • The <div> isn’t a child of <ng-template> in the rendered DOM โ€” <ng-template> is a placeholder, not a wrapper
  • You can’t put *ngIf and *ngFor on the same element โ€” Angular can’t expand two asterisks into one <ng-template>. Use a wrapper <ng-template> or nest the elements
  • You can write <ng-template> explicitly when you need to (we’ll see this later)

Every structural directive works this way. *ngFor and *ngSwitch expand the same way.

Why <ng-template> exists: Angular needs a way to hold content that may or may not be rendered. <ng-template> is that container โ€” invisible, inert, waiting for a directive to decide.


*ngIf โ€” conditional rendering

*ngIf includes or excludes an element based on a boolean expression.

export class UserComponent {
  user: { name: string } | null = null;
  isLoggedIn = false;

  login(): void {
    this.user = { name: 'Alice' };
    this.isLoggedIn = true;
  }
}
<div *ngIf="isLoggedIn">
  Welcome back, {{ user?.name }}!
</div>

<button *ngIf="!isLoggedIn" (click)="login()">Log in</button>

When isLoggedIn is true, the welcome div renders and the button doesn’t. When it’s false, the reverse. Toggling isLoggedIn adds or removes the elements from the DOM.

Truthy/falsy: Any truthy value includes the element; any falsy value (false, 0, '', null, undefined, NaN) excludes it. *ngIf="items.length" works โ€” an empty array has length 0, so the element is hidden.

*ngIf with else:

<div *ngIf="user; else noUser">
  Hello, {{ user.name }}
</div>

<ng-template #noUser>
  <p>No user found.</p>
</ng-template>

The else clause points to a template reference variable (#noUser). When user is falsy, that template renders instead.

*ngIf with then:

<ng-template [ngIf]="user" [ngIfElse]="noUser">
  <!-- this becomes the "then" content -->
</ng-template>

<ng-template #noUser>
  <p>No user found.</p>
</ng-template>

The then clause is rarely used โ€” the standard *ngIf="cond; else tpl" form is clearer.

*ngIf as a variable:

<div *ngIf="user as u">
  Hello, {{ u.name }}
</div>

The as clause binds the truthy value to a template variable. Useful for narrowing User | null to User without repeated ?. โ€” inside the block, u is a User.

Why else uses a template reference: Angular needs a target for the “otherwise” content. A <ng-template #name> block is that target. It’s explicit, so there’s no ambiguity about what renders when.


*ngFor โ€” repeating elements

*ngFor renders a template once per item in a collection.

export class ListComponent {
  users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
    { id: 3, name: 'Carol' }
  ];
}
<ul>
  <li *ngFor="let user of users">
    {{ user.name }}
  </li>
</ul>

For each user, Angular stamps out an <li>. The result:

<ul>
  <li>Alice</li>
  <li>Bob</li>
  <li>Carol</li>
</ul>

Local variables inside the loop:

<li *ngFor="let user of users; let i = index; let first = first; let last = last; let odd = odd; let even = even">
  {{ i }}: {{ user.name }} {{ first ? '(first)' : '' }} {{ last ? '(last)' : '' }}
</li>
VariableMeaning
indexZero-based position
countTotal number of items
firsttrue on the first item
lasttrue on the last item
oddtrue on odd indices
eventrue on even indices

With trackBy:

<li *ngFor="let user of users; trackBy: trackById">
  {{ user.name }}
</li>
trackById(index: number, user: { id: number }): number {
  return user.id;
}

Without trackBy, Angular identifies items by object reference. When the array is replaced with new objects that have the same IDs, Angular destroys and recreates every DOM node. With trackBy, Angular matches by ID and only updates what changed.

When to use trackBy: Whenever your list can be refreshed with new object instances that represent the same entities. Fetching the same list twice from an API produces new objects โ€” without trackBy, every <li> is destroyed and rebuilt.

*ngFor without trackBy is fine for static lists that never change. For dynamic lists, always add it.

Why trackBy matters: Angular’s default identity check compares references. New objects fail the check even if they’re logically the same. trackBy gives Angular a stable key so it can reuse DOM nodes. That’s the difference between a smooth update and a full re-render.


*ngSwitch โ€” picking one branch

*ngSwitch renders exactly one of several templates based on a value.

export class StatusComponent {
  status: 'loading' | 'success' | 'error' = 'loading';
}
<div [ngSwitch]="status">
  <p *ngSwitchCase="'loading'">Loading...</p>
  <p *ngSwitchCase="'success'">Done!</p>
  <p *ngSwitchCase="'error'">Something went wrong.</p>
  <p *ngSwitchDefault>Unknown status.</p>
</div>

Notice the asymmetry: [ngSwitch] is a property binding (square brackets) on the container, while *ngSwitchCase and *ngSwitchDefault are structural (asterisks) on the branches.

Only one branch renders. *ngSwitchDefault catches anything unmatched.

*ngSwitchCase supports multiple values:

<p *ngSwitchCase="'error'">Error!</p>
<p *ngSwitchCase="'warning'">Warning!</p>

Each case has exactly one value. To match multiple, duplicate the element or use [ngSwitchCase] with a comma-separated expression via a method.

When to use *ngSwitch vs *ngIf:

  • *ngIf โ€” two branches (include or exclude)
  • *ngIf chains โ€” three or four mutually exclusive branches
  • *ngSwitch โ€” five or more mutually exclusive branches, or when a single value drives the choice

*ngSwitch is clearer when one value picks one of many. *ngIf/else chains become noisy beyond three options.

Why [ngSwitch] uses brackets: The container binding is a plain property binding to the ngSwitch directive’s ngSwitch input. The cases are structural because each conditionally includes its element. Different roles, different syntax.


Nesting and combining

You can nest structural directives freely.

<div *ngIf="users.length">
  <h2>Users</h2>
  <ul>
    <li *ngFor="let user of users">{{ user.name }}</li>
  </ul>
</div>

The outer *ngIf decides whether the list is shown. The inner *ngFor repeats for each user. Both work independently.

What you can’t do โ€” put two structural directives on the same element:

<div *ngIf="cond" *ngFor="let x of xs">...</div>  <!-- โŒ -->

Angular can’t expand two asterisks into one <ng-template>. To achieve this, wrap one in an <ng-template> or restructure:

<ng-template ngFor let-x [ngForOf]="xs">
  <div *ngIf="cond">{{ x }}</div>
</ng-template>

Or, more commonly:

<ng-container *ngIf="cond">
  <div *ngFor="let x of xs">{{ x }}</div>
</ng-container>

<ng-container> is a special element that renders nothing โ€” it exists only to group content. Use it when you need a structural directive without adding a wrapper element to the DOM.

Why <ng-container>: Sometimes you need to apply a structural directive but don’t want to add an extra <div> to the DOM. <ng-container> is invisible in the rendered output โ€” it just holds the directive.


A full example

A small user list that uses all three directives.

The class:

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

interface User {
  id: number;
  name: string;
  active: boolean;
}

@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './user-list.component.html',
  styleUrl: './user-list.component.css'
})
export class UserListComponent {
  loading = false;
  error: string | null = null;

  users: User[] = [
    { id: 1, name: 'Alice', active: true },
    { id: 2, name: 'Bob', active: false },
    { id: 3, name: 'Carol', active: true }
  ];

  trackById(_: number, user: User): number {
    return user.id;
  }
}

The template:

<div [ngSwitch]="true">
  <p *ngSwitchCase="loading">Loading...</p>
  <p *ngSwitchCase="!!error">Error: {{ error }}</p>
  <p *ngSwitchDefault>Loaded {{ users.length }} users.</p>
</div>

<ul *ngIf="users.length; else empty">
  <li *ngFor="let user of users; let i = index; trackBy: trackById"
      [class.inactive]="!user.active">
    {{ i + 1 }}. {{ user.name }}
    <span *ngIf="!user.active">(inactive)</span>
  </li>
</ul>

<ng-template #empty>
  <p>No users.</p>
</ng-template>

The styles:

.inactive {
  color: gray;
}

The status block uses *ngSwitch to show one of three states. The list uses *ngIf with an else template, and *ngFor with index and trackBy. Each inactive user gets an inline “(inactive)” span via a nested *ngIf.

Why this shape: Each directive does one job. *ngSwitch for multi-branch status, *ngIf for the list-or-empty choice, *ngFor for the list itself, and another *ngIf for the per-item marker. Nesting is fine โ€” combining on one element isn’t.


Complete Example Session

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

ng generate component user-list
# [ CREATE src/app/user-list/user-list.component.ts ]
# [ CREATE src/app/user-list/user-list.component.html ]
# [ CREATE src/app/user-list/user-list.component.css ]

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

cat > src/app/user-list/user-list.component.ts << 'EOF'
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';

interface User {
  id: number;
  name: string;
  active: boolean;
}

@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './user-list.component.html',
  styleUrl: './user-list.component.css'
})
export class UserListComponent {
  users: User[] = [
    { id: 1, name: 'Alice', active: true },
    { id: 2, name: 'Bob', active: false },
    { id: 3, name: 'Carol', active: true }
  ];

  trackById(_: number, user: User): number {
    return user.id;
  }
}
EOF

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

cat > src/app/user-list/user-list.component.html << 'EOF'
<h2>Users</h2>

<ul *ngIf="users.length; else empty">
  <li *ngFor="let user of users; let i = index; trackBy: trackById"
      [class.inactive]="!user.active">
    {{ i + 1 }}. {{ user.name }}
  </li>
</ul>

<ng-template #empty>
  <p>No users found.</p>
</ng-template>
EOF

# ============================================
# PART 4: ADD ngSwitch
# ============================================

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

@Component({
  selector: 'app-status',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div [ngSwitch]="status">
      <p *ngSwitchCase="'loading'">Loading...</p>
      <p *ngSwitchCase="'success'">Done!</p>
      <p *ngSwitchCase="'error'">Something went wrong.</p>
      <p *ngSwitchDefault>Unknown.</p>
    </div>
  `
})
export class StatusComponent {
  status: 'loading' | 'success' | 'error' | 'idle' = 'loading';
}
EOF

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

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

Quick Reference

Structural Directives

DirectivePurpose
*ngIfConditional include/exclude
*ngForRepeat for each item
*ngSwitchCaseOne branch of a switch
*ngSwitchDefaultDefault branch

*ngIf Forms

FormMeaning
*ngIf="cond"Include when truthy
*ngIf="cond; else tpl"Include, else fallback
*ngIf="val as v"Bind truthy value to v
*ngIf="cond; then a; else b"Long form

*ngFor Local Variables

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

*ngFor Syntax

FormMeaning
let x of xsIterate over xs
trackBy: fnStable identity for updates

*ngSwitch Syntax

FormRole
[ngSwitch]="value"Container binding
*ngSwitchCase="'x'"One case
*ngSwitchDefaultFallback

Comparison

DirectiveUse when
*ngIfTwo branches
*ngIf/elseTwo named branches
*ngSwitchMany mutually exclusive branches

Required Imports

DirectiveModule
*ngIfCommonModule
*ngForCommonModule
*ngSwitchCommonModule

Best Practices

โœ… Do This:

<!-- Use trackBy on dynamic lists -->
<li *ngFor="let u of users; trackBy: trackById">{{ u.name }}</li>   <!-- โœ… -->

<!-- Use the "as" clause to narrow types -->
<div *ngIf="user as u">Hello, {{ u.name }}</div>                    <!-- โœ… -->

<!-- Use else with a template reference -->
<div *ngIf="user; else noUser">...</div>                            <!-- โœ… -->

<!-- Use ng-container to group without extra DOM -->
<ng-container *ngIf="cond">...</ng-container>                       <!-- โœ… -->

<!-- Use ngSwitch for many exclusive branches -->
<div [ngSwitch]="status">...</div>                                  <!-- โœ… -->

<!-- Nest structural directives -->
<div *ngIf="list.length"><li *ngFor="let x of list">...</li></div>  <!-- โœ… -->

<!-- Import CommonModule where needed -->
imports: [CommonModule]                                             <!-- โœ… -->

โŒ Don’t Do This:

<!-- Don't put two structural directives on one element -->
<div *ngIf="c" *ngFor="let x of xs">...</div>                       <!-- โŒ -->

<!-- Don't use *ngIf for hiding โ€” use [hidden] if you want to keep it alive -->
<div *ngIf="hidden" [hidden]="true">...</div>                       <!-- โš ๏ธ pick one -->

<!-- Don't forget trackBy on dynamic lists -->
<li *ngFor="let u of users">{{ u.name }}</li>                       <!-- โš ๏ธ slow on refresh -->

<!-- Don't use ngSwitch with two branches -->
<div [ngSwitch]="x"><p *ngSwitchCase="'a'">A</p><p *ngSwitchDefault>B</p></div>  <!-- โš ๏ธ use ngIf -->

<!-- Don't use *ngIf="cond === true" -->
<div *ngIf="cond === true">...</div>                                <!-- โš ๏ธ just *ngIf="cond" -->

<!-- Don't use ngIf for CSS-only show/hide -->
<div *ngIf="visible" [style.display]="'none'">...</div>             <!-- โŒ contradictory -->

Common Pitfalls

PitfallProblemSolution
Two * on one elementAngular errorUse <ng-container>
No trackBy on dynamic listFull re-renderAdd trackBy
Using *ngIf to hideComponent destroyedUse [hidden] if you need it alive
Forgetting CommonModuleDirectives not foundImport it
*ngIf="value === true"Redundant comparisonJust *ngIf="value"
*ngSwitchCase without containerCase never matchesWrap in [ngSwitch]
Nested loops with same variableShadowingRename or use index
*ngFor over non-arrayRuntime errorEnsure iterable

Real-World Examples

1. Show a welcome block

<div *ngIf="isLoggedIn">Welcome back!</div>

2. Show a fallback

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

3. Use as to narrow

<div *ngIf="user as u">{{ u.name }} โ€” {{ u.email }}</div>

4. Basic list

<li *ngFor="let item of items">{{ item }}</li>

5. List with index

<li *ngFor="let item of items; let i = index">{{ i + 1 }}. {{ item }}</li>

6. List with trackBy

<li *ngFor="let user of users; trackBy: trackById">{{ user.name }}</li>

7. List with first/last markers

<li *ngFor="let x of xs; let first = first; let last = last">
  {{ x }}{{ first ? ' (first)' : '' }}{{ last ? ' (last)' : '' }}
</li>

8. Switch on a status

<div [ngSwitch]="status">
  <p *ngSwitchCase="'loading'">Loading...</p>
  <p *ngSwitchCase="'done'">Done!</p>
  <p *ngSwitchDefault>Unknown.</p>
</div>

9. Switch inside a loop

<div *ngFor="let item of items" [ngSwitch]="item.type">
  <span *ngSwitchCase="'text'">{{ item.value }}</span>
  <span *ngSwitchCase="'number'">{{ item.value | number }}</span>
  <span *ngSwitchDefault>?</span>
</div>

10. Group with ng-container

<ng-container *ngIf="items.length">
  <h2>Items</h2>
  <ul><li *ngFor="let i of items">{{ i }}</li></ul>
</ng-container>

11. Empty state

<ul *ngIf="users.length; else empty">
  <li *ngFor="let u of users">{{ u.name }}</li>
</ul>
<ng-template #empty><p>No users found.</p></ng-template>

12. Loading state

<p *ngIf="loading">Loading...</p>
<ul *ngIf="!loading && items.length">
  <li *ngFor="let item of items">{{ item }}</li>
</ul>

13. Per-item condition

<li *ngFor="let user of users">
  {{ user.name }}
  <span *ngIf="user.admin">(admin)</span>
</li>

14. Conditional wrapper

<ng-container *ngIf="editable; else viewMode">
  <input [(ngModel)]="value">
</ng-container>
<ng-template #viewMode>
  <p>{{ value }}</p>
</ng-template>

15. Nested loops

<div *ngFor="let group of groups">
  <h3>{{ group.name }}</h3>
  <ul>
    <li *ngFor="let member of group.members">{{ member }}</li>
  </ul>
</div>

Visual: The Asterisk Expansion

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  You write:                                  โ”‚
โ”‚                                              โ”‚
โ”‚  <div *ngIf="cond">Hello</div>               โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Angular expands to:                         โ”‚
โ”‚                                              โ”‚
โ”‚  <ng-template [ngIf]="cond">                 โ”‚
โ”‚    <div>Hello</div>                          โ”‚
โ”‚  </ng-template>                              โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  When cond is true:                          โ”‚
โ”‚                                              โ”‚
โ”‚  <div>Hello</div>                            โ”‚
โ”‚                                              โ”‚
โ”‚  When cond is false:                         โ”‚
โ”‚                                              โ”‚
โ”‚  (nothing โ€” the div is removed)              โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: *ngIf / else

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  *ngIf="user; else noUser"                   โ”‚
โ”‚                                              โ”‚
โ”‚  user truthy  โ”€โ”€โ–บ  render main template      โ”‚
โ”‚  user falsy   โ”€โ”€โ–บ  render #noUser template   โ”‚
โ”‚                                              โ”‚
โ”‚  Only one branch exists at a time            โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: *ngFor with trackBy

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Without trackBy                             โ”‚
โ”‚                                              โ”‚
โ”‚  Old array: [obj1, obj2, obj3]               โ”‚
โ”‚  New array: [obj1', obj2', obj3']            โ”‚
โ”‚                                              โ”‚
โ”‚  References differ โ†’ destroy + recreate all  โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  With trackBy: trackById                     โ”‚
โ”‚                                              โ”‚
โ”‚  Old: [id=1, id=2, id=3]                     โ”‚
โ”‚  New: [id=1, id=2, id=3]                     โ”‚
โ”‚                                              โ”‚
โ”‚  IDs match โ†’ reuse DOM nodes, patch only     โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: *ngSwitch

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  [ngSwitch]="status"                         โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ”œโ”€โ”€โ–บ *ngSwitchCase="'loading'"         โ”‚
โ”‚       โ”œโ”€โ”€โ–บ *ngSwitchCase="'success'"         โ”‚
โ”‚       โ”œโ”€โ”€โ–บ *ngSwitchCase="'error'"           โ”‚
โ”‚       โ””โ”€โ”€โ–บ *ngSwitchDefault                  โ”‚
โ”‚                                              โ”‚
โ”‚  Exactly one branch renders                  โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Structural vs Attribute Directive

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Structural (*)                              โ”‚
โ”‚                                              โ”‚
โ”‚  Adds / removes / replaces elements          โ”‚
โ”‚  Wraps in <ng-template>                      โ”‚
โ”‚  Example: *ngIf, *ngFor, *ngSwitchCase       โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Attribute (no *)                            โ”‚
โ”‚                                              โ”‚
โ”‚  Changes an existing element                 โ”‚
โ”‚  Example: [class.foo], [style.bar], ngClass  โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: <ng-container> vs <ng-template>

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  <ng-container>                              โ”‚
โ”‚                                              โ”‚
โ”‚  Renders nothing, but its children DO render โ”‚
โ”‚  Used to group elements and apply directives โ”‚
โ”‚                                              โ”‚
โ”‚  <ng-container *ngIf="cond">                 โ”‚
โ”‚    <div>A</div>                              โ”‚
โ”‚    <div>B</div>                              โ”‚
โ”‚  </ng-container>                             โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  <ng-template>                               โ”‚
โ”‚                                              โ”‚
โ”‚  Renders nothing by itself                   โ”‚
โ”‚  Its content only appears when stamped out   โ”‚
โ”‚                                              โ”‚
โ”‚  <ng-template #tpl>                          โ”‚
โ”‚    <div>Hidden by default</div>              โ”‚
โ”‚  </ng-template>                              โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ConceptMeaning
Structural directiveChanges DOM structure โ€” add, remove, replace
*Sugar for <ng-template>
*ngIfConditional include/exclude
*ngIf/elseTwo named branches
*ngForRepeat for each item
trackByStable identity for list updates
*ngSwitchCaseOne branch of a switch
*ngSwitchDefaultFallback branch
<ng-template>Inert container for conditional content
<ng-container>Invisible grouping element
CommonModuleModule that provides the built-ins

Key takeaways:

  • Structural directives change the shape of the DOM โ€” they add, remove, or replace elements
  • The asterisk is shorthand for wrapping the element in an <ng-template>
  • *ngIf includes or excludes based on a truthy/falsy value; else points to a <ng-template #ref>
  • as narrows the truthy value to a local template variable
  • *ngFor repeats for each item and exposes index, count, first, last, odd, even
  • trackBy is essential for dynamic lists โ€” it keeps DOM nodes alive across refreshes
  • *ngSwitch uses [ngSwitch] on a container and *ngSwitchCase/*ngSwitchDefault on the branches
  • Two structural directives can’t share an element โ€” wrap one in <ng-container> or restructure
  • <ng-container> renders nothing but holds directives
  • <ng-template> renders nothing by itself โ€” used as a target for else and then
  • All three built-ins come from CommonModule โ€” import it in standalone components

Remember: The asterisk is the tell โ€” any directive with * is structural. *ngIf picks, *ngFor repeats, *ngSwitch selects one of many. They’re sugar for <ng-template>, they add and remove real DOM nodes, and they can be nested but not stacked on the same element. Learn these three and you can shape any view. In new Angular code, prefer the block syntax from the next chapter โ€” but you’ll read *ngIf and *ngFor in existing codebases for years.


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!