Skip to content

Angular Directives

Directives are a fundamental feature of Angular that allow you to extend HTML with custom behavior. They are classes that add additional behavior to elements in your Angular applications. Angular provides several built-in directives and also allows you to create custom directives to encapsulate reusable DOM manipulation logic.

Understanding Directives

Angular directives are instructions in the DOM that tell Angular how to transform the DOM. There are three types of directives:

  1. Components: Directives with templates (covered in previous sections)
  2. Structural Directives: Change the DOM layout by adding/removing elements
  3. Attribute Directives: Change the appearance or behavior of elements

Directive Decorator

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

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
  constructor(private el: ElementRef) {}
}

Structural Directives

Structural directives are responsible for HTML layout. They shape or reshape the DOM's structure by adding, removing, or manipulating elements.

Key Characteristics

  • Prefixed with an asterisk (*) in templates
  • Can add or remove elements from the DOM
  • Only one structural directive per element

Microsyntax

Structural directives use a special microsyntax for their expressions:

<!-- Full form -->
<ng-template [ngIf]="condition">
  <div>Content</div>
</ng-template>

<!-- Shorthand with asterisk -->
<div *ngIf="condition">Content</div>

Built-in Structural Directives

*ngIf - Conditional Rendering

// component.ts
@Component({
  selector: 'app-conditional',
  template: `
    <div class="conditional-demo">
      <button (click)="toggleShow()">Toggle Content</button>
      <button (click)="toggleUser()">Toggle User</button>

      <!-- Basic ngIf -->
      <div *ngIf="showContent" class="content">
        <h3>This content is conditionally shown</h3>
      </div>

      <!-- ngIf with else -->
      <div *ngIf="user; else noUser">
        <h3>Welcome, {{ user.name }}!</h3>
        <p>Email: {{ user.email }}</p>
      </div>
      <ng-template #noUser>
        <p>Please log in to see user information.</p>
      </ng-template>

      <!-- ngIf with then and else -->
      <div *ngIf="isLoading; then loading; else loaded"></div>
      <ng-template #loading>
        <div class="loading">Loading...</div>
      </ng-template>
      <ng-template #loaded>
        <div class="loaded">Content has loaded!</div>
      </ng-template>

      <!-- ngIf with as (storing result) -->
      <div *ngIf="getUser() as currentUser">
        <h3>Current User: {{ currentUser.name }}</h3>
      </div>
    </div>
  `
})
export class ConditionalComponent {
  showContent = false;
  isLoading = false;
  user: { name: string; email: string } | null = null;

  toggleShow(): void {
    this.showContent = !this.showContent;
  }

  toggleUser(): void {
    this.user = this.user ? null : { name: 'John Doe', email: 'john@example.com' };
  }

  getUser(): { name: string; email: string } | null {
    return this.user;
  }
}

*ngFor - List Rendering

@Component({
  selector: 'app-list',
  template: `
    <div class="list-demo">
      <!-- Basic ngFor -->
      <ul>
        <li *ngFor="let item of items">{{ item }}</li>
      </ul>

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

      <!-- ngFor with trackBy for performance -->
      <div class="user-list">
        <div *ngFor="let user of users; trackBy: trackByUserId" class="user-card">
          <h4>{{ user.name }}</h4>
          <p>{{ user.email }}</p>
          <button (click)="updateUser(user.id)">Update</button>
        </div>
      </div>

      <!-- ngFor with multiple values -->
      <div *ngFor="let item of items; let i = index; let first = first; let last = last; let even = even; let odd = odd"
           [class.first]="first"
           [class.last]="last"
           [class.even]="even"
           [class.odd]="odd">
        {{ i }}: {{ item }}
      </div>

      <!-- ngFor with object properties -->
      <div *ngFor="let entry of objectEntries">
        <strong>{{ entry.key }}:</strong> {{ entry.value }}
      </div>
    </div>
  `
})
export class ListComponent {
  items = ['Apple', 'Banana', 'Cherry', 'Date'];

  users = [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
    { id: 2, name: 'Bob', email: 'bob@example.com' },
    { id: 3, name: 'Charlie', email: 'charlie@example.com' }
  ];

  userObject = {
    name: 'John Doe',
    age: 30,
    city: 'New York',
    occupation: 'Developer'
  };

  get objectEntries() {
    return Object.entries(this.userObject).map(([key, value]) => ({ key, value }));
  }

  trackByUserId(index: number, user: any): number {
    return user.id;
  }

  updateUser(userId: number): void {
    const user = this.users.find(u => u.id === userId);
    if (user) {
      user.name = `${user.name} (Updated)`;
    }
  }
}

*ngSwitch - Multiple Condition Rendering

@Component({
  selector: 'app-switch',
  template: `
    <div class="switch-demo">
      <select [(ngModel)]="selectedView">
        <option value="list">List View</option>
        <option value="grid">Grid View</option>
        <option value="table">Table View</option>
        <option value="chart">Chart View</option>
      </select>

      <div [ngSwitch]="selectedView" class="view-container">
        <div *ngSwitchCase="'list'" class="list-view">
          <h3>List View</h3>
          <ul>
            <li *ngFor="let item of data">{{ item.name }}</li>
          </ul>
        </div>

        <div *ngSwitchCase="'grid'" class="grid-view">
          <h3>Grid View</h3>
          <div class="grid">
            <div *ngFor="let item of data" class="grid-item">
              {{ item.name }}
            </div>
          </div>
        </div>

        <div *ngSwitchCase="'table'" class="table-view">
          <h3>Table View</h3>
          <table>
            <thead>
              <tr>
                <th>Name</th>
                <th>Value</th>
              </tr>
            </thead>
            <tbody>
              <tr *ngFor="let item of data">
                <td>{{ item.name }}</td>
                <td>{{ item.value }}</td>
              </tr>
            </tbody>
          </table>
        </div>

        <div *ngSwitchDefault class="default-view">
          <h3>Chart View</h3>
          <p>Chart visualization would go here</p>
        </div>
      </div>
    </div>
  `
})
export class SwitchComponent {
  selectedView = 'list';
  data = [
    { name: 'Item 1', value: 100 },
    { name: 'Item 2', value: 200 },
    { name: 'Item 3', value: 300 }
  ];
}

Built-in Attribute Directives

ngClass - Dynamic CSS Classes

@Component({
  selector: 'app-styling',
  template: `
    <div class="styling-demo">
      <!-- Object syntax -->
      <div [ngClass]="{
        'highlight': isHighlighted,
        'error': hasError,
        'disabled': isDisabled
      }">
        Object syntax example
      </div>

      <!-- Array syntax -->
      <div [ngClass]="['base-class', dynamicClass, conditionalClass]">
        Array syntax example
      </div>

      <!-- String syntax -->
      <div [ngClass]="classString">
        String syntax example
      </div>

      <!-- Method binding -->
      <div [ngClass]="getClasses()">
        Method binding example
      </div>

      <!-- Multiple conditions -->
      <div [ngClass]="{
        'status-active': user?.status === 'active',
        'status-inactive': user?.status === 'inactive',
        'premium-user': user?.isPremium,
        'new-user': isNewUser()
      }">
        User status indicator
      </div>

      <button (click)="toggleHighlight()">Toggle Highlight</button>
      <button (click)="toggleError()">Toggle Error</button>
      <button (click)="changeTheme()">Change Theme</button>
    </div>
  `,
  styles: [`
    .highlight { background-color: yellow; }
    .error { color: red; border: 1px solid red; }
    .disabled { opacity: 0.5; pointer-events: none; }
    .theme-dark { background-color: #333; color: white; }
    .theme-light { background-color: white; color: black; }
    .status-active { border-left: 4px solid green; }
    .status-inactive { border-left: 4px solid red; }
    .premium-user { background-color: gold; }
    .new-user { border: 2px dashed blue; }
  `]
})
export class StylingComponent {
  isHighlighted = false;
  hasError = false;
  isDisabled = false;
  currentTheme = 'light';

  user = {
    status: 'active',
    isPremium: true,
    joinDate: new Date()
  };

  get dynamicClass(): string {
    return this.currentTheme === 'dark' ? 'theme-dark' : 'theme-light';
  }

  get conditionalClass(): string {
    return this.hasError ? 'error' : '';
  }

  get classString(): string {
    return `base-class ${this.dynamicClass} ${this.conditionalClass}`;
  }

  getClasses(): object {
    return {
      'highlight': this.isHighlighted,
      'error': this.hasError,
      [`theme-${this.currentTheme}`]: true
    };
  }

  isNewUser(): boolean {
    const daysSinceJoin = (Date.now() - this.user.joinDate.getTime()) / (1000 * 60 * 60 * 24);
    return daysSinceJoin < 30;
  }

  toggleHighlight(): void {
    this.isHighlighted = !this.isHighlighted;
  }

  toggleError(): void {
    this.hasError = !this.hasError;
  }

  changeTheme(): void {
    this.currentTheme = this.currentTheme === 'light' ? 'dark' : 'light';
  }
}

ngStyle - Dynamic Inline Styles

@Component({
  selector: 'app-dynamic-styling',
  template: `
    <div class="dynamic-styling-demo">
      <!-- Object syntax -->
      <div [ngStyle]="{
        'color': textColor,
        'font-size': fontSize + 'px',
        'background-color': backgroundColor,
        'padding': padding + 'px',
        'border-radius': borderRadius + 'px'
      }">
        Dynamic styling with object syntax
      </div>

      <!-- Method binding -->
      <div [ngStyle]="getStyles()">
        Dynamic styling with method
      </div>

      <!-- Conditional styles -->
      <div [ngStyle]="{
        'transform': isRotated ? 'rotate(45deg)' : 'rotate(0deg)',
        'transition': 'transform 0.3s ease',
        'opacity': isVisible ? 1 : 0.3
      }">
        Conditional transformations
      </div>

      <!-- Progress bar example -->
      <div class="progress-container">
        <div
          class="progress-bar"
          [ngStyle]="{
            'width': progress + '%',
            'background-color': getProgressColor()
          }">
        </div>
      </div>

      <div class="controls">
        <label>
          Text Color:
          <input type="color" [(ngModel)]="textColor">
        </label>
        <label>
          Font Size:
          <input type="range" min="12" max="48" [(ngModel)]="fontSize">
        </label>
        <label>
          Background:
          <input type="color" [(ngModel)]="backgroundColor">
        </label>
        <label>
          Progress:
          <input type="range" min="0" max="100" [(ngModel)]="progress">
        </label>
        <button (click)="toggleRotation()">Toggle Rotation</button>
        <button (click)="toggleVisibility()">Toggle Visibility</button>
      </div>
    </div>
  `,
  styles: [`
    .progress-container {
      width: 100%;
      height: 20px;
      background-color: #f0f0f0;
      border-radius: 10px;
      margin: 10px 0;
    }
    .progress-bar {
      height: 100%;
      border-radius: 10px;
      transition: width 0.3s ease;
    }
    .controls {
      display: flex;
      flex-direction: column;
      gap: 10px;
      margin-top: 20px;
    }
    .controls label {
      display: flex;
      align-items: center;
      gap: 10px;
    }
  `]
})
export class DynamicStylingComponent {
  textColor = '#000000';
  fontSize = 16;
  backgroundColor = '#ffffff';
  padding = 10;
  borderRadius = 5;
  progress = 50;
  isRotated = false;
  isVisible = true;

  getStyles(): object {
    return {
      'color': this.textColor,
      'font-size': this.fontSize + 'px',
      'background-color': this.backgroundColor,
      'padding': this.padding + 'px',
      'border-radius': this.borderRadius + 'px',
      'border': '2px solid ' + this.textColor
    };
  }

  getProgressColor(): string {
    if (this.progress < 30) return '#ff4444';
    if (this.progress < 70) return '#ffaa00';
    return '#44ff44';
  }

  toggleRotation(): void {
    this.isRotated = !this.isRotated;
  }

  toggleVisibility(): void {
    this.isVisible = !this.isVisible;
  }
}

Creating Custom Attribute Directives

Basic Custom Directive

// highlight.directive.ts
import { Directive, ElementRef, Input, OnInit } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective implements OnInit {
  @Input() appHighlight = 'yellow';
  @Input() defaultColor = 'transparent';

  constructor(private el: ElementRef) {}

  ngOnInit(): void {
    this.el.nativeElement.style.backgroundColor = this.appHighlight || this.defaultColor;
  }
}

Interactive Custom Directive

// hover-highlight.directive.ts
import {
  Directive,
  ElementRef,
  HostListener,
  Input,
  Renderer2,
  OnInit
} from '@angular/core';

@Directive({
  selector: '[appHoverHighlight]'
})
export class HoverHighlightDirective implements OnInit {
  @Input() highlightColor = 'yellow';
  @Input() defaultColor = 'transparent';
  @Input() animationDuration = '300ms';

  constructor(
    private el: ElementRef,
    private renderer: Renderer2
  ) {}

  ngOnInit(): void {
    this.renderer.setStyle(
      this.el.nativeElement,
      'transition',
      `background-color ${this.animationDuration} ease`
    );
    this.setBackgroundColor(this.defaultColor);
  }

  @HostListener('mouseenter') onMouseEnter(): void {
    this.setBackgroundColor(this.highlightColor);
  }

  @HostListener('mouseleave') onMouseLeave(): void {
    this.setBackgroundColor(this.defaultColor);
  }

  private setBackgroundColor(color: string): void {
    this.renderer.setStyle(this.el.nativeElement, 'background-color', color);
  }
}

Advanced Custom Directive with Multiple Inputs

// tooltip.directive.ts
import {
  Directive,
  ElementRef,
  HostListener,
  Input,
  Renderer2,
  ViewContainerRef,
  ComponentRef,
  OnDestroy
} from '@angular/core';

@Directive({
  selector: '[appTooltip]'
})
export class TooltipDirective implements OnDestroy {
  @Input() appTooltip = '';
  @Input() tooltipPosition: 'top' | 'bottom' | 'left' | 'right' = 'top';
  @Input() tooltipDelay = 500;
  @Input() tooltipClass = '';

  private tooltipElement?: HTMLElement;
  private showTimeout?: number;
  private hideTimeout?: number;

  constructor(
    private el: ElementRef,
    private renderer: Renderer2
  ) {}

  @HostListener('mouseenter') onMouseEnter(): void {
    this.clearTimeouts();
    this.showTimeout = window.setTimeout(() => {
      this.showTooltip();
    }, this.tooltipDelay);
  }

  @HostListener('mouseleave') onMouseLeave(): void {
    this.clearTimeouts();
    this.hideTimeout = window.setTimeout(() => {
      this.hideTooltip();
    }, 100);
  }

  private showTooltip(): void {
    if (this.tooltipElement || !this.appTooltip) return;

    this.tooltipElement = this.renderer.createElement('div');
    this.renderer.appendChild(this.tooltipElement,
      this.renderer.createText(this.appTooltip));

    // Add tooltip classes
    this.renderer.addClass(this.tooltipElement, 'tooltip');
    this.renderer.addClass(this.tooltipElement, `tooltip-${this.tooltipPosition}`);

    if (this.tooltipClass) {
      this.renderer.addClass(this.tooltipElement, this.tooltipClass);
    }

    // Position tooltip
    this.renderer.setStyle(this.tooltipElement, 'position', 'absolute');
    this.renderer.setStyle(this.tooltipElement, 'z-index', '1000');
    this.renderer.setStyle(this.tooltipElement, 'background-color', '#333');
    this.renderer.setStyle(this.tooltipElement, 'color', 'white');
    this.renderer.setStyle(this.tooltipElement, 'padding', '8px 12px');
    this.renderer.setStyle(this.tooltipElement, 'border-radius', '4px');
    this.renderer.setStyle(this.tooltipElement, 'font-size', '14px');
    this.renderer.setStyle(this.tooltipElement, 'white-space', 'nowrap');

    this.renderer.appendChild(document.body, this.tooltipElement);
    this.positionTooltip();
  }

  private positionTooltip(): void {
    if (!this.tooltipElement) return;

    const hostRect = this.el.nativeElement.getBoundingClientRect();
    const tooltipRect = this.tooltipElement.getBoundingClientRect();

    let top: number, left: number;

    switch (this.tooltipPosition) {
      case 'top':
        top = hostRect.top - tooltipRect.height - 8;
        left = hostRect.left + (hostRect.width - tooltipRect.width) / 2;
        break;
      case 'bottom':
        top = hostRect.bottom + 8;
        left = hostRect.left + (hostRect.width - tooltipRect.width) / 2;
        break;
      case 'left':
        top = hostRect.top + (hostRect.height - tooltipRect.height) / 2;
        left = hostRect.left - tooltipRect.width - 8;
        break;
      case 'right':
        top = hostRect.top + (hostRect.height - tooltipRect.height) / 2;
        left = hostRect.right + 8;
        break;
    }

    this.renderer.setStyle(this.tooltipElement, 'top', `${top}px`);
    this.renderer.setStyle(this.tooltipElement, 'left', `${left}px`);
  }

  private hideTooltip(): void {
    if (this.tooltipElement) {
      this.renderer.removeChild(document.body, this.tooltipElement);
      this.tooltipElement = undefined;
    }
  }

  private clearTimeouts(): void {
    if (this.showTimeout) {
      clearTimeout(this.showTimeout);
      this.showTimeout = undefined;
    }
    if (this.hideTimeout) {
      clearTimeout(this.hideTimeout);
      this.hideTimeout = undefined;
    }
  }

  ngOnDestroy(): void {
    this.clearTimeouts();
    this.hideTooltip();
  }
}

Creating Custom Structural Directives

Basic Structural Directive

// unless.directive.ts
import {
  Directive,
  Input,
  TemplateRef,
  ViewContainerRef
} from '@angular/core';

@Directive({
  selector: '[appUnless]'
})
export class UnlessDirective {
  private hasView = false;

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) {}

  @Input() set appUnless(condition: boolean) {
    if (!condition && !this.hasView) {
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else if (condition && this.hasView) {
      this.viewContainer.clear();
      this.hasView = false;
    }
  }
}

Usage:

<div *appUnless="showContent">
  This content is shown when showContent is false
</div>

Advanced Structural Directive with Context

// repeat.directive.ts
import {
  Directive,
  Input,
  TemplateRef,
  ViewContainerRef,
  EmbeddedViewRef
} from '@angular/core';

interface RepeatContext {
  $implicit: number;
  index: number;
  first: boolean;
  last: boolean;
  even: boolean;
  odd: boolean;
  count: number;
}

@Directive({
  selector: '[appRepeat]'
})
export class RepeatDirective {
  private views: EmbeddedViewRef<RepeatContext>[] = [];

  constructor(
    private templateRef: TemplateRef<RepeatContext>,
    private viewContainer: ViewContainerRef
  ) {}

  @Input() set appRepeat(count: number) {
    this.viewContainer.clear();
    this.views = [];

    for (let i = 0; i < count; i++) {
      const context: RepeatContext = {
        $implicit: i + 1,
        index: i,
        first: i === 0,
        last: i === count - 1,
        even: i % 2 === 0,
        odd: i % 2 === 1,
        count: count
      };

      const view = this.viewContainer.createEmbeddedView(
        this.templateRef,
        context
      );
      this.views.push(view);
    }
  }
}

Usage:

<div *appRepeat="5; let num; let i = index; let isFirst = first">
  Item {{ num }} (index: {{ i }})
  <span *ngIf="isFirst">(First item)</span>
</div>

Conditional Rendering with Loading State

// loading-if.directive.ts
import {
  Directive,
  Input,
  TemplateRef,
  ViewContainerRef,
  OnDestroy
} from '@angular/core';
import { Observable, Subscription } from 'rxjs';

@Directive({
  selector: '[appLoadingIf]'
})
export class LoadingIfDirective implements OnDestroy {
  private hasView = false;
  private subscription?: Subscription;
  private loadingTemplateRef?: TemplateRef<any>;

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) {}

  @Input() set appLoadingIf(observable: Observable<any>) {
    this.subscription?.unsubscribe();

    if (observable) {
      this.showLoading();

      this.subscription = observable.subscribe({
        next: (data) => {
          this.showContent();
        },
        error: (error) => {
          this.showError(error);
        }
      });
    }
  }

  @Input() set appLoadingIfLoadingTemplate(template: TemplateRef<any>) {
    this.loadingTemplateRef = template;
  }

  private showLoading(): void {
    this.viewContainer.clear();
    this.hasView = false;

    if (this.loadingTemplateRef) {
      this.viewContainer.createEmbeddedView(this.loadingTemplateRef);
    } else {
      // Default loading template
      const loadingElement = document.createElement('div');
      loadingElement.textContent = 'Loading...';
      loadingElement.className = 'loading-indicator';
      this.viewContainer.element.nativeElement.appendChild(loadingElement);
    }
  }

  private showContent(): void {
    this.viewContainer.clear();
    this.viewContainer.createEmbeddedView(this.templateRef);
    this.hasView = true;
  }

  private showError(error: any): void {
    this.viewContainer.clear();
    const errorElement = document.createElement('div');
    errorElement.textContent = `Error: ${error.message || error}`;
    errorElement.className = 'error-indicator';
    this.viewContainer.element.nativeElement.appendChild(errorElement);
  }

  ngOnDestroy(): void {
    this.subscription?.unsubscribe();
  }
}

Directive Composition

Combining Multiple Directives

// Example component using multiple directives
@Component({
  selector: 'app-directive-demo',
  template: `
    <div class="directive-demo">
      <!-- Multiple attribute directives -->
      <button
        appHoverHighlight
        highlightColor="lightblue"
        appTooltip="Click to save changes"
        tooltipPosition="top"
        [ngClass]="{ 'save-button': true, 'disabled': isSaving }"
        [disabled]="isSaving"
        (click)="save()">
        {{ isSaving ? 'Saving...' : 'Save' }}
      </button>

      <!-- Structural and attribute directives -->
      <div *ngIf="showAdvanced"
           appHighlight="lightyellow"
           [ngStyle]="{ 'padding': '20px', 'margin': '10px' }">
        <h3>Advanced Options</h3>
        <div *ngFor="let option of advancedOptions; trackBy: trackByOption"
             appHoverHighlight
             highlightColor="lightgray">
          {{ option.name }}: {{ option.value }}
        </div>
      </div>

      <!-- Custom structural directive with attribute directives -->
      <div *appRepeat="3; let num"
           [ngClass]="'item-' + num"
           appTooltip="Item number {{ num }}">
        Repeated item {{ num }}
      </div>
    </div>
  `
})
export class DirectiveDemoComponent {
  isSaving = false;
  showAdvanced = true;
  advancedOptions = [
    { id: 1, name: 'Option A', value: 'enabled' },
    { id: 2, name: 'Option B', value: 'disabled' },
    { id: 3, name: 'Option C', value: 'pending' }
  ];

  save(): void {
    this.isSaving = true;
    setTimeout(() => {
      this.isSaving = false;
    }, 2000);
  }

  trackByOption(index: number, option: any): number {
    return option.id;
  }
}

Directive Communication

// parent.directive.ts
import { Directive, ContentChildren, QueryList, AfterContentInit } from '@angular/core';
import { ChildDirective } from './child.directive';

@Directive({
  selector: '[appParent]'
})
export class ParentDirective implements AfterContentInit {
  @ContentChildren(ChildDirective) children!: QueryList<ChildDirective>;

  ngAfterContentInit(): void {
    this.children.forEach((child, index) => {
      child.setIndex(index);
      child.setParent(this);
    });
  }

  updateAllChildren(): void {
    this.children.forEach(child => child.update());
  }
}

// child.directive.ts
import { Directive, ElementRef, Renderer2 } from '@angular/core';
import { ParentDirective } from './parent.directive';

@Directive({
  selector: '[appChild]'
})
export class ChildDirective {
  private index = 0;
  private parent?: ParentDirective;

  constructor(
    private el: ElementRef,
    private renderer: Renderer2
  ) {}

  setIndex(index: number): void {
    this.index = index;
    this.updateDisplay();
  }

  setParent(parent: ParentDirective): void {
    this.parent = parent;
  }

  update(): void {
    this.updateDisplay();
  }

  private updateDisplay(): void {
    this.renderer.setAttribute(
      this.el.nativeElement,
      'data-index',
      this.index.toString()
    );
    this.renderer.addClass(
      this.el.nativeElement,
      `child-${this.index}`
    );
  }
}

Best Practices

1. Use Renderer2 for DOM Manipulation

// ✅ Good: Using Renderer2
@Directive({
  selector: '[appSafeHighlight]'
})
export class SafeHighlightDirective {
  constructor(
    private el: ElementRef,
    private renderer: Renderer2
  ) {}

  @Input() set appSafeHighlight(color: string) {
    this.renderer.setStyle(this.el.nativeElement, 'background-color', color);
  }
}

// ❌ Bad: Direct DOM manipulation
@Directive({
  selector: '[appUnsafeHighlight]'
})
export class UnsafeHighlightDirective {
  constructor(private el: ElementRef) {}

  @Input() set appUnsafeHighlight(color: string) {
    this.el.nativeElement.style.backgroundColor = color; // Not safe for SSR
  }
}

2. Handle Edge Cases and Validation

@Directive({
  selector: '[appValidatedInput]'
})
export class ValidatedInputDirective implements OnInit {
  @Input() validationRules: string[] = [];
  @Input() errorClass = 'validation-error';

  constructor(
    private el: ElementRef,
    private renderer: Renderer2
  ) {}

  ngOnInit(): void {
    if (!this.isValidElement()) {
      console.warn('ValidatedInputDirective can only be used on input elements');
      return;
    }

    this.setupValidation();
  }

  @HostListener('blur') onBlur(): void {
    this.validateInput();
  }

  private isValidElement(): boolean {
    return this.el.nativeElement.tagName.toLowerCase() === 'input';
  }

  private setupValidation(): void {
    // Setup validation logic
  }

  private validateInput(): void {
    const value = this.el.nativeElement.value;
    const isValid = this.runValidation(value);

    if (isValid) {
      this.renderer.removeClass(this.el.nativeElement, this.errorClass);
    } else {
      this.renderer.addClass(this.el.nativeElement, this.errorClass);
    }
  }

  private runValidation(value: string): boolean {
    // Implementation of validation logic
    return true;
  }
}

3. Proper Cleanup

@Directive({
  selector: '[appCleanupDirective]'
})
export class CleanupDirective implements OnInit, OnDestroy {
  private subscription = new Subscription();
  private resizeListener?: () => void;

  constructor(
    private el: ElementRef,
    private renderer: Renderer2
  ) {}

  ngOnInit(): void {
    // Setup event listeners
    this.resizeListener = this.renderer.listen('window', 'resize', () => {
      this.onResize();
    });

    // Setup subscriptions
    this.subscription.add(
      // Add your observables here
    );
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
    if (this.resizeListener) {
      this.resizeListener();
    }
  }

  private onResize(): void {
    // Handle resize
  }
}

Common Use Cases

1. Form Field Enhancements

@Directive({
  selector: '[appAutoFocus]'
})
export class AutoFocusDirective implements AfterViewInit {
  @Input() autoFocusDelay = 0;

  constructor(private el: ElementRef) {}

  ngAfterViewInit(): void {
    setTimeout(() => {
      this.el.nativeElement.focus();
    }, this.autoFocusDelay);
  }
}

@Directive({
  selector: '[appNumericOnly]'
})
export class NumericOnlyDirective {
  @HostListener('keydown', ['$event']) onKeyDown(event: KeyboardEvent): void {
    const allowedKeys = ['Backspace', 'Delete', 'Tab', 'Escape', 'Enter', 'ArrowLeft', 'ArrowRight'];

    if (allowedKeys.includes(event.key) ||
        (event.key >= '0' && event.key <= '9') ||
        (event.ctrlKey && ['a', 'c', 'v', 'x'].includes(event.key))) {
      return;
    }

    event.preventDefault();
  }
}

2. Accessibility Enhancements

@Directive({
  selector: '[appA11yClick]'
})
export class A11yClickDirective {
  @HostListener('keydown', ['$event']) onKeyDown(event: KeyboardEvent): void {
    if (event.key === 'Enter' || event.key === ' ') {
      event.preventDefault();
      this.el.nativeElement.click();
    }
  }

  constructor(private el: ElementRef, private renderer: Renderer2) {
    // Make element focusable if it's not already
    if (!this.el.nativeElement.hasAttribute('tabindex')) {
      this.renderer.setAttribute(this.el.nativeElement, 'tabindex', '0');
    }

    // Add appropriate role if not present
    if (!this.el.nativeElement.hasAttribute('role')) {
      this.renderer.setAttribute(this.el.nativeElement, 'role', 'button');
    }
  }
}

3. Performance Optimizations

@Directive({
  selector: '[appLazyLoad]'
})
export class LazyLoadDirective implements OnInit, OnDestroy {
  @Input() appLazyLoad = '';
  private observer?: IntersectionObserver;

  constructor(private el: ElementRef) {}

  ngOnInit(): void {
    if ('IntersectionObserver' in window) {
      this.observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            this.loadContent();
            this.observer?.unobserve(this.el.nativeElement);
          }
        });
      });

      this.observer.observe(this.el.nativeElement);
    } else {
      // Fallback for browsers without IntersectionObserver
      this.loadContent();
    }
  }

  ngOnDestroy(): void {
    if (this.observer) {
      this.observer.disconnect();
    }
  }

  private loadContent(): void {
    if (this.appLazyLoad && this.el.nativeElement.tagName === 'IMG') {
      this.el.nativeElement.src = this.appLazyLoad;
    }
  }
}

Summary

Angular directives are powerful tools for extending HTML functionality:

Structural Directives:

  • Use * syntax for shorthand
  • Modify DOM structure
  • Examples: *ngIf, *ngFor, *ngSwitch

Attribute Directives:

  • Modify appearance or behavior
  • Examples: ngClass, ngStyle

Custom Directives:

  • Use @Directive decorator
  • Leverage ElementRef and Renderer2 for safe DOM manipulation
  • Implement proper lifecycle management
  • Handle edge cases and validation

Best Practices:

  • Use Renderer2 for DOM manipulation
  • Implement proper cleanup in ngOnDestroy
  • Validate inputs and handle edge cases
  • Follow accessibility guidelines
  • Consider performance implications

Next, we'll explore Angular pipes and how they help transform and format data in templates.