O directivă adaugă comportament unui element existent, fără template propriu: tooltip, ripple, drag, autofocus, trap focus, highlight. Componentele sunt directive cu template. Cu Directive Composition API (hostDirectives) poți „lipi” directive de o componentă, ca pe niște mixin-uri — fără moștenire de clase.
O directivă atribut
import { Directive, input, signal } from '@angular/core';
@Directive({
selector: '[appHighlight]',
host: {
'[style.backgroundColor]': 'active() ? color() : null',
'(mouseenter)': 'active.set(true)',
'(mouseleave)': 'active.set(false)',
},
})
export class Highlight {
// alias = numele selectorului → <span appHighlight="yellow">
readonly color = input('var(--accent-soft)', { alias: 'appHighlight' });
protected readonly active = signal(false);
}host: {} în loc de @HostBinding / @HostListener
// ✗ Stil vechi
@Directive({ selector: '[appHighlight]' })
export class Highlight {
@Input('appHighlight') color = 'yellow';
@HostBinding('style.backgroundColor') bg: string | null = null;
@HostListener('mouseenter') onEnter() { this.bg = this.color; }
@HostListener('mouseleave') onLeave() { this.bg = null; }
}
// ✓ Stil actual: metadata `host` (recomandat de style guide; type-checked din v21)
@Directive({
selector: '[appHighlight]',
host: {
'[style.backgroundColor]': 'active() ? color() : null',
'(mouseenter)': 'active.set(true)',
'(mouseleave)': 'active.set(false)',
'(document:keydown.escape)': 'active.set(false)', // evenimente globale: window:, document:
},
})
export class Highlight { /* ... */ }O directivă cu DOM și cleanup
@Directive({
selector: '[appTooltip]',
host: { '(mouseenter)': 'show()', '(mouseleave)': 'hide()', '(focusin)': 'show()', '(focusout)': 'hide()' },
})
export class Tooltip {
readonly text = input.required<string>({ alias: 'appTooltip' });
private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);
private readonly document = inject(DOCUMENT);
private tip: HTMLElement | null = null;
constructor() {
inject(DestroyRef).onDestroy(() => this.hide()); // nu lăsa elemente orfane în <body>
}
show() { /* creează un <div> poziționat lângă host */ }
hide() { this.tip?.remove(); this.tip = null; }
}Directive Composition API
Ai trei butoane diferite care toate au nevoie de ripple și tooltip? Nu le mai pui manual de fiecare dată și nu creezi o clasă de bază. Le declari în hostDirectives, iar input-urile/output-urile directivelor pot fi expuse (și redenumite) pe componentă.
// Directive Composition API: comportamente reutilizabile „lipite” de o componentă
@Component({
selector: 'app-action-button',
hostDirectives: [
Ripple, // fără input-uri expuse
{ directive: Tooltip, inputs: ['appTooltip: hint'] }, // expune input-ul sub alt nume
// { directive: Draggable, inputs: ['axis'], outputs: ['dragEnd'] },
],
host: { role: 'button', tabindex: '0', '[class]': 'variant()' },
template: `<ng-content />`,
})
export class ActionButton {
readonly variant = input<'primary' | 'ghost'>('primary');
}
// <app-action-button hint="Salvează" variant="primary">Salvează</app-action-button>// Componenta poate injecta directivele din hostDirectives (sunt pe același element)
export class ActionButton {
private readonly tooltip = inject(Tooltip);
showHelp() { this.tooltip.show(); }
}
// v22: dacă aceeași directivă apare de mai multe ori în lanțul de hostDirectives,
// Angular o de-duplică (o singură instanță).Componentă sau directivă?
- Adaugi comportament unui element pe care nu-l controlezi (input nativ, buton din altă librărie).
- Comportamentul e ortogonal pe aspect: tooltip, autofocus, click-outside, lazy image, permisiuni (
*appHasRole). - Vrei să-l combini liber cu alte comportamente.
- Ai nevoie de markup propriu (template).
- Ai nevoie de stiluri încapsulate.
- E un „lucru” vizual de sine stătător (card, badge, dialog).
Clasele și stilurile puse din host sunt explicate la Class & style bindings; o directivă care face dintr-un element un control de formular e la Controale custom de formular.
Capcane frecvente
- Manipulare DOM fără cleanup: orice element adăugat în
<body>trebuie șters înDestroyRef.onDestroy. document/windowdirect: foloseșteinject(DOCUMENT)— merge și la SSR și în teste.- Conflicte de host bindings: dacă și componenta și o host directive setează aceeași clasă/atribut, ordinea contează. Ține responsabilitățile separate.
- Prea multe hostDirectives: fiecare e o instanță în plus pe fiecare element. Pentru liste de mii de elemente, măsoară.
Fișă rapidă
@Directive({ selector: '[appX]', host: { '[attr.x]': 'y()', '(click)': 'on($event)' } })
input('', { alias: 'appX' }) // <div appX="valoare">
inject(ElementRef) / inject(DOCUMENT) / inject(DestroyRef)
hostDirectives: [Dir, { directive: Dir2, inputs: ['a: b'], outputs: ['c'] }]
// Angular CDK are multe directive gata făcute: cdkDrag, cdkTrapFocus, cdkObserveContent…