Eine Attribute Directive ist eine Klasse mit @Directive-Decorator, die das Verhalten oder Aussehen eines bestehenden Elements verändert — ohne ein eigenes Template mitzubringen. Während eine Komponente immer einen eigenen DOM-Subtree rendert, hängt sich eine Direktive an ein vorhandenes Element und manipuliert dort: setzt Klassen, hört auf Events, fokussiert nach Render, schließt Dropdowns bei Klicks außerhalb. NgClass, NgStyle und NgModel aus dem Framework sind alle Attribute-Direktiven — und genau dasselbe Toolkit steht dir für eigene Direktiven offen.
Dieser Artikel zeigt mit Angular 21 den ganzen Werkzeugkasten: @Directive mit Bracket-Selector, Renderer2 für SSR-sicheres DOM-Update, das host-Property im Decorator als moderne Alternative zu @HostBinding/@HostListener, Signal-Inputs für konfigurierbare Werte und zwei Praxis-Direktiven (Auto-Focus + Click-Outside), die zeigen, wie afterNextRender, DestroyRef und Renderer2.listen zusammenspielen.
Verhalten und Aussehen ohne eigenes Template
Eine Attribute Directive ist eine Klasse, die mit @Directive markiert ist und an einem bereits vorhandenen Element greift. Im Template sieht das wie ein zusätzliches Attribut aus: <p appHighlight>...</p> oder <input [appAutofocus]="true">. Die Direktive bekommt eine Referenz auf das Host-Element, kann auf seine Inputs reagieren, Events auf ihm registrieren und seine Klassen, Styles und Attribute setzen.
Klassiker aus dem Framework selbst sind NgClass, NgStyle, NgModel, RouterLink, RouterLinkActive. Eigene Direktiven sind das Mittel gegen Copy-Paste-Logik: Wenn fünf Komponenten denselben Hover-Highlight-Code haben, gehört der in eine appHighlight-Direktive. Wenn drei Forms den gleichen mouseenter + mouseleave + Renderer2-Block enthalten, gehört der in eine wiederverwendbare Direktive.
Selector mit Brackets, Standalone als Default
Der @Directive-Decorator verlangt mindestens einen selector. Für eine Attribute-Direktive steht der Selector immer in eckigen Klammern: [appHighlight]. Ohne Brackets würde Angular die Direktive als Element-Direktive behandeln und nur auf <app-highlight>-Tags greifen — was selten gewollt ist.
import { Directive, ElementRef, inject, Renderer2 } from '@angular/core';
@Directive({
selector: '[appHighlight]',
// standalone: true ist seit v17 Default — nicht mehr nötig hinzuschreiben
})
export class HighlightDirective {
private el = inject(ElementRef<HTMLElement>);
private renderer = inject(Renderer2);
constructor() {
this.renderer.setStyle(this.el.nativeElement, 'background-color', 'yellow');
}
}Weitere Decorator-Properties, die für Direktiven relevant sind:
| Property | Zweck |
|---|---|
selector | CSS-Selector für das Element, auf dem die Direktive greift |
host | Map für Property-/Event-/Attribute-Bindings am Host-Element |
providers | DI-Provider, die diese Direktive (und ihre Kinder) bereitstellt |
exportAs | Template-Variablen-Name (#tooltip="appTooltip") |
standalone | Default true seit v17; auf false setzen für NgModule-Variante |
hostDirectives | Andere Direktiven, die mit dieser zusammen auf den Host greifen |
Lifecycle-Hooks (ngOnInit, ngOnDestroy, ngOnChanges) funktionieren in Direktiven exakt wie in Components.
DOM-Manipulation, die auch unter SSR sauber läuft
ElementRef gibt dir per nativeElement Zugriff auf das DOM-Element. Direkt darauf herumzuschreiben (el.nativeElement.style.color = 'red') ist nicht SSR-safe: Im Node-Renderer existiert keine window, kein document, und nicht jedes Property hat dort eine Implementierung. Renderer2 ist die plattform-agnostische Abstraktion — derselbe Aufruf läuft im Browser und im Server-Rendering.
import { Directive, ElementRef, inject, Renderer2, OnInit } from '@angular/core';
@Directive({
selector: '[appHighlight]',
})
export class HighlightDirective implements OnInit {
private el = inject(ElementRef<HTMLElement>);
private renderer = inject(Renderer2);
ngOnInit() {
// Style setzen
this.renderer.setStyle(this.el.nativeElement, 'background-color', '#fff8a8');
// Klasse hinzufügen
this.renderer.addClass(this.el.nativeElement, 'is-highlighted');
// Attribute setzen
this.renderer.setAttribute(this.el.nativeElement, 'data-highlighted', 'true');
}
}Renderer2 bietet alle DOM-Operationen, die du brauchst: setStyle, removeStyle, addClass, removeClass, setAttribute, removeAttribute, setProperty, appendChild, listen. Letzteres ist besonders nützlich für Listener auf document oder window, die du sauber per Cleanup-Funktion wieder entfernen kannst — siehe Click-Outside-Beispiel weiter unten.
Property- und Event-Bindings als Decorator
@HostBinding deklariert eine Klassen-Property, deren Wert Angular automatisch auf eine Property, Klasse, Style oder ein Attribute des Host-Elements bindet. @HostListener deklariert eine Methode, die auf ein DOM-Event am Host (oder global an window / document) reagiert. Beide Decorators sind seit Angular-1.x da und nach wie vor voll unterstützt.
import { Directive, HostBinding, HostListener } from '@angular/core';
@Directive({
selector: '[appHover]',
})
export class HoverDirective {
@HostBinding('class.is-hover') isHover = false;
@HostBinding('style.cursor') cursor = 'pointer';
@HostListener('mouseenter')
onEnter() {
this.isHover = true;
}
@HostListener('mouseleave')
onLeave() {
this.isHover = false;
}
@HostListener('window:keydown.escape')
onEscape() {
this.isHover = false;
}
}Dieselbe Funktion, kürzer notiert
Das host-Property im Decorator ist die moderne, deklarative Alternative zu @HostBinding und @HostListener. Du beschreibst Bindings als Map: Keys mit [...] sind Property-Bindings, Keys mit (...) sind Event-Bindings, Keys ohne Brackets sind statische Attribute.
import { Directive, signal } from '@angular/core';
@Directive({
selector: '[appHover]',
host: {
'[class.is-hover]': 'isHover()',
'[style.cursor]': '"pointer"',
'(mouseenter)': 'isHover.set(true)',
'(mouseleave)': 'isHover.set(false)',
'(window:keydown.escape)': 'isHover.set(false)',
},
})
export class HoverDirective {
isHover = signal(false);
}| Aspekt | @HostBinding / @HostListener | host-Property |
|---|---|---|
| Zeilenanzahl bei vielen Bindings | wächst linear mit jedem Decorator | bleibt kompakt in einer Map |
| Lesbarkeit | gut bei 1–2 Bindings | klar überlegen ab 3+ Bindings |
| Statische Werte | Klassen-Property mit Initializer | inline als String ohne extra Property |
| Signal-Werte | direkt als Property, Decorator liest sie | als Aufruf in Quotes: 'mySignal()' |
| Empfehlung 2026 | OK für Bestandscode | bevorzugt für Neuanlagen |
Konfigurierbare Direktiven mit @Input und input()
Damit eine Direktive von außen konfiguriert werden kann, deklariert sie Inputs — entweder klassisch via @Input() oder modern via input() Signal. Trick: Wenn du dem Input denselben Namen wie dem Selector gibst, kannst du [appHighlight]="'yellow'" schreiben statt appHighlight [color]="'yellow'".
import { Directive, input, effect, inject, Renderer2, ElementRef } from '@angular/core';
@Directive({
selector: '[appHighlight]',
host: {
'[style.background-color]': 'color()',
'[style.padding]': '"2px 4px"',
},
})
export class HighlightDirective {
// Selector-aliased Input: <p [appHighlight]="'yellow'">
color = input<string>('yellow', { alias: 'appHighlight' });
// Zweiter, separater Input
border = input<string | null>(null);
private el = inject(ElementRef<HTMLElement>);
private renderer = inject(Renderer2);
constructor() {
effect(() => {
const b = this.border();
if (b) {
this.renderer.setStyle(this.el.nativeElement, 'border', b);
} else {
this.renderer.removeStyle(this.el.nativeElement, 'border');
}
});
}
}Im Template lassen sich beide Inputs unabhängig setzen:
<p [appHighlight]="'mistyrose'" [border]="'1px solid crimson'">
Wichtig
</p>Services injecten wie in Components
Direktiven sind in Angulars DI-Hierarchie gleichberechtigte Bürger. inject() funktioniert genauso wie in Components — du holst Services aus dem Root-Injector, aus einem Component-Injector oder aus einem Eltern-Provider. Das macht Direktiven zur idealen Stelle, um Cross-Cutting-Concerns wie Tracking, Logging oder Analytics zu kapseln.
import { Directive, inject, input } from '@angular/core';
import { AnalyticsService } from './analytics.service';
@Directive({
selector: '[appTrackClick]',
host: {
'(click)': 'onClick($event)',
},
})
export class TrackClickDirective {
private analytics = inject(AnalyticsService);
// Aliased Input: <button [appTrackClick]="'cta-hero'">
event = input<string>('', { alias: 'appTrackClick' });
onClick(ev: MouseEvent) {
const name = this.event();
if (name) {
this.analytics.track(name, {
x: ev.clientX,
y: ev.clientY,
});
}
}
}Eine einzige Direktive ersetzt damit dutzende (click)="track('cta-hero', $event)"-Aufrufe im Template — der Service-Aufruf passiert zentral, das Tracking-Vokabular bleibt konsistent.
Element nach Render fokussieren — sauber, ohne setTimeout
Forms haben oft ein Erstes-Feld-fokussieren-nach-Mount-Pattern. Statt das in jeder Component mit ngAfterViewInit + setTimeout(0) zu lösen, gehört das in eine [appAutofocus]-Direktive. Mit afterNextRender (seit v16) ist das Timing sauber — der Callback läuft erst, nachdem Angular den ersten Frame committed hat, also nach dem Mount und vor dem ersten Paint.
import {
Directive, ElementRef, inject, input, afterNextRender,
} from '@angular/core';
@Directive({
selector: '[appAutofocus]',
})
export class AutofocusDirective {
private el = inject(ElementRef<HTMLElement>);
// Mit boolean-Input optional ein-/aussschaltbar
enabled = input<boolean>(true, { alias: 'appAutofocus' });
constructor() {
afterNextRender(() => {
if (this.enabled()) {
this.el.nativeElement.focus();
}
});
}
}Im Template:
<input type="text" [appAutofocus]="true" placeholder="E-Mail">
<!-- bedingt: nur fokussieren, wenn Form gerade neu eingeblendet wurde -->
<input type="text" [appAutofocus]="isFirstStep()" placeholder="Vorname">Dropdown schließen, wenn der User außerhalb klickt
Ein Dropdown soll sich schließen, wenn der User irgendwo außerhalb klickt. Lösung: Eine (appClickOutside)-Direktive, die einen click-Listener auf document registriert, prüft, ob das Klick-Target innerhalb des Host-Elements liegt — und falls nicht, ein Output-Event feuert. Cleanup übernehmen Renderer2.listen (das eine Unsubscribe-Funktion zurückgibt) und DestroyRef.
import {
Directive, ElementRef, inject, output,
Renderer2, DestroyRef, afterNextRender,
} from '@angular/core';
@Directive({
selector: '[appClickOutside]',
})
export class ClickOutsideDirective {
private el = inject(ElementRef<HTMLElement>);
private renderer = inject(Renderer2);
private destroyRef = inject(DestroyRef);
// Output-Event, das gefeuert wird
clickOutside = output<MouseEvent>({ alias: 'appClickOutside' });
constructor() {
// Erst nach erstem Render registrieren, sonst feuert der Listener
// direkt beim Öffnen-Klick, der das Element überhaupt ins DOM bringt.
afterNextRender(() => {
const unlisten = this.renderer.listen('document', 'click', (ev: MouseEvent) => {
const target = ev.target as Node;
if (!this.el.nativeElement.contains(target)) {
this.clickOutside.emit(ev);
}
});
this.destroyRef.onDestroy(unlisten);
});
}
}Im Template:
@if (open()) {
<div class="dropdown" (appClickOutside)="open.set(false)">
<ul>...</ul>
</div>
}In Component imports-Array vs. NgModule-declarations
Standalone-Direktiven (Default seit v17) werden direkt im imports-Array der konsumierenden Component aufgelistet. Kein zusätzlicher NgModule-Tanz, kein declarations/exports-Paar — nur ein Import in der Component, die die Direktive nutzt.
import { Component, signal } from '@angular/core';
import { HighlightDirective } from './highlight.directive';
import { AutofocusDirective } from './autofocus.directive';
import { ClickOutsideDirective } from './click-outside.directive';
@Component({
selector: 'app-form',
standalone: true,
imports: [HighlightDirective, AutofocusDirective, ClickOutsideDirective],
template: `
<input type="text"
[appAutofocus]="true"
[appHighlight]="'lightyellow'">
@if (open()) {
<div class="menu" (appClickOutside)="open.set(false)">...</div>
}
`,
})
export class FormComponent {
open = signal(false);
}Für ältere NgModule-basierte Apps musst du die Direktive deklarieren und exportieren:
import { NgModule } from '@angular/core';
import { HighlightDirective } from './highlight.directive';
@NgModule({
declarations: [HighlightDirective],
exports: [HighlightDirective],
})
export class HighlightModule { }In modernen Standalone-Apps ist NgModule die Ausnahme. Für eine Library, die alte Konsumenten unterstützen muss, kannst du Direktiven aber problemlos standalone: false markieren und in einem klassischen Module deklarieren.
Wichtige Details, die in Code-Reviews auffallen
Selector-Brackets sind Pflicht für Attribute-Direktiven
selector: ‘[appHighlight]’ matcht ein Attribut. selector: ‘appHighlight’ matcht dagegen ein Element namens <appHighlight> — fast nie das, was du willst. Häufiger Anfängerfehler: Brackets vergessen und sich wundern, warum die Direktive nicht greift.
ElementRef.nativeElement direkt zu schreiben ist nicht SSR-safe
Im Browser läuft el.nativeElement.style.color = ‘red’ tadellos — unter Angular Universal / SSR gibt es aber kein echtes DOM, und manche Properties existieren dort nicht. Renderer2.setStyle(…) ist die plattform-agnostische Variante; dieselbe Direktive läuft Browser-seitig und im Node-Renderer.
@HostBinding läuft pro Change-Detection-Cycle
Jeder Getter, der mit @HostBinding dekoriert ist, wird bei jedem CD-Cycle ausgewertet. Unter OnPush ist das nur bei Trigger-Events kritisch, unter Default-CD aber häufig. Workaround: Ergebnis in eine Property cachen oder ein computed-Signal verwenden, das nur bei echten Änderungen neu rechnet.
@HostListener-Cleanup ist automatisch
Listener, die via @HostListener oder host: { ‘(click)’: ’…’ } registriert werden, räumt Angular beim Destroy automatisch auf. Manuelle Listener auf document oder window via renderer.listen brauchen dagegen explizites Cleanup über die Unsubscribe-Funktion plus DestroyRef.
host-Property ist Standalone-fähig und kürzer
Der Decorator-host-Eintrag ersetzt @HostBinding + @HostListener in einem deklarativen Block. Viele Style-Guides (auch der Angular-API-Doc-Hinweis seit v17) bevorzugen das für Neucode. Bestehende Decorator-Schreibweisen funktionieren weiter — beides darf sogar in derselben Direktive nebeneinander vorkommen.
Direktiven können andere Direktiven via hostDirectives erweitern
Seit Angular v15 erlaubt hostDirectives: [{ directive: TooltipDirective, inputs: [‘tooltip’] }], andere Direktiven beim Match transparent mit anzuwenden. Das ist die offizielle Composition-API für wiederverwendbare Verhalten — siehe Querverweis auf Directive Composition.
Direktiven nutzen DI wie Components
inject(MyService) funktioniert in Direktiven exakt so wie in Components. Du kannst darüber Analytics-Tracker, Logger, Form-Builder oder jeden anderen Provider holen — und über providers: […] im Decorator sogar eigene Services für die Direktiv-Subtree-Hierarchie bereitstellen.
Selector kann Tag-Names mit Attributen kombinieren
selector: ‘button[appConfirm]’ matcht ausschließlich <button>-Elemente, die das appConfirm-Attribut tragen. Damit kannst du Direktiven gezielt einschränken — z. B. eine Form-Submit-Direktive, die nur an <button type=“submit”> Sinn ergibt: selector: ‘button[type=“submit”][appConfirm]‘.
Weiterführende Ressourcen
Externe Quellen
- Attribute Directives Guide – Angular.dev
- @Directive API – Angular.dev
- @HostBinding API – Angular.dev
- @HostListener API – Angular.dev
- Renderer2 API – Angular.dev