Structural Directives in Angular
Structural directives reshape the DOM by adding, removing, or manipulating elements. Unlike attribute directives that change the appearance of an existing element, structural directives decide whether an element exists in the DOM at all — or how many times it appears.
Angular's built-in structural directives are *ngIf, *ngFor, and *ngSwitch. Angular 17 also introduced new built-in control flow blocks (@if, @for, @switch) that replace them in modern code.
The Asterisk * Desugaring
The * prefix is syntactic sugar. Angular expands it into a <ng-template> with a binding on the directive. Understanding this helps you use structural directives in more advanced ways.
<!-- Short form (syntactic sugar) --> <p *ngIf="isVisible">Hello</p> <!-- Expanded form (what Angular really uses) --> <ng-template [ngIf]="isVisible"> <p>Hello</p> </ng-template>
*ngIf
*ngIf conditionally includes or removes an element from the DOM based on a truthy expression. The element is destroyed (not just hidden) when the condition is false.
import { Component } from '@angular/core';
import { NgIf } from '@angular/common';
@Component({
selector: 'app-ngif-demo',
standalone: true,
imports: [NgIf],
template: `
<!-- Basic -->
<p *ngIf="isLoggedIn">Welcome back, {{ username }}!</p>
<!-- With else -->
<div *ngIf="isLoading; else content">
<p>Loading...</p>
</div>
<ng-template #content>
<p>Data loaded!</p>
</ng-template>
<!-- With then/else -->
<ng-container *ngIf="status === 'active'; then activeBlock; else inactiveBlock" />
<ng-template #activeBlock><p class="green">Active</p></ng-template>
<ng-template #inactiveBlock><p class="red">Inactive</p></ng-template>
`,
})
export class NgIfDemoComponent {
isLoggedIn = true;
username = 'Alice';
isLoading = false;
status = 'active';
}
*ngFor
*ngFor repeats a template for each item in an iterable. It exposes several local variables you can use inside the loop.
import { Component } from '@angular/core';
import { NgFor } from '@angular/common';
interface Product {
id: number;
name: string;
price: number;
inStock: boolean;
}
@Component({
selector: 'app-product-list',
standalone: true,
imports: [NgFor],
template: `
<ul>
<li *ngFor="
let product of products;
let i = index;
let first = first;
let last = last;
let even = even;
let odd = odd;
trackBy: trackById
" [class.even-row]="even" [class.first-item]="first">
{{ i + 1 }}. {{ product.name }} — {{ product.price | currency }}
<span *ngIf="!product.inStock" class="out-of-stock">Out of stock</span>
<em *ngIf="last">(last item)</em>
</li>
</ul>
`,
})
export class ProductListComponent {
products: Product[] = [
{ id: 1, name: 'Laptop', price: 999, inStock: true },
{ id: 2, name: 'Monitor', price: 349, inStock: true },
{ id: 3, name: 'Webcam', price: 89, inStock: false },
{ id: 4, name: 'Headset', price: 149, inStock: true },
];
trackById(index: number, product: Product) {
return product.id;
}
}
*ngFor Local Variables Reference
Variable | Type | Value |
|---|---|---|
index | number | Current iteration index (0-based) |
count | number | Total number of items |
first | boolean | true for the first item |
last | boolean | true for the last item |
even | boolean | true for even-indexed items (0, 2, 4...) |
odd | boolean | true for odd-indexed items (1, 3, 5...) |
*ngSwitch
*ngSwitch is the structural equivalent of a switch statement — it renders one of several templates based on a matching expression.
import { Component } from '@angular/core';
import { NgSwitch, NgSwitchCase, NgSwitchDefault } from '@angular/common';
@Component({
selector: 'app-status-badge',
standalone: true,
imports: [NgSwitch, NgSwitchCase, NgSwitchDefault],
template: `
<div [ngSwitch]="orderStatus">
<span *ngSwitchCase="'pending'" class="badge yellow">Pending</span>
<span *ngSwitchCase="'shipped'" class="badge blue">Shipped</span>
<span *ngSwitchCase="'delivered'" class="badge green">Delivered</span>
<span *ngSwitchCase="'cancelled'" class="badge red">Cancelled</span>
<span *ngSwitchDefault class="badge grey">Unknown</span>
</div>
<select [(ngModel)]="orderStatus">
<option>pending</option>
<option>shipped</option>
<option>delivered</option>
<option>cancelled</option>
</select>
`,
})
export class StatusBadgeComponent {
orderStatus = 'pending';
}
ng-container — The Invisible Wrapper
<ng-container> is a grouping element that Angular removes from the DOM at render time. It has no HTML equivalent. Use it when you need to apply a structural directive without adding an extra DOM element.
<!-- Apply *ngIf without a wrapper element -->
<ng-container *ngIf="user">
<p>Name: {{ user.name }}</p>
<p>Email: {{ user.email }}</p>
</ng-container>
<!-- Apply *ngFor without a wrapping <div> -->
<table>
<ng-container *ngFor="let row of rows">
<tr><td>{{ row.col1 }}</td><td>{{ row.col2 }}</td></tr>
</ng-container>
</table>
<!-- Combine *ngIf and *ngFor on separate elements -->
<ng-container *ngFor="let item of items">
<div *ngIf="item.visible">{{ item.label }}</div>
</ng-container>
Creating a Custom Structural Directive
You can write your own structural directive. The key is injecting TemplateRef (the <ng-template> content) and ViewContainerRef (where to stamp it into the DOM).
// repeat.directive.ts
import {
Directive,
Input,
TemplateRef,
ViewContainerRef,
OnChanges,
} from '@angular/core';
@Directive({
selector: '[appRepeat]',
standalone: true,
})
export class RepeatDirective implements OnChanges {
@Input('appRepeat') times = 0;
constructor(
private templateRef: TemplateRef<{ $implicit: number }>,
private viewContainer: ViewContainerRef,
) {}
ngOnChanges() {
this.viewContainer.clear();
for (let i = 0; i < this.times; i++) {
this.viewContainer.createEmbeddedView(this.templateRef, { $implicit: i });
}
}
}
<!-- Renders "Item 0", "Item 1", "Item 2" -->
<p *appRepeat="3; let i">Item {{ i }}</p>
<!-- Renders 5 stars -->
<span *appRepeat="5">★</span>
Modern Alternative: Built-in Control Flow
Angular 17 introduced @if, @for, and @switch as native template syntax, replacing structural directives in new code. They require no imports and have better type narrowing.
<!-- @if replaces *ngIf -->
@if (isLoggedIn) {
<p>Welcome!</p>
} @else {
<p>Please log in.</p>
}
<!-- @for replaces *ngFor (track is required) -->
@for (item of items; track item.id) {
<li>{{ item.name }}</li>
} @empty {
<p>No items found.</p>
}
<!-- @switch replaces *ngSwitch -->
@switch (role) {
@case ('admin') { <app-admin-panel /> }
@case ('editor') { <app-editor-panel /> }
@default { <app-viewer-panel /> }
}
Summary
*ngIf conditionally adds or removes elements from the DOM.
*ngFor repeats a template for each item; always use trackBy to optimise re-renders.
*ngSwitch renders one of several templates based on a matching value.
The * prefix desugars to an <ng-template> — you can use the long form for advanced cases.
<ng-container> is an invisible wrapper; use it to avoid adding unwanted DOM elements.
Custom structural directives use TemplateRef + ViewContainerRef.
Angular 17+ @if / @for / @switch are the preferred replacement for structural directives.