Eine Direktive fügt einem bestehenden Element Verhalten hinzu, ohne ein eigenes Template: Tooltip, Ripple, Drag, Autofocus, Trap Focus, Highlight. Komponenten sind Direktiven mit Template. Mit der Directive Composition API (hostDirectives) kannst du Direktiven an eine Komponente „anhängen“, wie Mixins — ohne Klassenvererbung.
Eine Attribut-Direktive
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 = der Name des Selektors → <span appHighlight="yellow">
readonly color = input('var(--accent-soft)', { alias: 'appHighlight' });
protected readonly active = signal(false);
}host: {} statt @HostBinding / @HostListener
// ✗ Alter Stil
@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; }
}
// ✓ Aktueller Stil: die `host`-Metadaten (vom Style Guide empfohlen; ab v21 typgeprüft)
@Directive({
selector: '[appHighlight]',
host: {
'[style.backgroundColor]': 'active() ? color() : null',
'(mouseenter)': 'active.set(true)',
'(mouseleave)': 'active.set(false)',
'(document:keydown.escape)': 'active.set(false)', // globale Events: window:, document:
},
})
export class Highlight { /* ... */ }Eine Direktive mit DOM und 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()); // keine verwaisten Elemente in <body> zurücklassen
}
show() { /* erzeugt ein <div>, das neben dem Host positioniert wird */ }
hide() { this.tip?.remove(); this.tip = null; }
}Directive Composition API
Du hast drei verschiedene Buttons, die alle einen Ripple und einen Tooltip brauchen? Du fügst sie nicht jedes Mal von Hand hinzu und legst auch keine Basisklasse an. Du deklarierst sie in hostDirectives, und die Inputs/Outputs der Direktiven können an der Komponente exponiert (und umbenannt) werden.
// Directive Composition API: wiederverwendbares Verhalten, das an eine Komponente „angehängt“ wird
@Component({
selector: 'app-action-button',
hostDirectives: [
Ripple, // keine Inputs exponiert
{ directive: Tooltip, inputs: ['appTooltip: hint'] }, // exponiert den Input unter einem anderen Namen
// { 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="Speichern" variant="primary">Speichern</app-action-button>// Die Komponente kann die Direktiven aus hostDirectives injizieren (sie sitzen auf demselben Element)
export class ActionButton {
private readonly tooltip = inject(Tooltip);
showHelp() { this.tooltip.show(); }
}
// v22: Taucht dieselbe Direktive mehrfach in der hostDirectives-Kette auf,
// dedupliziert Angular sie (eine einzige Instanz).Komponente oder Direktive?
- Du fügst einem Element Verhalten hinzu, das du nicht kontrollierst (ein natives Input, ein Button aus einer anderen Bibliothek).
- Das Verhalten ist unabhängig vom Aussehen: Tooltip, Autofocus, Click-Outside, Lazy Image, Berechtigungen (
*appHasRole). - Du willst es frei mit anderem Verhalten kombinieren.
- Du brauchst eigenes Markup (ein Template).
- Du brauchst gekapselte Styles.
- Es ist ein in sich geschlossenes visuelles „Ding“ (Card, Badge, Dialog).
Über host gesetzte Klassen und Styles erklärt Class- & Style-Bindings; eine Direktive, die ein Element zum Formular-Control macht, steht unter Eigene Formular-Controls.
Häufige Stolperfallen
- DOM-Manipulation ohne Cleanup: Jedes Element, das zu
<body>hinzugefügt wird, muss inDestroyRef.onDestroywieder entfernt werden. document/windowdirekt verwenden: Nutzeinject(DOCUMENT)— das funktioniert auch mit SSR und in Tests.- Konflikte bei Host-Bindings: Setzen die Komponente und eine Host-Direktive dieselbe Klasse bzw. dasselbe Attribut, kommt es auf die Reihenfolge an. Halte die Zuständigkeiten getrennt.
- Zu viele hostDirectives: Jede ist eine zusätzliche Instanz an jedem Element. Bei Listen mit Tausenden von Elementen solltest du messen.
Spickzettel
@Directive({ selector: '[appX]', host: { '[attr.x]': 'y()', '(click)': 'on($event)' } })
input('', { alias: 'appX' }) // <div appX="wert">
inject(ElementRef) / inject(DOCUMENT) / inject(DestroyRef)
hostDirectives: [Dir, { directive: Dir2, inputs: ['a: b'], outputs: ['c'] }]
// Das Angular CDK bringt viele fertige Direktiven mit: cdkDrag, cdkTrapFocus, cdkObserveContent…