Skip to content

Angular Directives Explained — Structural and Attribute Directives Guide

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Angular Directives Explained. We cover key concepts, practical examples, and best practices to help you master this topic.

Angular directives are classes that add behavior to elements in your templates, either by manipulating the DOM structure or by changing the appearance and behavior of existing elements.

What You'll Learn

  • The difference between structural and attribute directives
  • How to use *ngIf, *ngFor, and *ngSwitch
  • How to use ngClass, ngStyle, and ngModel
  • How to build custom directives
  • When to use directives versus components

Why It Matters

Directives let you write less code by reusing DOM manipulation logic across your app. Instead of writing the same conditional rendering code in every component, you apply a directive. This keeps templates clean and logic centralized.

Real-World Use

Durga Antivirus Pro uses an appHighlight directive to color-code threat levels. Structural directives control whether the scan results panel, empty state, or loading spinner appears based on the current scan status.

flowchart TD
    A[Directives] --> B[Structural]
    A --> C[Attribute]
    B --> D[*ngIf]
    B --> E[*ngFor]
    B --> F[*ngSwitch]
    C --> G[ngClass]
    C --> H[ngStyle]
    C --> I[Custom Directives]
    style A fill:#f97316,color:#fff

Structural Directives

Structural directives change the DOM layout by adding, removing, or replacing elements. They start with an asterisk * as syntactic sugar.

*ngIf

*ngIf conditionally includes or excludes an element from the DOM:

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

@Component({
  selector: "app-login-status",
  standalone: true,
  imports: [CommonModule],
  template: `
    <div *ngIf="isLoggedIn; else loginPrompt">
      Welcome back, {{ username }}!
      <button (click)="logout()">Logout</button>
    </div>
    <ng-template #loginPrompt>
      <p>Please log in to continue.</p>
      <button (click)="login()">Login</button>
    </ng-template>
  `
})
export class LoginStatusComponent {
  isLoggedIn = false;
  username = "guest";

  login() { this.isLoggedIn = true; }
  logout() { this.isLoggedIn = false; }
}

Expected output: When isLoggedIn is false, the "Please log in" message appears. After clicking Login, the welcome message replaces it.

The else syntax references an ng-template element by its template reference variable. When the condition is false, Angular removes the element with *ngIf and renders the ng-template instead.

*ngFor

*ngFor iterates over a collection and renders a template for each item:

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

@Component({
  selector: "app-task-list",
  standalone: true,
  imports: [CommonModule],
  template: `
    <ul>
      <li *ngFor="let task of tasks; let i = index; trackBy: trackById">
        <span>{{ i + 1 }}.</span>
        <span [style.textDecoration]="task.done ? 'line-through' : 'none'">
          {{ task.title }}
        </span>
        <button (click)="toggleTask(i)">{{ task.done ? "Undo" : "Done" }}</button>
      </li>
    </ul>
  `
})
export class TaskListComponent {
  tasks = [
    { id: 1, title: "Learn Angular", done: false },
    { id: 2, title: "Build an app", done: false },
    { id: 3, title: "Deploy to production", done: false },
  ];

  toggleTask(index: number) {
    this.tasks[index].done = !this.tasks[index].done;
  }

  trackById(_index: number, task: { id: number }) {
    return task.id;
  }
}

Expected output: A numbered task list with toggle buttons. Completed tasks show with strikethrough text.

The trackBy function helps Angular identify which items changed. Without it, Angular re-renders the entire list whenever the array reference changes. With trackBy, it only updates the changed items.

*ngSwitch

*ngSwitch displays one template from a set of choices:

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

@Component({
  selector: "app-status-message",
  standalone: true,
  imports: [CommonModule],
  template: `
    <div [ngSwitch]="status">
      <p *ngSwitchCase="'loading'">Fetching data, please wait...</p>
      <p *ngSwitchCase="'success'">Data loaded successfully!</p>
      <p *ngSwitchCase="'error'">An error occurred. Please try again.</p>
      <p *ngSwitchDefault>Unknown status.</p>
    </div>
    <button (click)="cycleStatus()">Next Status</button>
  `
})
export class StatusMessageComponent {
  status: "loading" | "success" | "error" = "loading";
  private states = ["loading", "success", "error"] as const;
  private index = 0;

  cycleStatus() {
    this.index = (this.index + 1) % this.states.length;
    this.status = this.states[this.index];
  }
}

Expected output: A message that cycles through loading, success, and error states with each button click.

ngSwitchCase defines each possible value. ngSwitchDefault is the fallback when no case matches. Angular removes all elements that do not match the current value.

Attribute Directives

Attribute directives change the appearance or behavior of an element without altering its structure.

ngClass

ngClass dynamically adds or removes CSS classes:

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

@Component({
  selector: "app-priority-label",
  standalone: true,
  imports: [CommonModule],
  template: `
    <span [ngClass]="{
      'priority-high': priority === 'high',
      'priority-medium': priority === 'medium',
      'priority-low': priority === 'low'
    }">
      {{ priority | uppercase }}
    </span>
    <button (click)="cyclePriority()">Change Priority</button>
  `,
  styles: [`
    .priority-high { color: white; background: #ef4444; padding: 4px 8px; border-radius: 4px; }
    .priority-medium { color: white; background: #f97316; padding: 4px 8px; border-radius: 4px; }
    .priority-low { color: white; background: #22c55e; padding: 4px 8px; border-radius: 4px; }
  `]
})
export class PriorityLabelComponent {
  priority: "high" | "medium" | "low" = "medium";
  private priorities = ["high", "medium", "low"] as const;
  private index = 1;

  cyclePriority() {
    this.index = (this.index + 1) % this.priorities.length;
    this.priority = this.priorities[this.index];
  }
}

Expected output: A colored priority label that cycles through red (high), orange (medium), and green (low) on button click.

The object passed to ngClass maps class names to boolean conditions. When a condition is true, Angular adds that class to the element. When false, it removes it.

ngStyle

ngStyle sets inline styles dynamically:

@Component({
  selector: "app-color-picker",
  standalone: true,
  template: `
    <div [ngStyle]="{
      'background-color': bgColor,
      'color': textColor,
      'font-size.px': fontSize,
      'padding.px': 16,
      'border-radius.px': 8
    }">
      Styled dynamically with ngStyle
    </div>
    <label>Background: <input [(ngModel)]="bgColor" /></label>
  `,
  imports: [FormsModule]
})
export class ColorPickerComponent {
  bgColor = "#f97316";
  textColor = "white";
  fontSize = 18;
}

Expected output: A colored box whose background, text color, and font size change based on input values.

ngStyle accepts an object where keys are CSS property names and values are the property values. Suffix keys like font-size.px specify units.

Custom Directives

You can create your own directives using the @Directive decorator:

import { Directive, ElementRef, HostListener, Input } from "@angular/core";

@Directive({
  selector: "[appHighlight]",
  standalone: true
})
export class HighlightDirective {
  @Input() appHighlight = "";
  @Input() highlightDuration = 2000;

  constructor(private el: ElementRef) {}

  @HostListener("mouseenter") onMouseEnter() {
    this.el.nativeElement.style.backgroundColor = this.appHighlight || "yellow";
  }

  @HostListener("mouseleave") onMouseLeave() {
    this.el.nativeElement.style.backgroundColor = "transparent";
  }
}

Usage:

<p [appHighlight]="'lightblue'" highlightDuration="1000">Hover over me!</p>

Expected output: When you hover over the paragraph, its background changes to light blue. When you leave, it returns to transparent.

The @Directive selector uses brackets to indicate an attribute selector. @HostListener registers event handlers on the host element. ElementRef gives access to the native DOM element.

Common Mistakes

  1. Using multiple structural directives on one element — Angular does not allow *ngIf and *ngFor on the same element. Wrap one in an <ng-container> instead.

  2. Forgetting the asterisk prefix — Writing ngIf="condition" instead of *ngIf="condition" silently fails because Angular expects the structural directive syntax.

  3. Mutating arrays in ngFor — Pushing to or splicing an array does not trigger change detection if the array reference stays the same. Reassign the array or use immutability.

  4. Using ngClass with conflicting classes — If two conditions in the ngClass object are true for the same class, the later one in the object wins, which can be confusing.

  5. Not cleaning up in custom directives — If your directive subscribes to events or observables, unsubscribe in ngOnDestroy to prevent memory leaks.

Practice Questions

  1. What is the difference between structural and attribute directives? Structural directives change the DOM layout (add/remove elements). Attribute directives change appearance or behavior of existing elements.

  2. Why does Angular not allow multiple structural directives on one element? Angular's microsyntax would create ambiguous precedence. Use <ng-container> to wrap one directive.

  3. What does trackBy do in *ngFor? It provides a unique identifier for each item so Angular can track changes efficiently without re-rendering the entire list.

  4. How do you create a custom attribute directive? Use the @Directive decorator with a selector, inject ElementRef, and use @HostListener for events.

  5. What is the ngSwitchDefault for? It renders when no ngSwitchCase matches the switch expression.

Challenge

Create a custom appTooltip directive that shows a tooltip on hover. Use @Input() for the tooltip text and position (top, bottom, left, right). Style it with absolute positioning relative to the host element.

FAQ

Can I pass inputs to structural directives?

Yes, structural directives can have inputs. Use the microsyntax: *ngFor="let item of items; trackBy: trackFn".

What is used for?

It is a grouping element that does not render in the DOM. Use it to apply structural directives without adding extra tags.

Can I use ngClass with string or array syntax?

Yes, [ngClass]="'class1 class2'" or [ngClass]="['class1', 'class2']" are both valid.

What is the `as` keyword in ngFor?

*ngFor="let item of items; let i = index" assigns the index to a local variable.

How do I create a structural directive?

Implement the ngTemplateOutlet pattern by injecting TemplateRef and ViewContainerRef and using createEmbeddedView.

Mini Project

Build a PermissionGuardDirective that hides elements based on user roles. Create a custom structural directive *appHasRole that takes a role string and removes the element from the DOM if the current user does not have that role. Combine it with a login component that cycles through admin, editor, and viewer roles.

What's Next

Continue learning about data transformation and services:

Angular Pipes, Angular Services, Angular Components

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro