Directives
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):
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:
<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:
<p class="highlighted" style="background-color: rgb(209, 247, 196);">Hover: light green</p>
How it works:
'(mouseenter)': '...'inhostis 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 returnsnull, 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:
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:
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());
}
}<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":
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);
}
});
}
}<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:
@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
hostbindings overnativeElement.stylechanges; they work with server-side rendering and are easier to test. - Writing a structural directive for one-off logic. For a single condition,
@ifis clearer.
Key points
- Directives add behaviour to elements; components are directives with templates.
- Attribute directives use a selector like
[appHighlight]and ahostobject 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
TemplateRefandViewContainerRefto create or remove views; the*prefix is shorthand for<ng-template>. hostDirectiveslets 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
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);
}
}<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?
Square brackets in a selector mean "has this attribute".
2. In a directive's host object, what does '(mouseenter)': 'hovered.set(true)' do?
Round brackets in host keys are event listeners, as in templates.
3. What does the * in *appIfRole="'staff'" mean?
The star is shorthand for a structural directive on an ng-template.
4. What does hostDirectives do?
Host directives let you compose reusable behaviour.
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
