Skip to main content

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:

html
<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

html
<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:

html
<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:

html
<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

html
<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:

html
<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 item
  • last: Boolean indicating if this is the last item
  • even: Boolean indicating if this is an even-indexed item
  • odd: 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:

typescript
// In component class
trackByItems(index: number, item: any): number {
return item.id;
}
html
<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:

html
<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:

html
<div *ngIf="showContent">Content to show</div>

Gets transformed into:

html
<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:

html
<!-- 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:

html
<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.

typescript
// 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);
}
}
html
<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:

typescript
// 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:

html
<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

Exercises

  1. Create a simple to-do list application using *ngFor that allows adding and removing items.
  2. Build a tabbed interface using *ngIf or *ngSwitch to show different content based on the selected tab.
  3. Create a custom structural directive that shows content only during certain times of day (e.g., "Morning", "Afternoon", "Evening").
  4. Implement a data table with sorting and filtering using structural directives.
  5. 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! :)