Composez des comportements Angular réutilisables avec la Directive Composition API : attachez inputs, outputs et logique via hostDirectives sans héritage.
Le problème : héritage vs composition
Pendant des années, la seule façon de partager du comportement entre composants Angular était l'héritage de classe. Vous créiez un BaseButtonComponent, puis PrimaryButtonComponent extends BaseButtonComponent. Cette approche semble propre au début, mais elle devient rapidement un piège dès que plusieurs comportements orthogonaux doivent cohabiter.
hostDirectives modélise.
Le principal défaut de l'héritage en TypeScript est qu'il est mono-parent : une classe ne peut étendre qu'une seule autre classe. Si vous voulez qu'un composant soit à la fois « accessible », « traqué » et « surligné », l'héritage vous force à empiler ces responsabilités dans une seule classe de base fourre-tout.
// ❌ Héritage : une hiérarchie rigide et fragile
class AccessibleBase {
// logique ARIA...
}
// On veut AUSSI du tracking → on l'entasse dans la base
class AccessibleTrackableBase extends AccessibleBase {
// logique analytics...
}
// On veut AUSSI du highlight → encore une couche
class AccessibleTrackableHighlightBase extends AccessibleTrackableBase {
// logique de style...
}
// Le composant final hérite d'une base monolithique
@Component({ selector: 'app-fancy-button', standalone: true, template: '...' })
class FancyButtonComponent extends AccessibleTrackableHighlightBase {
// impossible de retirer UN seul comportement sans casser la chaîne
}
Chaque nouveau besoin gonfle la classe de base. Retirer un comportement d'un seul composant devient impossible sans dupliquer toute la hiérarchie. C'est le fameux problème de la classe de base obèse (fat base class).
La composition renverse le modèle : chaque comportement vit dans sa propre directive standalone, testable et réutilisable isolément. Le composant hôte déclare simplement la liste des comportements qu'il souhaite adopter.
// ✅ Composition : chaque comportement est une brique indépendante
@Component({
selector: 'app-fancy-button',
standalone: true,
template: '<ng-content></ng-content>',
// On assemble les comportements comme des LEGO
hostDirectives: [AccessibleDirective, TrackingDirective, HighlightDirective]
})
class FancyButtonComponent {
// aucune classe de base, aucune duplication
}
On peut ajouter, retirer ou réordonner les comportements sans toucher à la logique interne du composant. C'est précisément la promesse de la Directive Composition API, que nous détaillons dans la section suivante.
Le pourquoi profond de ce changement tient à un principe de conception éprouvé : favoriser la composition à l'héritage (composition over inheritance). L'héritage crée un couplage fort entre parent et enfant : modifier la classe de base peut casser silencieusement tous ses descendants. La composition, à l'inverse, assemble des unités indépendantes qui ne se connaissent pas entre elles. Chaque directive ignore l'existence des autres, ce qui rend le système résilient au changement : ajouter un comportement ne peut pas régresser les autres.
Dans l'écosystème Angular, ce basculement a été rendu naturel par l'arrivée des composants et directives standalone. Avant standalone, partager une directive imposait de jongler avec les declarations et exports des NgModules. Désormais, une directive est une unité autoportante que l'on importe et compose à la demande. hostDirectives est l'aboutissement logique de cette évolution : la brique de comportement la plus fine possible, réutilisable partout.
La Directive Composition API : concept et syntaxe
Introduite en preview dans Angular v14 et stabilisée en v15, la Directive Composition API repose sur une seule propriété du décorateur : hostDirectives. Elle est disponible aussi bien sur @Component que sur @Directive.
Le principe est simple : lorsqu'Angular instancie l'hôte, il instancie aussi chaque directive listée dans hostDirectives et l'applique sur le même élément DOM. Les directives partagent donc l'ElementRef de l'hôte, ses bindings d'hôte (host) et son cycle de vie.
La propriété accepte deux formes. La forme courte, quand vous n'avez pas besoin d'exposer d'inputs/outputs :
// Forme courte : on liste simplement les classes de directive
@Component({
selector: 'app-card',
standalone: true,
template: '<ng-content></ng-content>',
// Chaque directive doit être standalone (contrainte obligatoire)
hostDirectives: [ElevationDirective, RippleDirective]
})
export class CardComponent {}
Et la forme objet, qui permet de re-exposer (mapper) les inputs et outputs de la directive source vers l'API publique de l'hôte :
// Forme objet : contrôle fin sur ce qui est exposé
@Component({
selector: 'app-card',
standalone: true,
template: '<ng-content></ng-content>',
hostDirectives: [
{
directive: ElevationDirective,
// Rendre l'input "level" accessible sous le nom "elevation"
inputs: ['level: elevation'],
// Re-exposer l'output "raised" tel quel
outputs: ['raised']
}
]
})
export class CardComponent {}
inputs/outputs. C'est un choix de conception délibéré : l'encapsulation par défaut évite de polluer l'API publique de l'hôte.
Voici comment les deux formes se comparent, pour choisir la bonne selon votre besoin :
| Critère | Forme courte [Dir] |
Forme objet { directive: Dir } |
|---|---|---|
| Syntaxe | Tableau de classes | Tableau d'objets de config |
| Exposer un input | Impossible | Via inputs: [] |
| Exposer un output | Impossible | Via outputs: [] |
| Renommer (alias) | Non | 'src: alias' |
| Cas d'usage | Comportement « boîte noire » | Comportement configurable |
Retenez la règle : commencez toujours par la forme courte, et passez à la forme objet uniquement quand vous devez rendre configurable un comportement depuis l'extérieur.
Un point souvent mal compris concerne le cycle de vie partagé. Les host directives ne sont pas des composants imbriqués : elles vivent sur le même nœud DOM que l'hôte. Leur ngOnInit, ngOnDestroy et leurs bindings d'hôte s'exécutent en synchronisation avec ceux de l'hôte. Concrètement, quand Angular détruit le composant, il détruit aussi proprement toutes ses host directives — vous n'avez aucune gestion de nettoyage supplémentaire à écrire. Cette symétrie de vie est ce qui rend la composition si sûre comparée à une instanciation manuelle de directive via ViewContainerRef.
Enfin, sachez que les host directives participent pleinement à l'injection de dépendances de l'hôte. Une host directive peut injecter un service, l'ElementRef partagé, ou même une autre directive présente sur l'hôte. C'est ce qui autorise des coordinations fines entre comportements — par exemple une directive de validation qui lit l'état d'une directive de formulaire sur le même élément.
Créer sa première directive composable
Passons à la pratique avec l'exemple canonique : une HighlightDirective qui applique une couleur de fond au survol. Nous allons la construire comme une directive parfaitement autonome, puis la composer sur un composant.
Étape 1 : écrire la directive standalone
// src/app/directives/highlight.directive.ts
import { Directive, ElementRef, HostListener, Input, inject } from '@angular/core';
@Directive({
selector: '[appHighlight]',
// OBLIGATOIRE pour être utilisable dans hostDirectives
standalone: true
})
export class HighlightDirective {
// Injection de l'élément hôte (partagé avec le composant hôte)
private el = inject(ElementRef<HTMLElement>);
// Couleur configurable, valeur par défaut jaune pâle
@Input() color = '#fff3cd';
// Listener déclenché à l'entrée de la souris
@HostListener('mouseenter')
onEnter(): void {
// Applique la couleur de fond au survol
this.el.nativeElement.style.backgroundColor = this.color;
}
// Listener déclenché à la sortie de la souris
@HostListener('mouseleave')
onLeave(): void {
// Réinitialise le fond
this.el.nativeElement.style.backgroundColor = '';
}
}
Cette directive fonctionne déjà seule : vous pourriez l'appliquer directement dans un template avec <div appHighlight>. Mais l'intérêt de la composition est de la rendre implicite sur un composant, sans que le consommateur ait à connaître son sélecteur.
Étape 2 : composer la directive sur un composant
// src/app/components/highlight-card.component.ts
import { Component } from '@angular/core';
import { HighlightDirective } from '../directives/highlight.directive';
@Component({
selector: 'app-highlight-card',
standalone: true,
// Le comportement de surlignage est "cousu" dans le composant
hostDirectives: [HighlightDirective],
template: `
<div class="card-body">
<ng-content></ng-content>
</div>
`
})
export class HighlightCardComponent {}
Résultat : tout élément <app-highlight-card> se surligne au survol, sans que le template parent ajoute quoi que ce soit. Le comportement est encapsulé dans le composant.
<!-- Utilisation : le surlignage est automatique -->
<app-highlight-card>
Passez la souris ici, le fond se colore.
</app-highlight-card>
HighlightCardComponent ET HighlightDirective sur le même élément DOM. La directive partage l'ElementRef de l'hôte, donc this.el.nativeElement pointe vers le <app-highlight-card> lui-même.
Notez qu'ici l'input color n'est pas exposé : le consommateur ne peut pas changer la couleur depuis le template. C'est voulu pour l'instant. Nous verrons dans la section suivante comment l'exposer proprement.
Exposer inputs et outputs des host directives
Par défaut, les inputs d'une host directive sont invisibles depuis l'extérieur. Pour les rendre accessibles, on utilise le tableau inputs de la forme objet. La syntaxe supporte deux variantes : passage direct ou avec alias.
Exposer un input sous son nom d'origine :
// L'input "color" de HighlightDirective devient public tel quel
@Component({
selector: 'app-highlight-card',
standalone: true,
hostDirectives: [
{
directive: HighlightDirective,
// "color" est maintenant bindable depuis le parent
inputs: ['color']
}
],
template: '<ng-content></ng-content>'
})
export class HighlightCardComponent {}
<!-- Le parent peut désormais piloter la couleur -->
<app-highlight-card color="#d1e7dd">Fond vert au survol</app-highlight-card>
Exposer un input sous un alias (renommage) : c'est la syntaxe la plus utile en pratique. Elle évite les collisions de noms et clarifie l'API publique.
@Component({
selector: 'app-highlight-card',
standalone: true,
hostDirectives: [
{
directive: HighlightDirective,
// Syntaxe 'nomSource: aliasPublic'
// "color" (interne) est exposé comme "highlightColor" (public)
inputs: ['color: highlightColor']
}
],
template: '<ng-content></ng-content>'
})
export class HighlightCardComponent {}
<!-- L'API publique utilise l'alias, pas le nom interne -->
<app-highlight-card highlightColor="#f8d7da">
Fond rouge pâle au survol
</app-highlight-card>
'color: highlightColor', la partie gauche (color) est le nom dans la directive source, la partie droite (highlightColor) est le nom exposé au monde extérieur. C'est l'inverse d'un alias d'@Input classique — une source d'erreur fréquente.
Exposer un output : le principe est identique. Prenons une directive qui émet un événement lorsqu'elle est cliquée.
// src/app/directives/clickable.directive.ts
import { Directive, EventEmitter, HostListener, Output } from '@angular/core';
@Directive({ selector: '[appClickable]', standalone: true })
export class ClickableDirective {
// Output émis à chaque clic sur l'hôte
@Output() activated = new EventEmitter<MouseEvent>();
@HostListener('click', ['$event'])
onClick(event: MouseEvent): void {
// Propage l'événement vers l'extérieur
this.activated.emit(event);
}
}
@Component({
selector: 'app-action-card',
standalone: true,
hostDirectives: [
{
directive: ClickableDirective,
// Expose l'output "activated" sous l'alias "cardActivated"
outputs: ['activated: cardActivated']
}
],
template: '<ng-content></ng-content>'
})
export class ActionCardComponent {}
<!-- On écoute l'output re-exposé comme un output natif -->
<app-action-card (cardActivated)="onActivate($event)">
Cliquez sur toute la carte
</app-action-card>
Les inputs re-exposés supportent aussi les signal inputs d'Angular v17+. Une directive qui déclare color = input('#fff3cd') se compose exactement de la même manière : la mécanique inputs: ['color: highlightColor'] reste inchangée.
Composer plusieurs comportements
La vraie puissance de hostDirectives apparaît quand on empile plusieurs comportements orthogonaux sur un même hôte. Construisons un composant qui combine trois préoccupations : tracking analytics, tooltip et accessibilité.
Directive 1 — tracking analytics :
// src/app/directives/track-click.directive.ts
import { Directive, HostListener, Input, inject } from '@angular/core';
import { AnalyticsService } from '../services/analytics.service';
@Directive({ selector: '[appTrackClick]', standalone: true })
export class TrackClickDirective {
private analytics = inject(AnalyticsService);
// Nom de l'événement à remonter
@Input() trackId = 'unknown';
@HostListener('click')
onClick(): void {
// Envoie l'événement au service analytics à chaque clic
this.analytics.track('click', { id: this.trackId });
}
}
Directive 2 — tooltip minimaliste :
// src/app/directives/tooltip.directive.ts
import { Directive, ElementRef, HostListener, Input, inject } from '@angular/core';
@Directive({ selector: '[appTooltip]', standalone: true })
export class TooltipDirective {
private el = inject(ElementRef<HTMLElement>);
// Texte de l'info-bulle
@Input() tooltipText = '';
@HostListener('mouseenter')
show(): void {
// Utilise l'attribut title natif (simplifié pour l'exemple)
this.el.nativeElement.setAttribute('title', this.tooltipText);
}
}
Directive 3 — accessibilité (rôle + focusabilité) :
// src/app/directives/a11y-button.directive.ts
import { Directive, ElementRef, HostListener, inject } from '@angular/core';
@Directive({
selector: '[appA11yButton]',
standalone: true,
// Les host bindings appliquent role et tabindex sur l'élément
host: {
'role': 'button',
'tabindex': '0'
}
})
export class A11yButtonDirective {
private el = inject(ElementRef<HTMLElement>);
// Rend le composant activable au clavier (Enter/Espace)
@HostListener('keydown.enter')
@HostListener('keydown.space')
onKey(): void {
// Simule un clic pour l'équivalence clavier/souris
this.el.nativeElement.click();
}
}
Composition finale : on assemble les trois briques et on ré-expose uniquement ce dont le consommateur a besoin.
// src/app/components/smart-action.component.ts
import { Component } from '@angular/core';
import { TrackClickDirective } from '../directives/track-click.directive';
import { TooltipDirective } from '../directives/tooltip.directive';
import { A11yButtonDirective } from '../directives/a11y-button.directive';
@Component({
selector: 'app-smart-action',
standalone: true,
hostDirectives: [
// Accessibilité : aucune config à exposer, forme courte
A11yButtonDirective,
// Tracking : on expose l'identifiant analytics
{
directive: TrackClickDirective,
inputs: ['trackId']
},
// Tooltip : on renomme pour clarifier l'API
{
directive: TooltipDirective,
inputs: ['tooltipText: hint']
}
],
template: '<ng-content></ng-content>'
})
export class SmartActionComponent {}
<!-- Un seul élément, trois comportements combinés -->
<app-smart-action trackId="cta-hero" hint="Lance l'inscription">
S'inscrire
</app-smart-action>
A11yButtonDirective sur dix composants différents sans jamais dupliquer la logique clavier. C'est la réutilisabilité horizontale que l'héritage ne permettait pas.
Pourquoi parle-t-on de comportements « orthogonaux » ? Parce que le tracking, le tooltip et l'accessibilité sont des préoccupations totalement indépendantes : changer la couleur du tooltip n'a aucun impact sur la logique analytics, et inversement. En programmation, deux responsabilités orthogonales sont précisément celles qu'il faut séparer en modules distincts. L'héritage les aurait fusionnées dans une même classe ; la composition les garde isolées tout en les faisant cohabiter sur un seul élément.
Un dernier détail améliore l'ergonomie : dans notre exemple, le consommateur du composant <app-smart-action> n'a jamais besoin de connaître l'existence des trois directives sous-jacentes. Il voit une API simple — trackId et hint — sans savoir qu'elle est alimentée par une composition. Cette encapsulation est essentielle pour un design system : l'implémentation interne peut évoluer (ajouter une directive de logging, remplacer le tooltip) sans jamais casser le contrat public du composant.
hostDirectives : composant vs directive
hostDirectives fonctionne aussi bien sur un @Component que sur un @Directive. Cela ouvre un pattern puissant : créer une directive « agrégat » qui regroupe plusieurs comportements réutilisables, et composer cet agrégat ailleurs.
Composer des directives sur une autre directive :
// Une directive qui agrège tracking + accessibilité
@Directive({
selector: '[appInteractive]',
standalone: true,
// Une directive peut composer d'autres directives
hostDirectives: [
A11yButtonDirective,
{ directive: TrackClickDirective, inputs: ['trackId'] }
]
})
export class InteractiveDirective {}
// On réutilise l'agrégat sur un composant, en une ligne
@Component({
selector: 'app-menu-item',
standalone: true,
// Compose l'agrégat → hérite de TOUS ses comportements
hostDirectives: [{ directive: InteractiveDirective, inputs: ['trackId'] }],
template: '<ng-content></ng-content>'
})
export class MenuItemComponent {}
Ce chaînage est transitif : les comportements de A11yButtonDirective et TrackClickDirective remontent jusqu'à MenuItemComponent. Vous construisez ainsi des presets de comportement réutilisables.
Voici les différences et contraintes selon la cible de la composition :
| Aspect | hostDirectives sur Composant | hostDirectives sur Directive |
|---|---|---|
| Cible standalone requise | Oui pour les directives listées | Oui pour les directives listées |
| Template propre | Oui (l'hôte a un template) | Non (pas de template) |
| Créer des presets réutilisables | Moins idéal | Cas d'usage phare |
| Partage de l'ElementRef | Élément hôte du composant | Élément portant la directive |
hostDirectives doit être standalone: true. Une directive déclarée dans un NgModule déclenche l'erreur « NG... hostDirectives must be standalone » à la compilation. C'est la raison pour laquelle cette API est indissociable de l'architecture standalone d'Angular moderne.
Autre limite à connaître : vous ne pouvez pas ajouter dynamiquement des host directives à l'exécution. La liste hostDirectives est statique, résolue à la compilation. Pour du dynamique, il faut recourir à ViewContainerRef et createComponent, ce qui sort du périmètre de la composition déclarative.
Cas réels en entreprise
Au-delà des exemples pédagogiques, voici trois patterns que l'on retrouve fréquemment en production dans des design systems Angular.
Cas 1 — Bouton accessible réutilisable
Dans un design system, tous les composants cliquables (bouton, chip, item de menu, carte cliquable) doivent partager le même socle d'accessibilité : rôle ARIA, focusabilité, gestion clavier, état désactivé. Plutôt que de le réimplémenter, on le compose.
// src/app/directives/accessible-control.directive.ts
import { Directive, HostBinding, Input } from '@angular/core';
@Directive({ selector: '[appAccessibleControl]', standalone: true })
export class AccessibleControlDirective {
@Input() disabled = false;
// Reflète l'état désactivé dans aria-disabled
@HostBinding('attr.aria-disabled')
get ariaDisabled(): string {
return String(this.disabled);
}
// Retire du flux de tabulation si désactivé
@HostBinding('attr.tabindex')
get tabindex(): string {
return this.disabled ? '-1' : '0';
}
}
// Tous les contrôles du design system héritent du socle a11y
@Component({
selector: 'app-ds-button',
standalone: true,
hostDirectives: [
{ directive: AccessibleControlDirective, inputs: ['disabled'] }
],
template: '<ng-content></ng-content>'
})
export class DsButtonComponent {}
Cas 2 — Directive analytics transverse
Une équipe growth veut tracker automatiquement les interactions sur des dizaines de composants sans polluer chaque template. Une directive analytics composée offre un point d'instrumentation unique et cohérent.
// La directive analytics reste 100% découplée de l'UI
@Directive({ selector: '[appAnalytics]', standalone: true })
export class AnalyticsDirective {
private analytics = inject(AnalyticsService);
@Input() eventName = 'interaction';
@Input() eventPayload: Record<string, unknown> = {};
@HostListener('click')
report(): void {
// Un seul endroit à maintenir pour toute l'instrumentation
this.analytics.track(this.eventName, this.eventPayload);
}
}
// Composée sur un composant produit sans ligne de tracking dans son template
@Component({
selector: 'app-product-tile',
standalone: true,
hostDirectives: [
{ directive: AnalyticsDirective, inputs: ['eventName', 'eventPayload'] }
],
template: '<ng-content></ng-content>'
})
export class ProductTileComponent {}
Cas 3 — Effet ripple (Material-like)
L'effet d'ondulation au clic est un comportement purement visuel, idéal pour la composition car totalement indépendant de la logique métier du composant.
// src/app/directives/ripple.directive.ts
import { Directive, ElementRef, HostListener, inject } from '@angular/core';
@Directive({
selector: '[appRipple]',
standalone: true,
// Position relative nécessaire pour contenir l'onde
host: { 'style': 'position: relative; overflow: hidden;' }
})
export class RippleDirective {
private el = inject(ElementRef<HTMLElement>);
@HostListener('click', ['$event'])
ripple(event: MouseEvent): void {
const host = this.el.nativeElement;
const circle = document.createElement('span');
const diameter = Math.max(host.clientWidth, host.clientHeight);
// Positionne l'onde au point de clic
circle.style.width = circle.style.height = diameter + 'px';
circle.style.left = (event.offsetX - diameter / 2) + 'px';
circle.style.top = (event.offsetY - diameter / 2) + 'px';
circle.className = 'ripple-effect';
host.appendChild(circle);
// Nettoie l'élément après l'animation CSS
circle.addEventListener('animationend', () => circle.remove());
}
}
// On combine ripple + accessibilité pour un bouton complet
@Component({
selector: 'app-fab',
standalone: true,
hostDirectives: [RippleDirective, AccessibleControlDirective],
template: '<ng-content></ng-content>'
})
export class FabComponent {}
Tests, pièges courants et bonnes pratiques
Tester un composant qui compose des host directives demande une approche spécifique : on ne teste pas la directive à travers son sélecteur, mais à travers le composant hôte réel dans un composant de test.
Tester avec TestBed et un host component
// smart-action.component.spec.ts
import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { SmartActionComponent } from './smart-action.component';
// Composant hôte de test qui utilise le composant à tester
@Component({
standalone: true,
imports: [SmartActionComponent],
template: `
<app-smart-action trackId="test-cta" hint="Aide">
Cliquez
</app-smart-action>
`
})
class TestHostComponent {}
describe('SmartActionComponent (host directives)', () => {
let fixture: ComponentFixture<TestHostComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TestHostComponent]
}).compileComponents();
fixture = TestBed.createComponent(TestHostComponent);
fixture.detectChanges();
});
it('applique role="button" via la host directive a11y', () => {
// On vérifie l'effet DOM produit par A11yButtonDirective
const el = fixture.debugElement.query(By.css('app-smart-action'));
expect(el.nativeElement.getAttribute('role')).toBe('button');
expect(el.nativeElement.getAttribute('tabindex')).toBe('0');
});
it('expose le tooltip via l\'input aliasé "hint"', () => {
const el = fixture.debugElement.query(By.css('app-smart-action'));
// mouseenter déclenche l'ajout de l'attribut title
el.nativeElement.dispatchEvent(new MouseEvent('mouseenter'));
fixture.detectChanges();
expect(el.nativeElement.getAttribute('title')).toBe('Aide');
});
});
On peut aussi récupérer une instance de host directive via l'injecteur du composant, pour des tests unitaires plus ciblés :
it('permet d\'injecter la host directive depuis l\'hôte', () => {
const fixture = TestBed.createComponent(SmartActionComponent);
// La host directive est disponible dans l'injecteur de l'hôte
const tracking = fixture.debugElement.injector.get(TrackClickDirective);
expect(tracking).toBeInstanceOf(TrackClickDirective);
});
Pièges courants
Piège 1 — l'ordre d'application compte. Les host directives s'appliquent dans l'ordre du tableau, avant l'hôte lui-même. Si deux directives modifient le même attribut ou le même host binding, la dernière de la liste — puis l'hôte — l'emporte.
// L'ordre détermine qui gagne sur un même attribut/binding
hostDirectives: [
SetBlueBackgroundDirective, // s'applique en premier
SetRedBackgroundDirective // écrase → le rouge gagne
]
// Puis un host binding de l'hôte lui-même écraserait encore ces deux-là
Piège 2 — la DI et les providers. Une host directive partage l'injecteur de l'hôte. Si la directive déclare des providers, ceux-ci sont ajoutés à l'injecteur du composant hôte. Attention aux collisions : deux directives fournissant le même token provoquent un écrasement silencieux.
Piège 3 — inputs privés par défaut. Oublier de lister un input dans inputs: [] est l'erreur la plus fréquente : le binding parent est alors silencieusement ignoré, sans erreur. Vérifiez toujours l'exposition explicite.
Piège 4 — sens du mapping inversé. Comme vu plus haut, 'source: alias' se lit interne → public, à l'inverse de l'alias d'@Input. Relisez toujours vos mappings.
Checklist avant d'adopter hostDirectives
- Toutes les directives composées sont
standalone: true - Chaque directive porte une seule responsabilité claire
- Les inputs à piloter de l'extérieur sont listés dans
inputs: [] - Le sens des alias
'source: public'est vérifié - L'ordre du tableau
hostDirectivesest intentionnel (host bindings) - Aucune collision de
providersentre directives composées - Les tests passent par un host component réel via TestBed
- La composition remplace un héritage, pas une simple fonction utilitaire
- Pas de besoin d'ajout dynamique (sinon
ViewContainerRef)
La Directive Composition API marque un tournant dans la manière de concevoir les composants Angular : on passe d'une pensée hiérarchique à une pensée modulaire. En isolant chaque comportement dans une directive standalone testable, vous obtenez des composants plus légers, un design system plus cohérent et une base de code où l'ajout d'un comportement se fait en une ligne, sans jamais casser l'existant. Commencez petit — une HighlightDirective, un socle d'accessibilité — puis étendez la composition à mesure que vos patterns se stabilisent.