Angular Structural Directives
Introduction
Structural directives are one of Angular's most powerful features. Unlike attribute directives which only change the appearance or behavior of an existing element, structural directives actually shape or reshape the DOM's structure by adding, removing, or manipulating elements.
You can recognize structural directives in templates by their asterisk (*) prefix:
<div *ngIf="isVisible">This content is conditionally rendered</div>
In this comprehensive guide, you'll learn how Angular's built-in structural directives work and how to use them effectively in your applications. We'll also explore how to create your own custom structural directives.
Core Built-in Structural Directives
Angular comes with three primary structural directives that you'll use in almost every application:
*ngIf
: Conditionally includes a template based on an expression*ngFor
: Repeats a template for each item in an iterable*ngSwitch
: Switches between different templates based on expression
Let's explore each one in detail.
NgIf: Conditional Rendering
The *ngIf
directive adds or removes an element from the DOM based on a boolean expression.
Basic Usage
<div *ngIf="isLoggedIn">
Welcome back, User!
</div>
In the example above, the <div>
element and its contents only appear in the DOM when isLoggedIn
evaluates to true
.
Using Else Blocks
Angular also allows you to specify an alternative template to render when the condition is false:
<div *ngIf="userList.length > 0; else noUsers">
<h2>User List ({{userList.length}} users)</h2>
<!-- User list content -->
</div>
<ng-template #noUsers>
<p>No users found.</p>
</ng-template>
Here, if userList
has items, we show the user list. Otherwise, we display the "No users found" message.
If-Then-Else Pattern
You can even use a "then" template with *ngIf
:
<div *ngIf="dataLoaded; then dataView else loadingView"></div>
<ng-template #dataView>
<app-data-display [data]="data"></app-data-display>
</ng-template>
<ng-template #loadingView>
<app-loading-spinner></app-loading-spinner>
</ng-template>
This approach is useful for cleaner template organization, especially for complex conditional views.
NgFor: List Rendering
The *ngFor
directive repeats a portion of the DOM for each item in an iterable (like an array).
Basic Usage
<ul>
<li *ngFor="let item of items">{{ item.name }}</li>
</ul>
This will create a list item for each object in the items
array.
Additional Variables
*ngFor
provides several variables you can use within the template:
<div *ngFor="let item of items; index as i; first as isFirst; last as isLast; even as isEven; odd as isOdd">
<p>Item {{ i + 1 }}/{{ items.length }}: {{ item.name }}</p>
<span *ngIf="isFirst">This is the first item!</span>
<span *ngIf="isLast">This is the last item!</span>
<span [class.even-row]="isEven" [class.odd-row]="isOdd">{{ item.description }}</span>
</div>
In this example, we're using:
index
: Current item index (zero-based)first
: Boolean indicating if this is the first itemlast
: Boolean indicating if this is the last itemeven
: Boolean indicating if this is an even-indexed itemodd
: Boolean indicating if this is an odd-indexed item
TrackBy Function
For performance optimization, especially with large lists, you should use trackBy
with *ngFor
. This helps Angular identify which items have changed:
// In component class
trackByItems(index: number, item: any): number {
return item.id;
}
<div *ngFor="let item of items; trackBy: trackByItems">
{{ item.name }}
</div>
With trackBy
, Angular can reuse DOM elements when the data changes, instead of recreating the entire list.
NgSwitch: Multiple Conditional Views
The ngSwitch
directive is useful when you need to choose between multiple possible views:
<div [ngSwitch]="userRole">
<div *ngSwitchCase="'admin'">
<h2>Admin Dashboard</h2>
<app-admin-controls></app-admin-controls>
</div>
<div *ngSwitchCase="'editor'">
<h2>Editor Tools</h2>
<app-editor-tools></app-editor-tools>
</div>
<div *ngSwitchDefault>
<h2>User Profile</h2>
<app-user-profile></app-user-profile>
</div>
</div>
Here, different content is shown based on the userRole
value. Only one of the ngSwitchCase
templates will be rendered, or the ngSwitchDefault
if no cases match.
How Structural Directives Actually Work
Behind the scenes, Angular transforms the asterisk syntax into a more verbose form using <ng-template>
. For example, this:
<div *ngIf="showContent">Content to show</div>
Gets transformed into:
<ng-template [ngIf]="showContent">
<div>Content to show</div>
</ng-template>
This understanding is helpful when you need to create more complex templates or build custom structural directives.
Combining Structural Directives
You cannot apply multiple structural directives to the same element. This won't work:
<!-- This is WRONG! -->
<div *ngFor="let item of items" *ngIf="item.visible">
{{ item.name }}
</div>
Instead, you need to use a container element or ng-container
:
<ng-container *ngFor="let item of items">
<div *ngIf="item.visible">
{{ item.name }}
</div>
</ng-container>
The <ng-container>
is a special element that Angular recognizes but doesn't render in the DOM. It's perfect for grouping elements or applying multiple structural directives.
Real-World Example: Dynamic Form Creation
Let's look at a practical example where structural directives are extremely useful: dynamic form creation.
// in component.ts
export class DynamicFormComponent {
formFields = [
{ name: 'name', label: 'Name', type: 'text', required: true },
{ name: 'email', label: 'Email', type: 'email', required: true },
{ name: 'age', label: 'Age', type: 'number', required: false },
{ name: 'message', label: 'Message', type: 'textarea', required: false }
];
formData = {};
submitted = false;
onSubmit() {
this.submitted = true;
console.log('Form submitted:', this.formData);
}
}
<form (ngSubmit)="onSubmit()">
<ng-container *ngFor="let field of formFields">
<div class="form-group">
<label [for]="field.name">{{ field.label }}</label>
<ng-container [ngSwitch]="field.type">
<!-- Text input -->
<input *ngSwitchCase="'text'"
[id]="field.name"
[name]="field.name"
type="text"
[(ngModel)]="formData[field.name]"
[required]="field.required">
<!-- Email input -->
<input *ngSwitchCase="'email'"
[id]="field.name"
[name]="field.name"
type="email"
[(ngModel)]="formData[field.name]"
[required]="field.required">
<!-- Number input -->
<input *ngSwitchCase="'number'"
[id]="field.name"
[name]="field.name"
type="number"
[(ngModel)]="formData[field.name]"
[required]="field.required">
<!-- Textarea -->
<textarea *ngSwitchCase="'textarea'"
[id]="field.name"
[name]="field.name"
[(ngModel)]="formData[field.name]"
[required]="field.required"></textarea>
</ng-container>
<!-- Required field indicator -->
<span class="required-indicator" *ngIf="field.required">*</span>
</div>
</ng-container>
<button type="submit">Submit</button>
<!-- Confirmation message -->
<div class="confirmation" *ngIf="submitted">
Form submitted successfully!
</div>
</form>
In this example, we use:
*ngFor
to iterate over form fields*ngSwitch
to render different input types*ngIf
to show required field indicators and the confirmation message
This creates a dynamic form that adapts to the formFields
configuration array.
Creating a Custom Structural Directive
Let's create a simple custom structural directive that renders content only when the user has a specific role:
// has-role.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef, OnInit } from '@angular/core';
import { AuthService } from './auth.service'; // Your auth service
@Directive({
selector: '[appHasRole]'
})
export class HasRoleDirective implements OnInit {
private roles: string[];
private hasView = false;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private authService: AuthService
) { }
@Input() set appHasRole(roles: string[]) {
this.roles = roles;
this.updateView();
}
ngOnInit() {
// React to changes in the user's role
this.authService.userRole$.subscribe(() => {
this.updateView();
});
}
private updateView() {
const hasRole = this.checkRole();
if (hasRole && !this.hasView) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
} else if (!hasRole && this.hasView) {
this.viewContainer.clear();
this.hasView = false;
}
}
private checkRole(): boolean {
if (!this.roles || this.roles.length === 0) {
return true; // If no roles specified, show content
}
const userRole = this.authService.getCurrentUserRole();
return this.roles.includes(userRole);
}
}
Here's how you would use this directive:
<div *appHasRole="['admin', 'superuser']">
This content only appears for admins and superusers!
<button class="danger">Delete Everything</button>
</div>
<div *appHasRole="['editor']">
This content only appears for editors!
<app-editor-tools></app-editor-tools>
</div>
Summary
Structural directives are a powerful feature in Angular that allow you to dynamically manipulate the DOM structure based on conditions and data. The three built-in directives—*ngIf
, *ngFor
, and *ngSwitch
—form the foundation of dynamic templates in Angular applications.
Key takeaways:
- Structural directives are prefixed with an asterisk (*)
- They manipulate DOM elements by adding, removing, or replacing them
- You cannot apply multiple structural directives to the same element
- Use
ng-container
to apply multiple structural directives in sequence - You can create custom structural directives for specialized behaviors
By mastering structural directives, you gain the ability to create highly dynamic and responsive user interfaces that adapt to changing data and user interactions.
Additional Resources
- Official Angular Documentation on Structural Directives
- NgIf API Documentation
- NgFor API Documentation
- NgSwitch API Documentation
Exercises
- Create a simple to-do list application using
*ngFor
that allows adding and removing items. - Build a tabbed interface using
*ngIf
or*ngSwitch
to show different content based on the selected tab. - Create a custom structural directive that shows content only during certain times of day (e.g., "Morning", "Afternoon", "Evening").
- Implement a data table with sorting and filtering using structural directives.
- Create a multi-step form wizard where different form sections are shown based on the current step.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)