EthicsComputerCourses
Join training +91 90532 10052

Directives

Angular · Lesson 25 of 41 Advanced 7 min read Updated

A directive is a class that adds behaviour to an element in a template. You have already used many: routerLink, formControlName and [formField] are all directives. Components are directives too; they are simply directives that come with their own template.

In this lesson you will write your own attribute directives, which change the look or behaviour of the element they sit on, and a small structural directive, which adds or removes parts of the page.

Your first attribute directive

Generate one with ng g directive highlight. A directive has a selector, usually an attribute in square brackets, and a host object that binds properties and listens to events on the element it is attached to (its host element):

TypeScript
import { Directive, input, signal } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  host: {
    '(mouseenter)': 'hovered.set(true)',
    '(mouseleave)': 'hovered.set(false)',
    '[style.backgroundColor]': 'hovered() ? color() : null',
    '[class.highlighted]': 'hovered()',
  },
})
export class Highlight {
  readonly color = input('#fff3b0', { alias: 'appHighlight' });
  protected hovered = signal(false);
}

Import the directive in a component and add the attribute to any element:

HTML
<p appHighlight>Hover: default yellow</p>
<p appHighlight="#d1f7c4">Hover: light green</p>

When the mouse enters the second paragraph, Angular sets its style and class:

Output
<p class="highlighted" style="background-color: rgb(209, 247, 196);">Hover: light green</p>

How it works:

  • '(mouseenter)': '...' in host is an event listener on the host element, just like (mouseenter) in a template.
  • '[style.backgroundColor]': '...' is a property binding on the host element. When the expression returns null, the style is removed.
  • input('#fff3b0', { alias: 'appHighlight' }) lets the same attribute both apply the directive and pass a value. Without a value, the default colour is used.

Directives are standalone, like components: a component must list Highlight in its imports to use it.

Directives that use the element

Some behaviour needs the real DOM element. Inject ElementRef to get it. This directive focuses an input when it first appears:

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

@Directive({
  selector: 'input[appAutofocus]',
})
export class Autofocus {
  private el = inject<ElementRef<HTMLInputElement>>(ElementRef);

  constructor() {
    afterNextRender(() => this.el.nativeElement.focus());
  }
}

The selector input[appAutofocus] means "an <input> element with the appAutofocus attribute", so the directive cannot be put on a <div> by mistake. afterNextRender waits until the element is really on the page, and does not run during server-side rendering.

Inputs, outputs and static host attributes

Directives can have outputs too. This one copies text to the clipboard when the element is clicked and tells the parent:

TypeScript
import { Directive, input, output } from '@angular/core';

@Directive({
  selector: '[appCopy]',
  host: {
    '(click)': 'copy()',
    '[attr.title]': '"Click to copy " + appCopy()',
    style: 'cursor: pointer',
  },
})
export class Copy {
  readonly appCopy = input.required<string>();
  readonly copied = output<string>();

  protected async copy() {
    await navigator.clipboard?.writeText(this.appCopy());
    this.copied.emit(this.appCopy());
  }
}
HTML
<span appCopy="UPI: canteen@okbank" (copied)="message.set('Copied ' + $event)">canteen@okbank</span>
<p>{{ message() }}</p>

A key without brackets, like style: 'cursor: pointer', sets a fixed attribute on the host element. After a click, the paragraph shows "Copied UPI: canteen@okbank".

Structural directives

A structural directive decides whether, or how many times, a piece of template is created. Built-in control flow (@if, @for) covers most needs, but a custom structural directive is useful for repeated app-specific rules, such as "show this only to staff":

TypeScript
import { Directive, TemplateRef, ViewContainerRef, effect, inject, input } from '@angular/core';
import { Session } from './session';

@Directive({
  selector: '[appIfRole]',
})
export class IfRole {
  readonly appIfRole = input.required<'student' | 'staff'>();
  private template = inject(TemplateRef<unknown>);
  private container = inject(ViewContainerRef);
  private session = inject(Session);

  constructor() {
    effect(() => {
      this.container.clear();
      if (this.session.role() === this.appIfRole()) {
        this.container.createEmbeddedView(this.template);
      }
    });
  }
}
HTML
<button *appIfRole="'staff'">Edit menu</button>

The star * is shorthand: Angular wraps the element in an <ng-template>, which is a piece of template that is not rendered by itself. The directive receives that template as a TemplateRef and a ViewContainerRef, the place where it may create copies. The effect re-runs whenever the session's role signal changes, so the button appears as soon as a staff member logs in. Remember that hiding a button is not security; the server must still check the user's role.

Composing directives

A component or directive can apply other directives to its own host element with hostDirectives, so you can reuse behaviour without inheritance:

TypeScript
@Component({
  selector: 'app-menu-tile',
  hostDirectives: [{ directive: Highlight, inputs: ['appHighlight: tileColor'] }],
  template: `<ng-content />`,
})
export class MenuTile {}

Every <app-menu-tile> now highlights on hover, and the parent can set the colour with tileColor="#ffe0e0".

Common mistakes

  • Forgetting the brackets in the selector. selector: 'appHighlight' matches an element named <appHighlight>, not an attribute. Write '[appHighlight]'.
  • Not importing the directive. If it is missing from imports, the attribute is silently treated as a plain HTML attribute and nothing happens.
  • Manipulating the DOM directly for things bindings can do. Prefer host bindings over nativeElement.style changes; they work with server-side rendering and are easier to test.
  • Writing a structural directive for one-off logic. For a single condition, @if is clearer.

Key points

  • Directives add behaviour to elements; components are directives with templates.
  • Attribute directives use a selector like [appHighlight] and a host object for event listeners, property bindings and fixed attributes.
  • Directives can have inputs (often aliased to the selector) and outputs, just like components.
  • Structural directives use TemplateRef and ViewContainerRef to create or remove views; the * prefix is shorthand for <ng-template>.
  • hostDirectives lets a component reuse directives on its own host element.

Exercise

Write an attribute directive appMaxLengthCounter for <textarea> elements. It takes a maximum length as its input (for example appMaxLengthCounter="200"), sets the native maxlength attribute through a host binding, and listens to the input event to update a remaining signal. Expose remaining publicly and show it in the template using a template reference variable with exportAs.

Show solution
TypeScript
import { Directive, input, numberAttribute, signal } from '@angular/core';

@Directive({
  selector: 'textarea[appMaxLengthCounter]',
  exportAs: 'counter',
  host: {
    '[attr.maxlength]': 'max()',
    '(input)': 'update($event)',
  },
})
export class MaxLengthCounter {
  readonly max = input(200, { alias: 'appMaxLengthCounter', transform: numberAttribute });
  readonly remaining = signal(200);

  protected update(event: Event) {
    const length = (event.target as HTMLTextAreaElement).value.length;
    this.remaining.set(this.max() - length);
  }
}
HTML
<textarea appMaxLengthCounter="200" #c="counter"></textarea>
<small>{{ c.remaining() }} characters left</small>

exportAs: 'counter' lets #c="counter" refer to the directive instance instead of the element. numberAttribute converts the string "200" from the attribute into a number.

Quick Quiz

1. Which selector applies a directive to any element that has the attribute appHighlight?

2. In a directive's host object, what does '(mouseenter)': 'hovered.set(true)' do?

3. What does the * in *appIfRole="'staff'" mean?

4. What does hostDirectives do?

Learn Angular with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call