A directive is a component without a template. It attaches behaviour to an element that already exists, without wrapping it in anything or changing its markup.
You have been using them since lesson one. routerLink,
routerLinkActive, ngModel and formGroup are all directives —
attributes that make an element do something it otherwise would not:
<a class="nav-link" routerLink="/admin" routerLinkActive="active" (click)="closeMenus()"
>Admin</a
>That is an ordinary anchor. routerLink intercepts the click and hands it to the
router; routerLinkActive adds a class when the route matches. Neither changed the
element into something else.
Writing one
The pizza app has one custom directive. It focuses the element it is put on:
@Directive({
selector: '[appAutofocus]',
})
export class Autofocus {
private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);Three things to notice.
The selector is in square brackets, which means "an element carrying this
attribute". A directive can select on anything CSS can express —
selector: 'a[href^="http"]' would match only external links, and applying it needs no
edit to any template that already contains one. That reach is the thing a directive has and a
component does not.
inject(ElementRef) gives you the host element. The directive is
instantiated with the element it sits on, and ElementRef.nativeElement is the real
DOM node.
The work happens after render:
constructor() {
afterNextRender(() => {
if (this.appAutofocus()) this.host.nativeElement.focus();
});
}⚠️ Calling .focus() in the constructor does nothing. The element exists, and it
is not yet in the document — and focusing a detached node fails silently, which is what
makes it worth a warning. afterNextRender runs once the DOM has been written.
Inputs on a directive
Directives take inputs exactly as components do. An input named the same as the selector lets the attribute double as the binding:
readonly appAutofocus = input(true, { transform: booleanAttribute });So appAutofocus alone is on, and [appAutofocus]="false" is off. The
booleanAttribute transform is doing necessary work: a bare attribute arrives as the
empty string, not as true.
host
To bind a property or listen for an event on the host element itself, use the
host object:
@Directive({
selector: '[appExternalLink]',
host: {
'[attr.target]': '"_blank"',
'[attr.rel]': '"noopener noreferrer"',
'(click)': 'report($event)',
},
})
export class ExternalLink {
report(event: MouseEvent) { /* … */ }
}The keys are the same binding syntax you would write in a template — square brackets for
properties, round for events — applied to the host rather than to a child. Components accept a
host object too, for the same purpose.
hostDirectives
A component or directive can apply other directives to itself, without the caller knowing:
@Component({
selector: 'app-fancy-input',
hostDirectives: [Autofocus],
template: `<input />`,
})
export class FancyInput {}This is composition where inheritance used to be the only option, and it is the reason directives are worth understanding even if you never write one directly: a behaviour written once as a directive can be attached to templates, composed into components, and tested on its own.
Structural directives
The third kind — the ones with an asterisk, *ngIf and *ngFor — add
and remove DOM rather than modifying an existing element. You can still write them, and you almost
certainly should not: the block syntax from lesson 7 covers what they were for, with better
ergonomics and no import.
What is next
Formatting a value for display without cluttering the class: pipes.