Une directive ajoute un comportement à un élément existant, sans template propre : tooltip, ripple, drag, autofocus, piège à focus, surlignage. Les composants sont des directives avec un template. Avec la Directive Composition API (hostDirectives), vous pouvez « attacher » des directives à un composant, comme des mixins — sans héritage de classe.
Une directive d’attribut
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 = le nom du sélecteur → <span appHighlight="yellow">
readonly color = input('var(--accent-soft)', { alias: 'appHighlight' });
protected readonly active = signal(false);
}host: {} plutôt que @HostBinding / @HostListener
// ✗ Ancien style
@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; }
}
// ✓ Style actuel : les métadonnées `host` (recommandées par le guide de style ; vérifiées par le typage depuis la v21)
@Directive({
selector: '[appHighlight]',
host: {
'[style.backgroundColor]': 'active() ? color() : null',
'(mouseenter)': 'active.set(true)',
'(mouseleave)': 'active.set(false)',
'(document:keydown.escape)': 'active.set(false)', // événements globaux : window:, document:
},
})
export class Highlight { /* ... */ }Une directive avec DOM et nettoyage
@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()); // ne laissez pas d’éléments orphelins dans <body>
}
show() { /* crée un <div> positionné à côté de l’hôte */ }
hide() { this.tip?.remove(); this.tip = null; }
}Directive Composition API
Vous avez trois boutons différents qui ont tous besoin d’un ripple et d’un tooltip ? Vous ne les ajoutez pas à la main à chaque fois et vous ne créez pas de classe de base. Vous les déclarez dans hostDirectives, et les inputs/outputs des directives peuvent être exposés (et renommés) sur le composant.
// Directive Composition API : des comportements réutilisables « attachés » à un composant
@Component({
selector: 'app-action-button',
hostDirectives: [
Ripple, // aucun input exposé
{ directive: Tooltip, inputs: ['appTooltip: hint'] }, // expose l’input sous un autre nom
// { 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="Enregistrer" variant="primary">Enregistrer</app-action-button>// Le composant peut injecter les directives de hostDirectives (elles sont sur le même élément)
export class ActionButton {
private readonly tooltip = inject(Tooltip);
showHelp() { this.tooltip.show(); }
}
// v22 : si la même directive apparaît plusieurs fois dans la chaîne hostDirectives,
// Angular la dédoublonne (une seule instance).Composant ou directive ?
- Vous ajoutez un comportement à un élément que vous ne contrôlez pas (un input natif, un bouton d’une autre bibliothèque).
- Le comportement est orthogonal à l’apparence : tooltip, autofocus, clic à l’extérieur, image en lazy loading, permissions (
*appHasRole). - Vous voulez le combiner librement avec d’autres comportements.
- Vous avez besoin de votre propre balisage (un template).
- Vous avez besoin de styles encapsulés.
- C’est un « objet » visuel autonome (carte, badge, boîte de dialogue).
Les classes et styles posés via host sont expliqués dans Bindings de classe et de style ; une directive qui transforme un élément en contrôle de formulaire, dans Contrôles de formulaire personnalisés.
Pièges fréquents
- Manipulation du DOM sans nettoyage : tout élément ajouté à
<body>doit être retiré dansDestroyRef.onDestroy. - Utiliser
document/windowdirectement : utilisezinject(DOCUMENT)— cela fonctionne aussi avec le SSR et dans les tests. - Conflits de host binding : si le composant et une host directive définissent la même classe/le même attribut, l’ordre compte. Gardez les responsabilités séparées.
- Trop de hostDirectives : chacune est une instance de plus sur chaque élément. Pour des listes de milliers d’éléments, mesurez.
Aide-mémoire
@Directive({ selector: '[appX]', host: { '[attr.x]': 'y()', '(click)': 'on($event)' } })
input('', { alias: 'appX' }) // <div appX="valeur">
inject(ElementRef) / inject(DOCUMENT) / inject(DestroyRef)
hostDirectives: [Dir, { directive: Dir2, inputs: ['a: b'], outputs: ['c'] }]
// Le CDK d’Angular propose de nombreuses directives prêtes à l’emploi: cdkDrag, cdkTrapFocus, cdkObserveContent…