Skip to main content
Directives are classes that add behavior to elements in your Angular applications. Angular has three types of directives: components (directives with templates), structural directives, and attribute directives.

Types of Directives

Components

Directives with templates

Structural

Change DOM layout (add/remove elements)

Attribute

Change appearance or behavior of elements

Built-in Structural Directives

Modern Angular uses built-in control flow (@if, @for, @switch) instead of structural directives. These are still available for compatibility.

*ngIf (Legacy)

import { Component } from '@angular/core';
import { NgIf } from '@angular/common';

@Component({
  selector: 'app-conditional',
  standalone: true,
  imports: [NgIf],
  template: `
    <div *ngIf="isVisible">This content is visible</div>
    
    <div *ngIf="user; else noUser">
      Welcome, {{ user.name }}!
    </div>
    <ng-template #noUser>
      <p>Please log in.</p>
    </ng-template>
    
    <!-- With then/else -->
    <div *ngIf="isLoading; then loading else content"></div>
    <ng-template #loading>Loading...</ng-template>
    <ng-template #content>Content loaded!</ng-template>
  `
})
export class ConditionalComponent {
  isVisible = true;
  user: { name: string } | null = null;
  isLoading = false;
}

*ngFor (Legacy)

import { Component } from '@angular/core';
import { NgFor } from '@angular/common';

@Component({
  selector: 'app-list',
  standalone: true,
  imports: [NgFor],
  template: `
    <ul>
      <li *ngFor="let item of items">{{ item }}</li>
    </ul>
    
    <!-- With index -->
    <div *ngFor="let user of users; let i = index">
      {{ i + 1 }}. {{ user.name }}
    </div>
    
    <!-- With trackBy -->
    <div *ngFor="let item of items; trackBy: trackById">
      {{ item.name }}
    </div>
    
    <!-- With context variables -->
    <div *ngFor="let item of items; let first = first; let last = last; let even = even">
      <span [class.first]="first" [class.last]="last" [class.even]="even">
        {{ item }}
      </span>
    </div>
  `
})
export class ListComponent {
  items = ['Item 1', 'Item 2', 'Item 3'];
  users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];

  trackById(index: number, item: any) {
    return item.id;
  }
}

*ngSwitch (Legacy)

import { Component } from '@angular/core';
import { NgSwitch, NgSwitchCase, NgSwitchDefault } from '@angular/common';

@Component({
  selector: 'app-switch',
  standalone: true,
  imports: [NgSwitch, NgSwitchCase, NgSwitchDefault],
  template: `
    <div [ngSwitch]="status">
      <p *ngSwitchCase="'loading'">Loading...</p>
      <p *ngSwitchCase="'success'">Success!</p>
      <p *ngSwitchCase="'error'">Error occurred</p>
      <p *ngSwitchDefault>Unknown status</p>
    </div>
  `
})
export class SwitchComponent {
  status: 'loading' | 'success' | 'error' | 'unknown' = 'loading';
}

Built-in Attribute Directives

ngClass

Dynamically add/remove CSS classes:
import { Component } from '@angular/core';
import { NgClass } from '@angular/common';

@Component({
  selector: 'app-styling',
  standalone: true,
  imports: [NgClass],
  template: `
    <!-- String syntax -->
    <div [ngClass]="'class1 class2'">Content</div>
    
    <!-- Array syntax -->
    <div [ngClass]="['class1', 'class2', condition ? 'class3' : '']">Content</div>
    
    <!-- Object syntax -->
    <div [ngClass]="{
      'active': isActive,
      'disabled': isDisabled,
      'highlighted': isHighlighted
    }">Content</div>
    
    <!-- Expression -->
    <div [ngClass]="getClasses()">Content</div>
  `,
  styles: [`
    .active { color: green; }
    .disabled { opacity: 0.5; }
    .highlighted { background: yellow; }
  `]
})
export class StylingComponent {
  isActive = true;
  isDisabled = false;
  isHighlighted = true;
  condition = false;

  getClasses() {
    return {
      'active': this.isActive,
      'disabled': this.isDisabled
    };
  }
}

ngStyle

Dynamically set inline styles:
import { Component } from '@angular/core';
import { NgStyle } from '@angular/common';

@Component({
  selector: 'app-inline-styles',
  standalone: true,
  imports: [NgStyle],
  template: `
    <!-- Object syntax -->
    <div [ngStyle]="{
      'color': textColor,
      'font-size': fontSize + 'px',
      'background-color': bgColor
    }">Styled text</div>
    
    <!-- Expression -->
    <div [ngStyle]="getStyles()">More styled text</div>
    
    <!-- With units -->
    <div [ngStyle]="{
      'width.px': width,
      'height.%': height
    }">Sized box</div>
  `
})
export class InlineStylesComponent {
  textColor = 'blue';
  fontSize = 16;
  bgColor = '#f0f0f0';
  width = 200;
  height = 50;

  getStyles() {
    return {
      'color': this.textColor,
      'font-size': this.fontSize + 'px'
    };
  }
}

ngModel

Two-way data binding for form inputs:
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-forms',
  standalone: true,
  imports: [FormsModule],
  template: `
    <input [(ngModel)]="name" placeholder="Enter name">
    <p>Hello, {{ name }}!</p>
    
    <input type="checkbox" [(ngModel)]="isChecked">
    <p>Checked: {{ isChecked }}</p>
    
    <select [(ngModel)]="selectedOption">
      <option value="1">Option 1</option>
      <option value="2">Option 2</option>
    </select>
    <p>Selected: {{ selectedOption }}</p>
  `
})
export class FormsComponent {
  name = '';
  isChecked = false;
  selectedOption = '1';
}

Custom Attribute Directive

Create your own attribute directive:
import { Directive, ElementRef, HostListener, Input, inject } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  private el = inject(ElementRef);
  
  @Input() appHighlight = '';
  @Input() defaultColor = 'yellow';

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.appHighlight || this.defaultColor);
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight('');
  }

  private highlight(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

// Usage
@Component({
  selector: 'app-demo',
  standalone: true,
  imports: [HighlightDirective],
  template: `
    <p appHighlight>Hover over me (default yellow)</p>
    <p [appHighlight]="'lightblue'">Hover over me (light blue)</p>
    <p appHighlight defaultColor="pink">Hover over me (pink)</p>
  `
})
export class DemoComponent { }

Custom Structural Directive

Create a custom structural directive:
import { Directive, Input, TemplateRef, ViewContainerRef, inject } from '@angular/core';

@Directive({
  selector: '[appUnless]',
  standalone: true
})
export class UnlessDirective {
  private templateRef = inject(TemplateRef<any>);
  private viewContainer = inject(ViewContainerRef);
  private hasView = false;

  @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
@Component({
  selector: 'app-demo',
  standalone: true,
  imports: [UnlessDirective],
  template: `
    <p *appUnless="isHidden">This is shown when isHidden is false</p>
    <button (click)="isHidden = !isHidden">Toggle</button>
  `
})
export class DemoComponent {
  isHidden = false;
}

Directive with Multiple Inputs

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

@Directive({
  selector: '[appStyler]',
  standalone: true
})
export class StylerDirective implements OnInit {
  private el = inject(ElementRef);
  
  @Input() textColor = 'black';
  @Input() bgColor = 'white';
  @Input() fontSize = 16;

  ngOnInit() {
    this.applyStyles();
  }

  private applyStyles() {
    const element = this.el.nativeElement;
    element.style.color = this.textColor;
    element.style.backgroundColor = this.bgColor;
    element.style.fontSize = `${this.fontSize}px`;
  }
}

// Usage
@Component({
  selector: 'app-demo',
  standalone: true,
  imports: [StylerDirective],
  template: `
    <p appStyler 
       [textColor]="'blue'" 
       [bgColor]="'lightgray'"
       [fontSize]="20">
      Styled paragraph
    </p>
  `
})
export class DemoComponent { }

Host Bindings and Listeners

Use host metadata for cleaner directives:
import { Directive, HostBinding, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[appButton]',
  standalone: true,
  host: {
    'class': 'btn',
    '[class.btn-primary]': 'isPrimary',
    '[attr.role]': '"button"',
    '(click)': 'onClick()'
  }
})
export class ButtonDirective {
  @Input() isPrimary = false;
  @HostBinding('class.disabled') @Input() disabled = false;

  @HostListener('mouseenter')
  onMouseEnter() {
    console.log('Mouse entered');
  }

  onClick() {
    if (!this.disabled) {
      console.log('Button clicked');
    }
  }
}
Modern Angular prefers using the host metadata object over @HostBinding and @HostListener decorators.

Directive Composition

Apply multiple directives to a single host:
import { Directive } from '@angular/core';
import { HighlightDirective } from './highlight.directive';
import { TooltipDirective } from './tooltip.directive';

@Directive({
  selector: '[appComposed]',
  standalone: true,
  hostDirectives: [
    HighlightDirective,
    {
      directive: TooltipDirective,
      inputs: ['tooltipText: text'],
      outputs: ['tooltipShown']
    }
  ]
})
export class ComposedDirective { }

Best Practices

  1. Use modern control flow - Prefer @if, @for, @switch over *ngIf, *ngFor, *ngSwitch
  2. Keep directives focused - Single responsibility
  3. Use host metadata - Instead of @HostBinding and @HostListener
  4. Make directives standalone - Better tree-shaking
  5. Provide meaningful selectors - Use attribute selectors with a prefix
  6. Document behavior - Clear documentation for custom directives

Next Steps

Templates

Learn template syntax and bindings

Components

Understand component architecture