DEV Community

Atilla Baspinar
Atilla Baspinar

Posted on

Angular Directives

A directive is a class that adds behavior to an element in the DOM. Angular has three kinds:

Type What it does Example
Component Owns a template and DOM subtree @Component
Attribute Changes behavior or appearance of the host element NgModel, custom @Directive
Structural Adds or removes elements from the DOM *ngIf, *ngFor — superseded by @if, @for in Angular 17+

1. Built-in attribute directives

Attribute directives do not alter the DOM structure — they change how the host element looks or behaves. Common built-in examples:

  • NgModel — two-way binding for form inputs (from FormsModule)
  • NgClass — conditionally apply CSS classes
  • NgStyle — conditionally apply inline styles

These are covered in the forms and class/style binding tutorials.


2. Structural directives

Structural directives modify the DOM by adding or removing elements. Angular 17+ introduced built-in control flow syntax that replaces the need for them:

Old (structural directive) Modern (built-in control flow)
*ngIf="condition" @if (condition) { }
*ngFor="let x of list" @for (x of list; track x.id) { }
*ngSwitch / *ngSwitchCase @switch / @case

The old * syntax still works but requires importing NgIf, NgFor (or CommonModule) in the component's imports array. Prefer built-in control flow in new code.

Custom structural directives use <ng-template> and ViewContainerRef under the hood — the *directive syntax is just shorthand for <ng-template [directive]>. They are rarely needed in modern Angular and are not covered in depth here.


3. Custom attribute directive

A custom directive is a class decorated with @Directive. The selector can be any valid CSS selector — element, attribute, or class — but an attribute selector ([appMyDirective]) is the convention for directives (element selectors are reserved for components).

Mark it standalone: true (recommended), then add it to the consuming component's imports array.

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

@Directive({
  selector: '[appHighlight]',
  standalone: true,
})
export class HighlightDirective {
  private el = inject(ElementRef);
}
Enter fullscreen mode Exit fullscreen mode
<!-- consumer template -->
<p appHighlight>Hover over me</p>
Enter fullscreen mode Exit fullscreen mode
// consumer component
@Component({
  imports: [HighlightDirective],
  ...
})
Enter fullscreen mode Exit fullscreen mode

Listening to host events

The host property in @Directive maps DOM event names to handler expressions. Use $event to receive the native event object.

@Directive({
  selector: '[appHighlight]',
  standalone: true,
  host: {
    '(mouseenter)': 'onEnter($event)',
    '(mouseleave)': 'onLeave($event)',
  },
})
export class HighlightDirective {
  private el = inject(ElementRef);

  onEnter(event: MouseEvent) {
    this.el.nativeElement.style.backgroundColor = 'lightyellow';
  }

  onLeave(event: MouseEvent) {
    this.el.nativeElement.style.backgroundColor = '';
  }
}
Enter fullscreen mode Exit fullscreen mode

@HostListener is the older decorator-based alternative and produces the same result:

@HostListener('mouseenter', ['$event'])
onEnter(event: MouseEvent) { ... }
Enter fullscreen mode Exit fullscreen mode

Prefer the host property in modern code — it keeps all host bindings in one place and does not require a decorator import.


Directive inputs

Directives accept inputs from the template the same way components do. Use @Input() (decorator) or input() (signal, Angular 17.1+).

Explicit input

The directive attribute applies the directive; a separate binding sets the input by name.

@Directive({
  selector: '[appHighlight]',
  standalone: true,
  host: {
    '(mouseenter)': 'onEnter()',
    '(mouseleave)': 'onLeave()',
  },
})
export class HighlightDirective {
  color = input('yellow'); // signal input, default 'yellow'
  private el = inject(ElementRef);

  onEnter() {
    this.el.nativeElement.style.backgroundColor = this.color();
  }

  onLeave() {
    this.el.nativeElement.style.backgroundColor = '';
  }
}
Enter fullscreen mode Exit fullscreen mode
<!-- directive applied with attribute; color passed separately -->
<p appHighlight [color]="'orange'">Hover me</p>
Enter fullscreen mode Exit fullscreen mode

Simplified input — alias matching the selector

When the input's alias matches the selector name, the directive attribute itself carries the value. No separate attribute is needed.

color = input('yellow', { alias: 'appHighlight' });
// decorator style: @Input('appHighlight') color = 'yellow';
Enter fullscreen mode Exit fullscreen mode
<!-- expression binding — evaluates 'orange' as a string expression -->
<p [appHighlight]="'orange'">Hover me</p>

<!-- static value — assigns the literal string without brackets -->
<p appHighlight="orange">Hover me</p>
Enter fullscreen mode Exit fullscreen mode

The alias makes [appHighlight] double as both the directive selector and the input binding — one attribute does both jobs.


Service injection

Directives support dependency injection the same way components do. Use inject() in a field initializer (preferred) or constructor injection.

import { Injectable, signal } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class AuthService {
  isAdmin = signal(false);
}
Enter fullscreen mode Exit fullscreen mode
import { Directive, ElementRef, inject, effect } from '@angular/core';
import { AuthService } from './auth.service';

@Directive({
  selector: '[appAdminOnly]',
  standalone: true,
})
export class AdminOnlyDirective {
  private auth = inject(AuthService);
  private el   = inject(ElementRef);

  constructor() {
    effect(() => {
      // reacts automatically whenever isAdmin changes
      this.el.nativeElement.style.display = this.auth.isAdmin() ? '' : 'none';
    });
  }
}
Enter fullscreen mode Exit fullscreen mode
<button appAdminOnly>Delete user</button>
Enter fullscreen mode Exit fullscreen mode

Constructor injection is also valid: constructor(private auth: AuthService) {}. inject() is preferred in modern code because it works in field initializers and keeps the constructor clean.


4. hostDirectives — applying a directive without an attribute

A component can attach directives to itself via hostDirectives in @Component. The directive is applied automatically — consumers do not need to add the attribute in the template.

import { Component } from '@angular/core';
import { HighlightDirective } from './highlight.directive';

@Component({
  selector: 'app-card',
  hostDirectives: [HighlightDirective],
  template: `<div class="card"><ng-content /></div>`,
})
export class CardComponent {}
Enter fullscreen mode Exit fullscreen mode
<!-- no [appHighlight] needed — it is already attached by the component -->
<app-card>Card content</app-card>
Enter fullscreen mode Exit fullscreen mode

To expose the directive's inputs or outputs to the component's consumers, list them explicitly:

hostDirectives: [{
  directive: HighlightDirective,
  inputs: ['color'],   // <app-card [color]="'blue'"> now works
}]
Enter fullscreen mode Exit fullscreen mode

Top comments (0)