Direktiven erweitern HTML-Elemente mit zusätzlicher Funktionalität. Sie sind Anweisungen, die Angular beim Parsen des DOM erkennt und ausführt. Während Komponenten die Grundbausteine einer Angular-Anwendung bilden, ermöglichen Direktiven die Anreicherung dieser Bausteine mit Verhalten, Styling und strukturellen Änderungen.
Technisch gesehen sind Direktiven TypeScript-Klassen mit einem
@Directive-Dekorator, der Metadaten für Angular
bereitstellt. Der Selektor im Dekorator bestimmt, auf welche
DOM-Elemente die Direktive angewendet wird. Angular integriert
Direktiven tief in sein Change Detection-System, wodurch sie reaktiv auf
Zustandsänderungen reagieren können.
Der Verarbeitungsprozess läuft systematisch ab: Angular parsed die Templates, identifiziert Direktiven anhand ihrer Selektoren, erstellt Instanzen der Direktiven-Klassen, injiziert Dependencies und führt die Direktiven-Logik aus. Diese Integration in den Change Detection-Zyklus macht Direktiven zu einem zentralen Werkzeug für dynamische, reaktive Benutzeroberflächen.
Angular unterscheidet drei Direktiven-Typen, die jeweils spezifische Aufgaben erfüllen und unterschiedliche Aspekte der DOM-Manipulation adressieren.
| Direktiven-Typ | Erkennung | Hauptzweck | Beispiele |
|---|---|---|---|
| Komponenten | Eigener Tag | UI-Bausteine mit Template | <app-hero> |
| Attribut-Direktiven | Attribut ohne * |
Verhalten/Aussehen ändern | ngClass, ngStyle |
| Struktur-Direktiven | Attribut mit * oder @ |
DOM-Struktur manipulieren | *ngIf, @for |
Komponenten sind spezialisierte Direktiven mit eigenem Template und Styling. Sie bilden die Architektur der Anwendung. Attribut-Direktiven erweitern existierende Elemente ohne strukturelle Änderungen. Struktur-Direktiven manipulieren die DOM-Hierarchie durch Hinzufügen, Entfernen oder Anordnen von Elementen.
Attribut-Direktiven werden als HTML-Attribute ohne Sternchen-Präfix verwendet. Sie ändern Aussehen oder Verhalten ihrer Host-Elemente, ohne die DOM-Struktur zu verändern. Angular bietet mehrere eingebaute Attribut-Direktiven für häufige Anwendungsfälle.
Die ngClass-Direktive verwaltet CSS-Klassen dynamisch
basierend auf Ausdrücken. Sie akzeptiert verschiedene Eingabeformate,
wobei die Objekt-Syntax am gebräuchlichsten ist:
<div [ngClass]="{'active': isActive, 'disabled': isDisabled, 'highlighted': isSelected}">
Dynamischer Inhalt
</div>Jeder Schlüssel im Objekt ist ein Klassenname, der zugehörige Wert
ein boolescher Ausdruck. Angular fügt Klassen hinzu, deren Ausdruck
true ergibt, und entfernt Klassen bei false.
Die Array-Syntax eignet sich für dynamisch generierte
Klassensammlungen:
<div [ngClass]="['base-class', userRole === 'admin' ? 'admin-highlight' : '']">
Inhalt mit bedingten Klassen
</div>Komplexere Logik gehört in Methoden:
export class StatusComponent {
status: 'idle' | 'loading' | 'success' | 'error' = 'idle';
getStatusClasses() {
return {
'status-indicator': true,
'status-idle': this.status === 'idle',
'status-loading': this.status === 'loading',
'status-success': this.status === 'success',
'status-error': this.status === 'error'
};
}
}Im Template:
<div [ngClass]="getStatusClasses()">Status-Anzeige</div>Diese Kapselung verbessert Testbarkeit und Wartbarkeit, besonders bei komplexen Bedingungen.
Die ngStyle-Direktive wendet inline CSS-Styles dynamisch
an:
<div [ngStyle]="{'color': textColor, 'font-size': fontSize + 'px', 'font-weight': isBold ? 'bold' : 'normal'}">
Gestylter Text
</div>Auch hier vereinfachen Methoden komplexe Stil-Logik:
export class ThemeComponent {
isDarkTheme = false;
baseFontSize = 16;
fontScale = 1;
getThemeStyles() {
return {
'color': this.isDarkTheme ? '#ffffff' : '#333333',
'background-color': this.isDarkTheme ? '#333333' : '#ffffff',
'font-size': `${this.baseFontSize * this.fontScale}px`,
'padding': '12px',
'border-radius': '8px'
};
}
}Die Trennung von Stil-Logik und Template erhöht die Klarheit. Für produktive Anwendungen sind CSS-Klassen jedoch oft die bessere Wahl, da sie Caching durch den Browser ermöglichen und Performance-Vorteile bieten.
Die ngModel-Direktive implementiert bidirektionale
Datenbindung zwischen Formularelementen und Komponenten-Eigenschaften.
Sie erfordert den Import des FormsModule:
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-user-form',
standalone: true,
imports: [FormsModule],
template: `
<input [(ngModel)]="username" name="username" required>
<p>Eingegebener Wert: {{ username }}</p>
`
})
export class UserFormComponent {
username = '';
}Die Syntax [(ngModel)] ist syntaktischer Zucker für eine
Kombination aus Property Binding und Event Binding:
<input
[ngModel]="username"
(ngModelChange)="username = $event"
name="username">Diese explizite Form ermöglicht Zwischenverarbeitung:
<input
[ngModel]="username"
(ngModelChange)="onUsernameChange($event)"
name="username">onUsernameChange(value: string) {
this.username = value.toLowerCase().trim();
}Die Template-Referenz #varName="ngModel" gibt Zugriff
auf den Validierungsstatus:
<input
[(ngModel)]="email"
name="email"
required
email
#emailField="ngModel">
<div *ngIf="emailField.invalid && emailField.touched">
<span *ngIf="emailField.errors?.['required']">E-Mail erforderlich</span>
<span *ngIf="emailField.errors?.['email']">Ungültiges Format</span>
</div>Angular’s Formular-Integration nutzt diese Mechanismen für komplexe Validierung und State-Management.
Eigene Attribut-Direktiven kapseln wiederverwendbares Verhalten. Eine Highlight-Direktive demonstriert die Grundlagen:
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
@Input('appHighlight') highlightColor = 'yellow';
@Input() defaultColor = 'transparent';
constructor(private el: ElementRef) {
this.setColor(this.defaultColor);
}
@HostListener('mouseenter') onMouseEnter() {
this.setColor(this.highlightColor);
}
@HostListener('mouseleave') onMouseLeave() {
this.setColor(this.defaultColor);
}
private setColor(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}Der @HostListener-Dekorator bindet an DOM-Events des
Host-Elements. ElementRef bietet direkten Zugriff auf das
native DOM-Element. Die Verwendung ist intuitiv:
<p appHighlight="lightblue">Text mit Hover-Effekt</p>
<p [appHighlight]="dynamicColor" [defaultColor]="'lightgray'">Dynamisch hervorgehoben</p>Für produktiven Code sollte Renderer2 statt direkter
DOM-Manipulation verwendet werden:
import { Directive, ElementRef, Renderer2, HostListener } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
constructor(
private el: ElementRef,
private renderer: Renderer2
) {}
@HostListener('mouseenter')
highlight() {
this.renderer.setStyle(this.el.nativeElement, 'backgroundColor', 'yellow');
}
@HostListener('mouseleave')
reset() {
this.renderer.removeStyle(this.el.nativeElement, 'backgroundColor');
}
}Renderer2 ist sicher für Server-Side Rendering und Web
Workers, da es DOM-Operationen abstrahiert.
Struktur-Direktiven ändern die DOM-Hierarchie durch Hinzufügen,
Entfernen oder Neuanordnen von Elementen. Traditionell werden sie mit
einem Sternchen-Präfix gekennzeichnet, das Angular in
<ng-template>-Konstrukte transformiert:
<div *ngIf="isVisible">Inhalt</div>wird zu:
<ng-template [ngIf]="isVisible">
<div>Inhalt</div>
</ng-template>Diese Template-Syntax verleiht Struktur-Direktiven ihre Fähigkeit, Elemente vollständig aus dem DOM zu entfernen, nicht nur zu verstecken.
Die ngIf-Direktive fügt Elemente bedingt zum DOM hinzu
oder entfernt sie vollständig:
<div *ngIf="userLoggedIn">
Willkommen zurück, {{ userName }}!
</div>Der Unterschied zu [hidden] ist fundamental:
ngIf entfernt das Element aus dem DOM, während
hidden es nur visuell versteckt. Dies hat
Performance-Implikationen bei komplexen UI-Elementen und
Sicherheitsvorteile bei sensiblen Inhalten.
Die else-Syntax bietet Alternative:
<div *ngIf="userLoggedIn; else loginPrompt">
Willkommen, {{ userName }}!
</div>
<ng-template #loginPrompt>
<p>Bitte anmelden</p>
<button (click)="login()">Login</button>
</ng-template>Für komplexe Szenarien unterstützt ngIf sowohl then als
auch else:
<div *ngIf="dataLoaded; then contentView else loadingView"></div>
<ng-template #contentView>
<app-data-display [data]="data"></app-data-display>
</ng-template>
<ng-template #loadingView>
<app-loading-spinner></app-loading-spinner>
</ng-template>Diese Syntax separiert Zustandslogik von UI-Struktur.
Die ngFor-Direktive iteriert über Arrays und rendert
Templates für jedes Element:
<ul>
<li *ngFor="let item of items; index as i">
{{ i + 1 }}. {{ item.name }}
</li>
</ul>Angular stellt lokale Variablen während der Iteration bereit:
<div *ngFor="let item of items;
index as i;
first as isFirst;
last as isLast;
even as isEven;
odd as isOdd"
[ngClass]="{'first-item': isFirst, 'last-item': isLast, 'stripe': isOdd}">
{{ i }}. {{ item.name }}
</div>Die trackBy-Funktion ist essentiell für Performance bei
dynamischen Listen:
export class ListComponent {
items: Item[] = [];
trackByFn(index: number, item: Item): number {
return item.id;
}
}Im Template:
<div *ngFor="let item of items; trackBy: trackByFn">
{{ item.name }}
</div>Ohne trackBy zerstört und erstellt Angular bei jeder
Listenänderung alle DOM-Elemente neu. Mit trackBy
identifiziert Angular unveränderte Elemente anhand ihrer ID und behält
sie bei. Bei Listen mit Hunderten Elementen ist der
Performance-Unterschied erheblich.
Die ngSwitch-Direktive bietet elegante
Mehrfachverzweigung:
<div [ngSwitch]="userRole">
<div *ngSwitchCase="'admin'">
<h2>Admin Dashboard</h2>
<p>Vollzugriff auf alle Funktionen</p>
</div>
<div *ngSwitchCase="'editor'">
<h2>Editor-Werkzeuge</h2>
<p>Inhalte bearbeiten und veröffentlichen</p>
</div>
<div *ngSwitchDefault>
<h2>Benutzer-Ansicht</h2>
<p>Standard-Benutzeroberfläche</p>
</div>
</div>Angular wertet den ngSwitch-Ausdruck aus und rendert nur
den passenden Fall. Im Gegensatz zu mehreren
ngIf-Direktiven kann ngSwitch immer nur einen
Fall gleichzeitig anzeigen.
Angular 17 führte eine überarbeitete Syntax für Kontrollstrukturen
ein. Die @-basierte Syntax ersetzt die Sternchen-Direktiven
durch lesbarere, typsicherere Alternativen.
Die neue Syntax ist direkter und unterstützt native else-if-Konstrukte:
@if (userLoggedIn) {
<div>Willkommen, {{ userName }}!</div>
} @else {
<p>Bitte anmelden</p>
<button (click)="login()">Login</button>
}Mehrfachbedingungen ohne verschachtelte Templates:
@if (userRole === 'admin') {
<admin-dashboard />
} @else if (userRole === 'editor') {
<editor-tools />
} @else {
<user-view />
}Die geschweiften Klammern machen Blockgrenzen explizit, die IDE-Integration ist besser, und TypeScript kann Typen effektiver prüfen.
Die neue For-Syntax macht Tracking obligatorisch und bietet einen integrierten Empty-State:
<ul>
@for (item of items; track item.id) {
<li>{{ item.name }}</li>
} @empty {
<li class="no-data">Keine Einträge vorhanden</li>
}
</ul>Lokale Variablen nutzen Präfix-Syntax:
@for (item of items; track item.id; let i = $index, isOdd = $odd) {
<div [class.highlight]="isOdd">
{{ i + 1 }}. {{ item.name }}
</div>
}Verfügbare Variablen: $index, $first,
$last, $even, $odd,
$count. Das obligatorische track-Keyword
erzwingt Best Practices von Beginn an.
Die Switch-Syntax ist konsistent mit @if und
@for:
@switch (userRole) {
@case ('admin') {
<h2>Admin Dashboard</h2>
}
@case ('editor') {
<h2>Editor Werkzeuge</h2>
}
@default {
<h2>Benutzer-Ansicht</h2>
}
}Die visuelle Strukturierung ist klarer als bei der Template-basierten Variante.
Die @defer-Direktive ermöglicht verzögertes Laden von
Template-Teilen basierend auf verschiedenen Triggern:
@defer (on viewport) {
<heavy-component />
} @loading {
<p>Lade Komponente...</p>
} @error {
<p>Fehler beim Laden</p>
}Verfügbare Trigger:
| Trigger | Beschreibung | Verwendung |
|---|---|---|
on viewport |
Lädt bei Sichtbarkeit | Unterhalb des Folds |
on interaction |
Lädt bei Interaktion | Click-to-Load |
on hover |
Lädt beim Hovern | Hover-to-Load |
on idle |
Lädt im Browser-Leerlauf | Nicht-kritische UI |
on timer(ms) |
Lädt nach Timeout | Verzögerte Anzeige |
when condition |
Lädt bei Bedingung | Bedingte Features |
Ein praktisches Beispiel zeigt die Strategie:
<article class="product-detail">
<!-- Kritischer Content: sofort laden -->
<header>
<h1>{{ product.name }}</h1>
<p class="price">{{ product.price | currency }}</p>
<button (click)="addToCart()">In den Warenkorb</button>
</header>
<!-- Detailinfos: bei Sichtbarkeit laden -->
@defer (on viewport) {
<section class="details">
<app-product-specs [specs]="product.specs"></app-product-specs>
</section>
} @loading {
<p>Lade Details...</p>
}
<!-- Bewertungen: bei Interaktion laden -->
@defer (on interaction(reviewBtn)) {
<section class="reviews">
<app-reviews [productId]="product.id"></app-reviews>
</section>
} @placeholder {
<button #reviewBtn>Bewertungen anzeigen</button>
}
<!-- Empfehlungen: im Leerlauf laden -->
@defer (on idle) {
<app-recommendations [productId]="product.id"></app-recommendations>
}
</article>Diese Strategie priorisiert kritischen Content, lädt Details bei Bedarf und nutzt Browser-Leerlauf für nicht-essentielle Features. Die Initial Load Time sinkt drastisch.
Eigene Struktur-Direktiven erfordern TemplateRef und
ViewContainerRef. Eine Unless-Direktive als Gegenstück zu
ngIf:
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[appUnless]',
standalone: true
})
export class UnlessDirective {
private hasView = false;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) {}
@Input() set appUnless(condition: boolean) {
if (!condition && !this.hasView) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
} else if (condition && this.hasView) {
this.viewContainer.clear();
this.hasView = false;
}
}
}Verwendung:
<div *appUnless="isHidden">
Nur sichtbar wenn isHidden false ist
</div>Der TemplateRef repräsentiert das Template,
ViewContainerRef den Container, in den das Template
gerendert wird. Die createEmbeddedView-Methode fügt das
Template hinzu, clear() entfernt alle Views.
Eine komplexere Repeat-Direktive zeigt erweiterte Möglichkeiten:
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[appRepeat]',
standalone: true
})
export class RepeatDirective {
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) {}
@Input() set appRepeat(count: number) {
this.viewContainer.clear();
for (let i = 0; i < count; i++) {
this.viewContainer.createEmbeddedView(this.templateRef, {
$implicit: i,
index: i,
count: count,
first: i === 0,
last: i === count - 1
});
}
}
}Die Verwendung mit Context-Variablen:
<div *appRepeat="5; let i; let isFirst = first; let isLast = last">
Item {{ i + 1 }}
<span *ngIf="isFirst">(Erster)</span>
<span *ngIf="isLast">(Letzter)</span>
</div>Das Context-Objekt ermöglicht Datenübergabe vom Template zur
Direktive. Die $implicit-Eigenschaft bindet an die
let-Variable ohne explizite Zuweisung.
Angular Signals bieten feinkörnigere Reaktivität als traditionelle Change Detection. Direktiven können Signals nutzen für effizientere Updates:
import { Directive, ElementRef, signal, computed, input, effect } from '@angular/core';
@Directive({
selector: '[appResizable]',
standalone: true,
host: {
'[style.resize]': '"both"',
'[style.overflow]': '"auto"',
'[style.min-width.px]': 'minWidth()',
'[style.min-height.px]': 'minHeight()'
}
})
export class ResizableDirective {
minWidth = input<number>(100);
minHeight = input<number>(100);
private width = signal(0);
private height = signal(0);
currentSize = computed(() => ({
width: this.width(),
height: this.height()
}));
constructor(private el: ElementRef) {
effect(() => {
const size = this.currentSize();
console.log(`Neue Größe: ${size.width}x${size.height}`);
});
}
}Signal-basierte Inputs mit input(), interne Signals mit
signal(), berechnete Werte mit computed() und
Reaktionen mit effect(). Angular aktualisiert nur die
tatsächlich betroffenen Teile der Anwendung, nicht den gesamten
Komponentenbaum.
Die host-Eigenschaft im Dekorator bindet
Direktiven-Properties direkt an Host-Attribute und -Styles ohne
@HostBinding:
@Directive({
selector: '[appTooltip]',
standalone: true,
host: {
'(mouseenter)': 'show()',
'(mouseleave)': 'hide()',
'[class.has-tooltip]': 'true',
'[attr.aria-describedby]': 'tooltipId()'
}
})
export class TooltipDirective {
tooltipId = signal('tooltip-' + Math.random().toString(36).substr(2, 9));
show() {
console.log('Tooltip anzeigen');
}
hide() {
console.log('Tooltip verstecken');
}
}Diese deklarative Syntax ist kompakter als mehrere
@HostListener und @HostBinding
Dekoratoren.
Direktiven können RxJS-Streams für komplexe asynchrone Logik nutzen:
import { Directive, ElementRef, Output, EventEmitter, inject } from '@angular/core';
import { fromEvent } from 'rxjs';
import { throttleTime, map, pairwise, filter } from 'rxjs/operators';
interface ScrollInfo {
direction: 'up' | 'down';
position: number;
delta: number;
}
@Directive({
selector: '[appScrollObserve]',
standalone: true
})
export class ScrollObserveDirective {
private el = inject(ElementRef);
@Output() scrollChange = new EventEmitter<ScrollInfo>();
constructor() {
fromEvent(this.el.nativeElement, 'scroll')
.pipe(
throttleTime(100),
map(() => this.el.nativeElement.scrollTop),
pairwise(),
map(([prev, curr]) => ({
direction: prev < curr ? 'down' as const : 'up' as const,
position: curr,
delta: Math.abs(curr - prev)
})),
filter(info => info.delta > 0)
)
.subscribe(info => this.scrollChange.emit(info));
}
}Der RxJS-Stream throttled Scroll-Events, berechnet Position und Richtung, filtert insignifikante Änderungen und emittiert strukturierte Daten. Die Verwendung:
<div class="content" appScrollObserve (scrollChange)="onScroll($event)">
<!-- Scrollbarer Inhalt -->
</div>onScroll(info: ScrollInfo) {
if (info.direction === 'down' && info.position > 300) {
this.hideHeader();
} else if (info.direction === 'up') {
this.showHeader();
}
}Diese Komposition aus Direktiven und RxJS ermöglicht deklarative, wiederverwendbare Event-Handling-Logik.