Direktiven sind ein Kernkonzept in Angular und stellen eines der mächtigsten Features des Frameworks dar. Sie ermöglichen es, HTML-Elemente mit zusätzlicher Funktionalität anzureichern, ihr Verhalten zu modifizieren und die DOM-Struktur dynamisch zu manipulieren. In diesem erweiterten Guide werden wir tiefer in die Welt der Angular-Direktiven eintauchen und ihre Funktionsweise, Anwendungsfälle und Best Practices ausführlich beleuchten.
Im Wesentlichen sind Direktiven Anweisungen im DOM (Document Object Model). Angular scannt das DOM nach Direktiven und wendet die entsprechenden Verhaltensweisen auf die Elemente an, bei denen diese Direktiven gefunden werden.
Auf technischer Ebene sind Direktiven TypeScript-Klassen, die mit
einem @Directive-Dekorator versehen sind. Dieser Dekorator
enthält Metadaten, die Angular mitteilen, wie die Direktive angewendet
werden soll. Der Selektor in diesen Metadaten bestimmt, auf welche
DOM-Elemente die Direktive angewendet wird.
Die Direktiven-Klassen selbst können verschiedene Angular-Mechanismen nutzen: - Abhängigkeitsinjektionssystem, um Services und andere Abhängigkeiten zu erhalten - Lifecycle Hooks, um auf verschiedene Phasen im Lebenszyklus der Direktive zu reagieren - Input- und Output-Properties für die Kommunikation mit anderen Komponenten
Wenn Angular eine Anwendung rendert, geht es folgendermaßen vor:
Dieser Prozess ist Teil des Angular Change Detection Cycle und ermöglicht die dynamische, reaktive Natur von Angular-Anwendungen.
Angular unterscheidet zwischen drei Haupttypen von Direktiven. Jeder Typ hat seine eigene Rolle im Framework und seine eigenen Besonderheiten.
Komponenten sind spezialisierte Direktiven mit Vorlagen (Templates). Sie bilden den Grundbaustein einer Angular-Anwendung und besitzen eine eigene Datei für die Vorlage und die Stilinformationen.
Eine vollständige Angular-Komponente besteht aus mehreren Teilen:
Beispiel einer ausführlicheren Komponente:
// hero-detail.component.ts
import { Component, Input, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Hero } from '../models/hero.model';
import { HeroService } from '../services/hero.service';
@Component({
selector: 'app-hero-detail',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div *ngIf="hero" class="hero-detail">
<h2>{{ hero.name | uppercase }} Details</h2>
<div class="hero-id">
<span>ID: </span>{{ hero.id }}
</div>
<div class="form-group">
<label for="hero-name">Hero name: </label>
<input id="hero-name" [(ngModel)]="hero.name" placeholder="Name">
</div>
<div class="hero-powers">
<h3>Powers:</h3>
<ul>
<li *ngFor="let power of hero.powers">{{ power }}</li>
</ul>
</div>
<div class="actions">
<button (click)="save()">Save</button>
<button (click)="goBack()">Go Back</button>
</div>
</div>
<div *ngIf="!hero" class="loading">
Loading hero...
</div>
`,
styles: [`
.hero-detail {
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
margin-bottom: 10px;
background-color: #f8f9fa;
}
.hero-id {
color: #6c757d;
margin-bottom: 15px;
}
.form-group {
margin-bottom: 15px;
}
input {
padding: 8px;
border: 1px solid #ced4da;
border-radius: 4px;
width: 100%;
max-width: 300px;
}
.actions button {
margin-right: 10px;
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.actions button:hover {
background-color: #0069d9;
}
.loading {
font-style: italic;
color: #6c757d;
}
`]
})
export class HeroDetailComponent implements OnInit {
@Input() hero?: Hero;
// Moderne Dependency Injection mit inject-Funktion (seit Angular 14)
private heroService = inject(HeroService);
ngOnInit(): void {
// Initialisierungslogik, z.B. Laden von Daten
if (!this.hero && this.heroId) {
this.loadHero();
}
}
private heroId?: number;
@Input() set id(id: number) {
this.heroId = id;
if (id) {
this.loadHero();
}
}
private loadHero(): void {
if (this.heroId) {
this.heroService.getHero(this.heroId).subscribe(
hero => this.hero = hero
);
}
}
save(): void {
if (this.hero) {
this.heroService.updateHero(this.hero).subscribe(() => {
// Nach dem Speichern Erfolgsbenachrichtigung anzeigen oder navigieren
this.goBack();
});
}
}
goBack(): void {
// Navigationslogik
window.history.back();
}
}In neueren Angular-Versionen (ab Angular 14, vollständig unterstützt seit Angular 15) können Komponenten als “standalone” konfiguriert werden. Dies bringt mehrere Vorteile:
Verbesserte Modularität: Standalone-Komponenten deklarieren ihre eigenen Abhängigkeiten direkt, ohne ein umschließendes Modul zu benötigen.
Einfachere Wiederverwendbarkeit: Sie können ohne zusätzliche Module-Konfiguration in andere Teile der Anwendung importiert werden.
Tree-Shakable: Nicht verwendete Komponenten werden beim Build automatisch entfernt, was zu kleineren Bundle-Größen führt.
Graduelle Migration: Bestehende Anwendungen können schrittweise zu Standalone-Komponenten migrieren.
Vergleich: Traditionelle vs. Standalone-Komponente:
Traditionell (mit NgModule):
// hero.component.ts
@Component({
selector: 'app-hero',
templateUrl: './hero.component.html',
styleUrls: ['./hero.component.css']
})
export class HeroComponent { ... }
// hero.module.ts
@NgModule({
declarations: [HeroComponent],
imports: [CommonModule],
exports: [HeroComponent]
})
export class HeroModule { }Standalone-Komponente:
// hero.component.ts
@Component({
selector: 'app-hero',
standalone: true,
imports: [CommonModule],
templateUrl: './hero.component.html',
styleUrls: ['./hero.component.css']
})
export class HeroComponent { ... }Der Standalone-Ansatz reduziert den Boilerplate-Code und macht die Abhängigkeiten einer Komponente expliziter und transparenter.
Attribut-Direktiven ändern das Aussehen oder Verhalten von DOM-Elementen. Sie werden als Attribute (ohne Sternchen) an HTML-Elementen angewendet.
Angular bietet mehrere vordefinierte Attribut-Direktiven, die häufig verwendete Funktionalitäten abdecken:
ngClass: Dynamische CSS-Klassenverwaltung
Diese Direktive ermöglicht es, CSS-Klassen basierend auf Ausdrücken dynamisch hinzuzufügen oder zu entfernen. Sie akzeptiert verschiedene Syntaxen:
<div [ngClass]="{'active': isActive, 'disabled': isDisabled, 'highlighted': isHighlighted}">
Dynamische Klassen
</div>Jeder Schlüssel im Objekt ist ein Klassenname, und der zugehörige Wert ist ein boolescher Ausdruck, der bestimmt, ob die Klasse angewendet wird.
<div [ngClass]="['base-class', condition ? 'conditional-class' : '']">
Dynamische Klassen mit Array-Syntax
</div>Diese Syntax ist nützlich, wenn die Klassensammlung dynamisch erstellt wird.
<div [ngClass]="classExpression">
Klassen aus String-Variable
</div>Wobei classExpression eine durch Leerzeichen getrennte
Liste von Klassennamen ist.
<div [ngClass]="getClassMap()">
Klassen aus Methode
</div>getClassMap(): Record<string, boolean> {
return {
'active': this.isActive,
'disabled': this.isDisabled,
'highlighted': this.isFirstItem || this.isSelected,
'large-text': this.fontSize > 16
};
}Diese Syntax ist nützlich, wenn die Klassenlogik komplexer ist und in der Komponente gekapselt werden sollte.
ngStyle: Inline-Stile dynamisch verwalten
ngStyle ermöglicht die dynamische Anwendung von inline CSS-Stilen:
<div [ngStyle]="{'color': textColor, 'font-size': fontSize + 'px', 'font-weight': isBold ? 'bold' : 'normal'}">
Text mit dynamischen Stilen
</div>Auch hier können Methoden verwendet werden, um die Stillogik zu kapseln:
<div [ngStyle]="getStyles()">
Stile aus Methode
</div>getStyles(): Record<string, string> {
return {
'color': this.isDarkTheme ? '#ffffff' : '#333333',
'background-color': this.isDarkTheme ? '#333333' : '#ffffff',
'font-size': `${this.baseFontSize * this.fontScale}px`,
'padding': this.compact ? '4px' : '12px',
'border-radius': this.rounded ? '8px' : '0'
};
}ngModel: Zwei-Wege-Datenbindung
ngModel implementiert die Zwei-Wege-Datenbindung zwischen Formularelementen und Komponenteneigenschaften. Es benötigt das FormsModule, um zu funktionieren:
// component.ts
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-input-example',
standalone: true,
imports: [FormsModule],
template: `
<form (ngSubmit)="onSubmit()">
<div>
<label for="username">Benutzername:</label>
<input
id="username"
[(ngModel)]="username"
name="username"
required
minlength="3"
#usernameModel="ngModel">
<div *ngIf="usernameModel.invalid && (usernameModel.dirty || usernameModel.touched)">
<div *ngIf="usernameModel.errors?.['required']">
Benutzername ist erforderlich.
</div>
<div *ngIf="usernameModel.errors?.['minlength']">
Benutzername muss mindestens 3 Zeichen lang sein.
</div>
</div>
</div>
<div>
<label for="email">E-Mail:</label>
<input
id="email"
[(ngModel)]="email"
name="email"
required
email
#emailModel="ngModel">
<div *ngIf="emailModel.invalid && (emailModel.dirty || emailModel.touched)">
<div *ngIf="emailModel.errors?.['required']">
E-Mail ist erforderlich.
</div>
<div *ngIf="emailModel.errors?.['email']">
Bitte geben Sie eine gültige E-Mail-Adresse ein.
</div>
</div>
</div>
<button type="submit" [disabled]="usernameModel.invalid || emailModel.invalid">
Registrieren
</button>
</form>
<div *ngIf="submitted">
<h3>Registrierungsdetails:</h3>
<p>Benutzername: {{ username }}</p>
<p>E-Mail: {{ email }}</p>
</div>
`
})
export class InputExampleComponent {
username = '';
email = '';
submitted = false;
onSubmit(): void {
this.submitted = true;
console.log('Form submitted', { username: this.username, email: this.email });
// Hier würde normalerweise ein API-Aufruf erfolgen
}
}In diesem Beispiel sehen wir folgendes:
[(ngModel)]-Syntax (oft als “Banana in a Box”
bezeichnet) stellt die Zwei-Wege-Bindung hername="username"), was für Formularvalidierung wichtig
ist#usernameModel="ngModel" gibt Zugriff auf das
NgModel-DirektivenobjektusernameModel.errors
abgerufen werdenDie [(ngModel)]-Syntax ist eigentlich eine Kombination
aus zwei separaten Bindungen: - Die Property-Bindung
[ngModel]="username" bindet den Wert an das Eingabefeld -
Die Event-Bindung (ngModelChange)="username=$event"
aktualisiert die Eigenschaft, wenn sich der Wert ändert
Diese kompakte Syntax kann auch explizit geschrieben werden:
<input
[ngModel]="username"
(ngModelChange)="username = $event"
name="username">Diese explizite Form kann nützlich sein, wenn Sie vor dem Aktualisieren der Eigenschaft zusätzliche Logik ausführen möchten:
<input
[ngModel]="username"
(ngModelChange)="onUsernameChange($event)"
name="username">onUsernameChange(newValue: string): void {
// Formatierung oder Validierung durchführen
this.username = newValue.toLowerCase().trim();
}Die Erstellung eigener Attribut-Direktiven ermöglicht es, wiederverwendbare Verhaltensweisen zu definieren, die auf verschiedene Elemente angewendet werden können. Sehen wir uns den Prozess im Detail an:
Schritt 1: Struktur erstellen
Zunächst erstellen wir die Grundstruktur der Direktive mit dem Angular CLI:
ng generate directive highlightDies erzeugt folgende Datei:
// highlight.directive.ts
import { Directive, ElementRef } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
constructor(private el: ElementRef) { }
}Schritt 2: Funktionalität hinzufügen
Nun fügen wir die gewünschte Funktionalität hinzu, indem wir HostListener-Dekoratoren verwenden, um auf DOM-Ereignisse zu reagieren:
// highlight.directive.ts
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.setBackgroundColor(this.defaultColor);
}
@HostListener('mouseenter') onMouseEnter() {
this.setBackgroundColor(this.highlightColor);
}
@HostListener('mouseleave') onMouseLeave() {
this.setBackgroundColor(this.defaultColor);
}
private setBackgroundColor(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}Schritt 3: Direktive verwenden
Jetzt können wir unsere Direktive in Templates verwenden:
<!-- Einfache Verwendung -->
<p appHighlight>
Bewege den Mauszeiger über mich, um die Hintergrundfarbe zu ändern.
</p>
<!-- Mit Parameter -->
<p appHighlight="lightblue" defaultColor="#f0f0f0">
Bewege den Mauszeiger über mich, um die Hintergrundfarbe zu ändern.
</p>Schritt 4: Komplexere Direktive mit Renderer2
Für fortgeschrittenere DOM-Manipulationen ist es besser, den Renderer2-Service zu verwenden, anstatt direkt auf nativeElement zuzugreifen:
// tooltip.directive.ts
import { Directive, ElementRef, HostListener, Input, OnDestroy, Renderer2 } from '@angular/core';
@Directive({
selector: '[appTooltip]',
standalone: true
})
export class TooltipDirective implements OnDestroy {
@Input('appTooltip') tooltipText = '';
@Input() tooltipPosition: 'top' | 'right' | 'bottom' | 'left' = 'top';
private tooltipElement: HTMLElement | null = null;
constructor(private el: ElementRef, private renderer: Renderer2) {}
@HostListener('mouseenter') onMouseEnter() {
this.showTooltip();
}
@HostListener('mouseleave') onMouseLeave() {
this.hideTooltip();
}
private showTooltip() {
if (this.tooltipElement) {
return; // Tooltip bereits sichtbar
}
// Tooltip-Element erstellen
this.tooltipElement = this.renderer.createElement('div');
this.renderer.addClass(this.tooltipElement, 'tooltip');
this.renderer.addClass(this.tooltipElement, `tooltip-${this.tooltipPosition}`);
// Text hinzufügen
const text = this.renderer.createText(this.tooltipText);
this.renderer.appendChild(this.tooltipElement, text);
// Zum DOM hinzufügen
this.renderer.appendChild(document.body, this.tooltipElement);
// Position berechnen und setzen
this.setTooltipPosition();
}
private setTooltipPosition() {
if (!this.tooltipElement) return;
const hostPos = this.el.nativeElement.getBoundingClientRect();
const tooltipPos = this.tooltipElement.getBoundingClientRect();
let top = 0, left = 0;
switch (this.tooltipPosition) {
case 'top':
top = hostPos.top - tooltipPos.height - 10;
left = hostPos.left + (hostPos.width - tooltipPos.width) / 2;
break;
case 'right':
top = hostPos.top + (hostPos.height - tooltipPos.height) / 2;
left = hostPos.right + 10;
break;
case 'bottom':
top = hostPos.bottom + 10;
left = hostPos.left + (hostPos.width - tooltipPos.width) / 2;
break;
case 'left':
top = hostPos.top + (hostPos.height - tooltipPos.height) / 2;
left = hostPos.left - tooltipPos.width - 10;
break;
}
// Scroll-Position berücksichtigen
top += window.scrollY;
left += window.scrollX;
// Styles setzen
this.renderer.setStyle(this.tooltipElement, 'top', `${top}px`);
this.renderer.setStyle(this.tooltipElement, 'left', `${left}px`);
}
private hideTooltip() {
if (this.tooltipElement) {
this.renderer.removeChild(document.body, this.tooltipElement);
this.tooltipElement = null;
}
}
ngOnDestroy() {
// Aufräumen, um Memory-Leaks zu vermeiden
this.hideTooltip();
}
}Diese komplexere Direktive erzeugt ein Tooltip, das an verschiedenen Positionen um das Host-Element herum angezeigt werden kann. Die Verwendung von Renderer2 ist sicherer bei serverseitigem Rendering und in Web Workers.
Verwendung:
<button
appTooltip="Klicken Sie hier, um die Aktion auszuführen"
tooltipPosition="right">
Aktion ausführen
</button>Um diese Direktive vollständig zu nutzen, müssten wir auch entsprechende CSS-Styles definieren:
.tooltip {
position: absolute;
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 14px;
z-index: 1000;
pointer-events: none;
transition: opacity 0.3s;
}
.tooltip::after {
content: '';
position: absolute;
border-width: 5px;
border-style: solid;
}
.tooltip-top::after {
top: 100%;
left: 50%;
margin-left: -5px;
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
}
.tooltip-right::after {
top: 50%;
left: 0;
margin-top: -5px;
margin-left: -5px;
border-color: transparent rgba(0, 0, 0, 0.8) transparent transparent;
}
.tooltip-bottom::after {
bottom: 100%;
left: 50%;
margin-left: -5px;
border-color: transparent transparent rgba(0, 0, 0, 0.8) transparent;
}
.tooltip-left::after {
top: 50%;
right: 0;
margin-top: -5px;
margin-right: -5px;
border-color: transparent transparent transparent rgba(0, 0, 0, 0.8);
}Struktur-Direktiven modifizieren die DOM-Struktur, indem sie Elemente
hinzufügen, entfernen oder manipulieren. Sie werden durch ein
Asterisk-Präfix (*) gekennzeichnet, das eine syntaktische Abkürzung für
die Verwendung eines <ng-template> darstellt.
Wenn Sie eine Strukturdirektive wie *ngIf verwenden,
transformiert Angular dies in eine
<ng-template>-Syntax. Zum Beispiel wird:
<div *ngIf="isVisible">Inhalt</div>von Angular in folgendes umgewandelt:
<ng-template [ngIf]="isVisible">
<div>Inhalt</div>
</ng-template>Diese Template-basierte Implementierung gibt Angular-Strukturdirektiven ihre Fähigkeit, Elemente dynamisch zum DOM hinzuzufügen oder daraus zu entfernen.
ngIf: Bedingte Anzeige von Elementen
Die ngIf-Direktive ist eine der am häufigsten verwendeten Strukturdirektiven in Angular. Sie fügt ein Element bedingt zum DOM hinzu oder entfernt es:
<div *ngIf="isVisible">Dieser Text wird nur angezeigt, wenn isVisible true ist.</div>Eine wichtige Sache zu verstehen: Wenn die Bedingung
false ist, wird das Element nicht nur versteckt (wie bei
[hidden]="!isVisible"), sondern vollständig aus dem DOM
entfernt. Dies hat mehrere Konsequenzen:
ngIf mit else-Bedingung:
<div *ngIf="userLoggedIn; else loginTemplate">
Willkommen zurück, {{ userName }}!
</div>
<ng-template #loginTemplate>
<p>Bitte melden Sie sich an, um fortzufahren.</p>
<button (click)="login()">Login</button>
</ng-template>ngIf mit then und else:
<div *ngIf="dataLoaded; then dataTemplate else loadingTemplate"></div>
<ng-template #dataTemplate>
<app-data-view [data]="data"></app-data-view>
</ng-template>
<ng-template #loadingTemplate>
<app-loading-spinner></app-loading-spinner>
</ng-template>Diese Syntax ist besonders nützlich, wenn sowohl der “true” als auch der “false” Zustand komplexe UI-Elemente enthalten.
ngFor: Iteration über Sammlungen
Die ngFor-Direktive ermöglicht die Iteration über Arrays, Listen oder andere iterierbare Objekte:
<ul>
<li *ngFor="let item of items; index as i; trackBy: trackByFn">
{{ i + 1 }}. {{ item.name }}
</li>
</ul>ngFor bietet mehrere lokale Variablen, die während der Iteration verfügbar sind:
index: Der aktuelle Index des Elements in der
Iterationfirst: Boolean, der angibt, ob das aktuelle Element das
erste in der Iteration istlast: Boolean, der angibt, ob das aktuelle Element das
letzte in der Iteration isteven: Boolean, der angibt, ob der aktuelle Index eine
gerade Zahl istodd: Boolean, der angibt, ob der aktuelle Index eine
ungerade Zahl istDiese Variablen können mit der “as”-Syntax für zusätzliche Kontrolle verwendet werden:
<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, 'even-row': isEven, 'odd-row': isOdd}">
{{ i + 1 }}. {{ item.name }}
</div>trackBy: Performance-Optimierung für ngFor
Bei großen Listen kann die Verwendung einer trackBy-Funktion die Performance erheblich verbessern. Ohne trackBy erstellt Angular bei jeder Änderung der Liste alle DOM-Elemente neu. Mit trackBy kann Angular identifizieren, welche Elemente unverändert bleiben:
trackByFn(index: number, item: any): number {
return item.id; // Verwende eine eindeutige und stabile ID
}Performance-Vergleich mit und ohne trackBy:
Ohne trackBy: 1. Liste mit 100 Elementen wird gerendert 2. Ein Element wird zur Liste hinzugefügt 3. Angular zerstört alle 100 DOM-Elemente und erstellt 101 neue
Mit trackBy: 1. Liste mit 100 Elementen wird gerendert 2. Ein Element wird zur Liste hinzugefügt 3. Angular behält die 100 bestehenden DOM-Elemente bei und erstellt nur 1 neues Element
Der Performance-Unterschied wird bei großen Listen und häufigen Aktualisierungen besonders deutlich. Es ist eine Best Practice, immer eine trackBy-Funktion für Arrays mit mehr als ein paar Dutzend Elementen zu implementieren.
ngSwitch: Bedingte Mehrfachverzweigung
Die ngSwitch-Direktive bietet eine elegante Lösung für bedingte Mehrfachverzweigungen und ist eine Alternative zu verschachtelten ngIf-Anweisungen:
<div [ngSwitch]="userRole">
<div *ngSwitchCase="'admin'">
<h2>Admin Dashboard</h2>
<p>Hier können Sie alle Systemeinstellungen verwalten.</p>
</div>
<div *ngSwitchCase="'editor'">
<h2>Editor Werkzeuge</h2>
<p>Hier können Sie Inhalte bearbeiten und veröffentlichen.</p>
</div>
<div *ngSwitchDefault>
<h2>Benutzer-Ansicht</h2>
<p>Willkommen in Ihrem persönlichen Bereich.</p>
</div>
</div>Unter der Haube: 1. Angular wertet den Ausdruck
[ngSwitch]="userRole" aus 2. Es vergleicht diesen Wert mit
den verschiedenen *ngSwitchCase-Ausdrücken 3. Wenn ein
passender Fall gefunden wird, wird nur dieses Element gerendert 4. Wenn
kein passender Fall gefunden wird, wird das
*ngSwitchDefault-Element gerendert (falls vorhanden)
Ein wichtiger Unterschied zu ngIf: ngSwitch kann immer nur einen der Fälle anzeigen, während mehrere ngIf-Direktiven gleichzeitig true sein können.
Mit Angular 17 wurde eine komplett überarbeitete Syntax für strukturelle Direktiven eingeführt. Diese neue Control Flow Syntax bietet eine modernere, lesbarere und typsicherere Alternative zu den klassischen Strukturdirektiven mit Sternchen-Präfix.
**@if: Die moderne Alternative zu *ngIf**
@if (isVisible) {
<div>Dieser Text wird nur angezeigt, wenn isVisible true ist.</div>
}
<!-- Mit Else-Bedingung -->
@if (userLoggedIn) {
<div>Willkommen zurück, {{ userName }}!</div>
} @else {
<p>Bitte melden Sie sich an, um fortzufahren.</p>
<button (click)="login()">Login</button>
}
<!-- Mit mehreren Bedingungen -->
@if (userRole === 'admin') {
<admin-dashboard />
} @else if (userRole === 'editor') {
<editor-tools />
} @else {
<user-view />
}Die Vorteile dieser Syntax im Vergleich zu *ngIf: 1. Verbesserte Lesbarkeit: Die geschweiften Klammern machen die Blockstruktur klarer 2. Nativer else-if-Support: Keine Notwendigkeit für verschachtelte Templates 3. Typsicherheit: Bessere TypeScript-Integration und Typprüfung 4. Verbesserte IDE-Unterstützung: Bessere Code-Vervollständigung und Syntaxhervorhebung
**@for: Die moderne Alternative zu *ngFor**
<ul>
@for (item of items; track item.id; let i = $index, isOdd = $odd) {
<li [ngClass]="{'odd-row': isOdd}">
{{ i + 1 }}. {{ item.name }}
</li>
} @empty {
<li>Keine Einträge vorhanden</li>
}
</ul>Vorteile gegenüber *ngFor: 1. Obligatorisches
Tracking: Das track-Keyword macht das
Tracking-Konzept expliziter und leichter verständlich 2.
Eingebauter Empty-Zustand: Der @empty-Block bietet eine elegante
Möglichkeit, leere Sammlungen zu behandeln 3. Verbesserte Syntax
für lokale Variablen: Die Mikrosyntax für lokale Variablen ist
intuitiver 4. Performance-Optimierung: Da Tracking
obligatorisch ist, werden die Best Practices von Anfang an erzwungen
Die verfügbaren lokalen Variablen in @for sind: - $index: Der aktuelle
Index des Elements - $first: Boolean, der angibt, ob das
aktuelle Element das erste ist - $last: Boolean, der
angibt, ob das aktuelle Element das letzte ist - $even:
Boolean, der angibt, ob der aktuelle Index gerade ist -
$odd: Boolean, der angibt, ob der aktuelle Index ungerade
ist - $count: Die Gesamtzahl der Elemente in der Sammlung
(neu in der @for-Syntax)
@switch: Die moderne Alternative zu ngSwitch
@switch (userRole) {
@case ('admin') {
<h2>Admin Dashboard</h2>
<p>Hier können Sie alle Systemeinstellungen verwalten.</p>
}
@case ('editor') {
<h2>Editor Werkzeuge</h2>
<p>Hier können Sie Inhalte bearbeiten und veröffentlichen.</p>
}
@default {
<h2>Benutzer-Ansicht</h2>
<p>Willkommen in Ihrem persönlichen Bereich.</p>
}
}Vorteile gegenüber ngSwitch: 1. Einheitliche Syntax: Consistent mit @if und @for 2. Verbesserte Lesbarkeit: Klare visuelle Unterscheidung zwischen verschiedenen Fällen 3. Weniger Boilerplate: Kein Bedarf für Container-Elemente bei jedem Fall
@defer: Revolutionäres Lazy Loading auf Komponentenebene
Die @defer-Direktive ist eine der innovativsten Neuerungen in Angular 17. Sie ermöglicht das verzögerte Laden von Teilen des Templates basierend auf verschiedenen Triggern:
@defer {
<!-- Dieser komplexe Inhalt wird verzögert geladen -->
<heavy-component />
}
<!-- Mit Trigger -->
@defer (on viewport) {
<lazy-loaded-content />
}
<!-- Mit Placeholder während des Ladens -->
@defer (on viewport) {
<charts-component />
} @loading {
<p>Lade Diagramme...</p>
} @error {
<p>Fehler beim Laden der Diagramme.</p>
}
<!-- Mit Prefetching-Strategien -->
@defer (on idle) {
<non-critical-ui />
}Die @defer-Direktive bietet mehrere
leistungsstarke Trigger-Optionen: - on viewport: Lädt, wenn
der Platzhalter im Viewport sichtbar wird - on interaction:
Lädt bei Interaktion mit einem referenzierten Element -
on hover: Lädt beim Hovern über ein referenziertes Element
- on immediate: Lädt sofort, aber asynchron (nächster
Angular-Zyklus) - on idle: Lädt, wenn der Browser im
Leerlauf ist - on timer(ms): Lädt nach einem bestimmten
Timeout - when condition: Lädt, wenn eine Bedingung erfüllt
ist
Zusätzlich bietet @defer verschiedene Zustände: -
@loading: Wird angezeigt, während der Inhalt geladen wird -
@error: Wird angezeigt, wenn beim Laden ein Fehler auftritt
- @placeholder: Wird angezeigt, bevor einer der Trigger
ausgelöst wird
Praktisches Beispiel für @defer mit verschiedenen Zuständen:
<article class="product-detail">
<header>
<h1>{{ product.name }}</h1>
<p class="price">{{ product.price | currency }}</p>
</header>
<!-- Sofort sichtbare kritische Inhalte -->
<section class="basic-info">
<img [src]="product.thumbnail" alt="{{ product.name }}" />
<p class="description">{{ product.shortDescription }}</p>
<button (click)="addToCart()">In den Warenkorb</button>
</section>
<!-- Verzögertes Laden der detaillierten Produktinformationen -->
@defer (on viewport) {
<section class="detailed-info">
<h2>Produktdetails</h2>
<app-product-specifications [specs]="product.specifications"></app-product-specifications>
<app-product-dimensions [dimensions]="product.dimensions"></app-product-dimensions>
</section>
} @loading {
<p class="loading-text">Lade detaillierte Produktinformationen...</p>
} @placeholder {
<button class="load-more-button">Mehr Informationen anzeigen</button>
}
<!-- Verzögertes Laden der Bewertungen erst bei Bedarf -->
@defer (on interaction(reviewsBtn)) {
<section class="reviews">
<h2>Kundenbewertungen</h2>
<app-reviews-list [productId]="product.id"></app-reviews-list>
</section>
} @loading {
<p class="loading-text">Lade Bewertungen...</p>
} @placeholder {
<button #reviewsBtn class="reviews-button">
Bewertungen anzeigen ({{ product.reviewCount }})
</button>
}
<!-- Verzögertes Laden der Produkt-Empfehlungen im Leerlauf -->
@defer (on idle) {
<section class="recommendations">
<h2>Das könnte Ihnen auch gefallen</h2>
<app-product-recommendations [productId]="product.id"></app-product-recommendations>
</section>
} @loading {
<p class="loading-text">Lade Empfehlungen...</p>
}
</article>In diesem Beispiel: 1. Die kritischen Inhalte (Produktname, Bild, Preis, kurze Beschreibung und Kaufen-Button) werden sofort geladen 2. Die detaillierten Produktinformationen werden geladen, sobald der Benutzer zu diesem Bereich scrollt 3. Die Bewertungen werden erst geladen, wenn der Benutzer explizit darauf klickt 4. Die Produkt-Empfehlungen werden im Browser-Leerlauf geladen, wenn nichts Wichtigeres zu tun ist
Diese Strategie verbessert die anfängliche Ladezeit und die Benutzerfreundlichkeit erheblich, indem sie die Priorität auf die wichtigsten Inhalte legt und weniger wichtige Inhalte verzögert lädt.
Die Erstellung eigener Strukturdirektiven erfordert ein gutes Verständnis davon, wie Angular mit Templates umgeht. Hier ist ein ausführlicheres Beispiel für eine benutzerdefinierte Strukturdirektive:
// unless.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef, OnChanges, SimpleChanges } from '@angular/core';
@Directive({
selector: '[appUnless]',
standalone: true
})
export class UnlessDirective implements OnChanges {
private hasView = false;
// Die Haupteingabe, die die unless-Bedingung steuert
@Input() set appUnless(condition: boolean) {
this.updateView(condition);
}
// Optionaler else-Template-Verweis
@Input() appUnlessElse?: TemplateRef<any>;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) {}
ngOnChanges(changes: SimpleChanges): void {
if ('appUnless' in changes) {
this.updateView(changes['appUnless'].currentValue);
}
}
private updateView(condition: boolean): void {
if (!condition && !this.hasView) {
// Wenn die Bedingung false ist und die Ansicht noch nicht gerendert wurde
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
} else if (condition && this.hasView) {
// Wenn die Bedingung true ist und die Ansicht bereits gerendert wurde
this.viewContainer.clear();
this.hasView = false;
// Render else-Template, falls vorhanden
if (this.appUnlessElse) {
this.viewContainer.createEmbeddedView(this.appUnlessElse);
}
}
}
}Diese erweiterte Version der Unless-Direktive unterstützt auch ein else-Template:
<div *appUnless="isConditionMet; else elseTemplate">
Dieser Inhalt wird angezeigt, wenn isConditionMet false ist.
</div>
<ng-template #elseTemplate>
Dieser Inhalt wird angezeigt, wenn isConditionMet true ist.
</ng-template>Eine fortgeschrittenere Struktur-Direktive: Repeater mit Pagination
Hier ist ein Beispiel für eine komplexere Strukturdirektive, die Wiederholung mit integrierter Paginierung implementiert:
// paginated-for.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef, OnChanges, SimpleChanges } from '@angular/core';
interface PaginationContext<T> {
$implicit: T;
index: number;
count: number;
first: boolean;
last: boolean;
even: boolean;
odd: boolean;
page: number;
totalPages: number;
}
@Directive({
selector: '[appPaginatedFor]',
standalone: true
})
export class PaginatedForDirective<T> implements OnChanges {
@Input() appPaginatedForOf: T[] = [];
@Input() appPaginatedForItemsPerPage = 10;
@Input() appPaginatedForCurrentPage = 1;
@Input() appPaginatedForTrackBy: (index: number, item: T) => any = index => index;
// Template für leeren Zustand
@Input() appPaginatedForEmpty?: TemplateRef<any>;
// Speichert die aktuelle Seitenansicht, um Änderungen zu erkennen
private _currentView: {
items: T[];
page: number;
trackByFn: (index: number, item: T) => any;
} | null = null;
constructor(
private templateRef: TemplateRef<PaginationContext<T>>,
private viewContainer: ViewContainerRef
) {}
ngOnChanges(changes: SimpleChanges): void {
// Ansicht aktualisieren, wenn sich relevante Eingaben ändern
const shouldUpdateView =
changes['appPaginatedForOf'] ||
changes['appPaginatedForItemsPerPage'] ||
changes['appPaginatedForCurrentPage'] ||
changes['appPaginatedForTrackBy'];
if (shouldUpdateView) {
this.updateView();
}
}
private updateView(): void {
// Ansicht leeren
this.viewContainer.clear();
const items = this.appPaginatedForOf || [];
const itemsPerPage = Math.max(1, this.appPaginatedForItemsPerPage);
const totalPages = Math.ceil(items.length / itemsPerPage);
const currentPage = Math.max(1, Math.min(this.appPaginatedForCurrentPage, totalPages));
// Prüfen auf leere Liste
if (items.length === 0) {
if (this.appPaginatedForEmpty) {
this.viewContainer.createEmbeddedView(this.appPaginatedForEmpty);
}
this._currentView = null;
return;
}
// Elemente für die aktuelle Seite berechnen
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = Math.min(startIndex + itemsPerPage, items.length);
const pageItems = items.slice(startIndex, endIndex);
// Aktuellen View-Zustand speichern
this._currentView = {
items: pageItems,
page: currentPage,
trackByFn: this.appPaginatedForTrackBy
};
// Elemente rendern
pageItems.forEach((item, localIndex) => {
const globalIndex = startIndex + localIndex;
const context: PaginationContext<T> = {
$implicit: item,
index: globalIndex,
count: items.length,
first: globalIndex === 0,
last: globalIndex === items.length - 1,
even: globalIndex % 2 === 0,
odd: globalIndex % 2 !== 0,
page: currentPage,
totalPages: totalPages
};
this.viewContainer.createEmbeddedView(this.templateRef, context);
});
}
}Und so würde man diese Direktive verwenden:
<div class="pagination-controls">
<button
[disabled]="currentPage === 1"
(click)="currentPage = currentPage - 1">
Vorherige Seite
</button>
<span>Seite {{ currentPage }} von {{ getTotalPages() }}</span>
<button
[disabled]="currentPage === getTotalPages()"
(click)="currentPage = currentPage + 1">
Nächste Seite
</button>
</div>
<ul>
<li *appPaginatedFor="
let item of items;
itemsPerPage: 5;
currentPage: currentPage;
trackBy: trackByFn;
empty: emptyTemplate;
let i = index;
let pg = page;
let total = totalPages"
>
Item #{{ i + 1 }}: {{ item.name }} (Seite {{ pg }} von {{ total }})
</li>
</ul>
<ng-template #emptyTemplate>
<li class="empty-state">Keine Elemente vorhanden.</li>
</ng-template>Mit der zugehörigen Komponenten-Logik:
@Component({...})
export class PaginationExampleComponent {
items = Array.from({ length: 50 }, (_, i) => ({
id: i + 1,
name: `Item ${i + 1}`
}));
currentPage = 1;
itemsPerPage = 5;
trackByFn(index: number, item: any): number {
return item.id;
}
getTotalPages(): number {
return Math.ceil(this.items.length / this.itemsPerPage);
}
}Diese Direktive demonstriert fortgeschrittene Konzepte: 1. Generische Typen: Die Direktive nutzt Generics für Typsicherheit 2. Komplexe Kontexterstellung: Sie erstellt einen reichhaltigen Kontext mit Paginierungsinformationen 3. Mehrere Eingaben: Sie kombiniert mehrere Input-Properties zur Steuerung des Verhaltens 4. Optimierte Aktualisierungen: Sie verfolgt den aktuellen Zustand, um unnötige Aktualisierungen zu vermeiden
Seit Angular 14 und insbesondere seit Angular 17 wurden bedeutende Verbesserungen für Direktiven eingeführt, die das API vereinfachen und die Performance verbessern.
Angular 16 führte Signals als neue Reaktivitätsprimitive ein, und Angular 17 erweiterte diese Unterstützung für Direktiven. Signals bieten eine effizientere Alternative zum klassischen Change Detection-System:
// resizable.directive.ts
import { Directive, ElementRef, signal, computed, input, effect, HostListener } 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 {
// Signal-basierte Inputs
minWidth = input<number>(100);
minHeight = input<number>(100);
// Interne State-Signals
private width = signal<number>(0);
private height = signal<number>(0);
// Computed-Signals für abgeleitete Werte
currentSize = computed(() => ({
width: this.width(),
height: this.height()
}));
isMinimumSize = computed(() =>
this.width() <= this.minWidth() ||
this.height() <= this.minHeight()
);
constructor(private el: ElementRef) {
// Initialisierung der Signals mit aktuellen Dimensionen
this.updateDimensions();
// Effect für Änderungen beobachten (ähnlich wie subscriptions, aber automatisch aufgeräumt)
effect(() => {
const size = this.currentSize();
if (size.width > 0 && size.height > 0) {
console.log(`Element resize: ${size.width}x${size.height}`);
}
});
}
@HostListener('mouseup')
onResize() {
this.updateDimensions();
}
private updateDimensions() {
const element = this.el.nativeElement;
this.width.set(element.offsetWidth);
this.height.set(element.offsetHeight);
}
}Verwendung:
<div
appResizable
[appResizable.minWidth]="200"
[appResizable.minHeight]="150"
class="resizable-container">
Inhalt hier - diese Box kann man in der Größe verändern!
</div>Die Vorteile der signalbasierten Ansatzes: 1. Feingranulare Reaktivität: Nur die von Änderungen betroffenen Teile der Anwendung werden aktualisiert 2. Verbesserte Performance: Weniger unnötige Berechnungen und DOM-Updates 3. Bessere Entwicklererfahrung: Die Datenflüsse werden expliziter und leichter zu verstehen 4. Automatische Ressourcenfreigabe: Effects werden automatisch aufgeräumt, wenn die Direktive zerstört wird
Die in Angular 14 eingeführte inject()-Funktion
vereinfacht die Dependency Injection erheblich und ermöglicht einen
klareren und flexibleren Code. Dies ist besonders nützlich in
Direktiven:
// outside-click.directive.ts
import { Directive, ElementRef, inject, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
@Directive({
selector: '[appOutsideClick]',
standalone: true
})
export class OutsideClickDirective implements OnInit, OnDestroy {
// DI mit inject-Funktion
private el = inject(ElementRef);
private document = inject(Document, { optional: true }) || document;
@Output() appOutsideClick = new EventEmitter<void>();
private clickListener!: (event: MouseEvent) => void;
ngOnInit() {
// Listener für Klicks definieren
this.clickListener = (event: MouseEvent) => {
const clickedElement = event.target as HTMLElement;
const hostElement = this.el.nativeElement;
// Prüfen, ob der Klick außerhalb des Host-Elements erfolgte
if (clickedElement !== hostElement && !hostElement.contains(clickedElement)) {
this.appOutsideClick.emit();
}
};
// Event-Listener hinzufügen
this.document.addEventListener('click', this.clickListener);
}
ngOnDestroy() {
// Event-Listener entfernen, um Memory-Leaks zu vermeiden
this.document.removeEventListener('click', this.clickListener);
}
}Verwendung:
<div class="dropdown" [class.open]="isOpen" (appOutsideClick)="isOpen = false">
<button (click)="isOpen = !isOpen">Dropdown öffnen</button>
<div class="dropdown-menu" *ngIf="isOpen">
<a href="#">Option 1</a>
<a href="#">Option 2</a>
<a href="#">Option 3</a>
</div>
</div>Die inject()-Funktion bietet mehrere Vorteile gegenüber
dem traditionellen Constructor-Injection-Ansatz: 1. Weniger
Boilerplate: Keine Notwendigkeit, Parameter im Konstruktor zu
deklarieren 2. Dynamische Injection: Dienste können
basierend auf Bedingungen injiziert werden 3. Optionale
Dependencies: Einfacheres Handling von optionalen
Abhängigkeiten 4. Lazy Injection: Dienste können bei
Bedarf injiziert werden, nicht nur bei der Konstruktion
Angular bietet umfangreiche Möglichkeiten für Host-Bindungen und Listeners, die es Direktiven ermöglichen, direkt mit ihrem Host-Element zu interagieren:
// drag-drop.directive.ts
import { Directive, ElementRef, Renderer2, HostBinding, HostListener, Input, Output, EventEmitter } from '@angular/core';
export interface DragPosition {
x: number;
y: number;
}
@Directive({
selector: '[appDragDrop]',
standalone: true,
host: {
'[class.draggable]': 'true',
'[class.dragging]': 'isDragging',
'[style.cursor]': 'isDragging ? "grabbing" : "grab"'
}
})
export class DragDropDirective {
@Input() dragEnabled = true;
@Input() boundToParent = false;
@Output() positionChange = new EventEmitter<DragPosition>();
// Host-Bindings mit @HostBinding-Dekorator
@HostBinding('style.position') position = 'relative';
@HostBinding('style.left.px') left = 0;
@HostBinding('style.top.px') top = 0;
// Status-Flag für Dragging
isDragging = false;
// Speichern der letzten Position und des Offsets
private startX = 0;
private startY = 0;
private elementX = 0;
private elementY = 0;
constructor(
private el: ElementRef,
private renderer: Renderer2
) {}
@HostListener('mousedown', ['$event'])
onMouseDown(event: MouseEvent): void {
if (!this.dragEnabled) return;
// Verhindern, dass Text selektiert wird während des Ziehens
event.preventDefault();
this.isDragging = true;
// Startposition speichern
this.startX = event.clientX;
this.startY = event.clientY;
this.elementX = this.left;
this.elementY = this.top;
// Globale Event-Listener hinzufügen
this.addDocumentListeners();
}
private addDocumentListeners(): void {
const moveListener = this.renderer.listen('document', 'mousemove', (event: MouseEvent) => {
if (this.isDragging) {
this.onDrag(event);
}
});
const upListener = this.renderer.listen('document', 'mouseup', () => {
if (this.isDragging) {
this.isDragging = false;
// Globale Event-Listener entfernen
moveListener();
upListener();
// Position-Change-Event emittieren
this.positionChange.emit({ x: this.left, y: this.top });
}
});
}
private onDrag(event: MouseEvent): void {
// Neue Position berechnen
let newLeft = this.elementX + (event.clientX - this.startX);
let newTop = this.elementY + (event.clientY - this.startY);
// Begrenzung auf Elterncontainer, falls aktiviert
if (this.boundToParent) {
const parentRect = this.el.nativeElement.parentElement.getBoundingClientRect();
const elementRect = this.el.nativeElement.getBoundingClientRect();
// Grenzen berechnen
const maxLeft = parentRect.width - elementRect.width;
const maxTop = parentRect.height - elementRect.height;
// Position auf Grenzen beschränken
newLeft = Math.max(0, Math.min(newLeft, maxLeft));
newTop = Math.max(0, Math.min(newTop, maxTop));
}
// Position aktualisieren
this.left = newLeft;
this.top = newTop;
}
}Verwendung:
<div class="container" style="position: relative; width: 500px; height: 300px; border: 1px solid #ccc;">
<div
appDragDrop
[dragEnabled]="true"
[boundToParent]="true"
(positionChange)="onPositionChanged($event)"
style="width: 100px; height: 100px; background-color: #3498db;">
Zieh mich!
</div>
</div>Mit den entsprechenden Komponenten-Methoden:
@Component({...})
export class DragDropDemoComponent {
onPositionChanged(position: DragPosition): void {
console.log(`Neue Position: x=${position.x}, y=${position.y}`);
// Hier könnten Sie die Position in einer Datenbank speichern oder andere Logik ausführen
}
}Diese Direktive demonstriert mehrere wichtige Konzepte: 1. Host-Bindungen: Direktes Setzen von Stilen und Klassen auf dem Host-Element 2. Event-Handling: Komplexe Event-Verarbeitung mit globalen Listenern 3. Bidirektionale Kommunikation: Input-Parameter und Output-Events 4. Dynamisches Styling: Anpassung des Aussehens basierend auf dem Zustand
Die Entwicklung effektiver und wartbarer Direktiven erfordert die Befolgung von Best Practices. Hier sind detaillierte Empfehlungen:
Standalone-Direktiven vereinfachen die Wiederverwendbarkeit und reduzieren Abhängigkeiten zu NgModules:
@Directive({
selector: '[appFeature]',
standalone: true,
// Explizite Imports anderer benötigter Direktiven oder Pipes
imports: [CommonModule, OtherStandaloneDirective]
})
export class FeatureDirective { }Vorteile: - Einfacheres Teilen zwischen Projekten - Bessere Tree-Shaking-Möglichkeiten - Explizitere Abhängigkeiten - Vereinfachte Testbarkeit
Ein konsistentes Namensmuster für Selektoren macht den Code lesbarer und verhindert Kollisionen:
// Gut: mit Präfix 'app'
@Directive({
selector: '[appHighlight]'
})
// Besser: mit spezifischerem Projekt- oder Feature-Präfix
@Directive({
selector: '[acmeToolbar]'
})Bei größeren Projekten oder Bibliotheken sollten Sie den Namen Ihrer Organisation oder des Projekts als Präfix verwenden.
Stellen Sie sicher, dass Ihre Direktiven die entsprechenden Lifecycle-Interfaces implementieren:
@Directive({...})
export class ComplexDirective implements OnInit, OnChanges, OnDestroy {
ngOnInit(): void {
// Initialisierungslogik
}
ngOnChanges(changes: SimpleChanges): void {
// Reaktion auf Änderungen der Input-Properties
if (changes['someInput']) {
// Spezifische Logik für diesen Input
}
}
ngOnDestroy(): void {
// Aufräumen: Event-Listener entfernen, Subscriptions beenden, etc.
this.subscription.unsubscribe();
}
}Dies ist besonders wichtig für Direktiven, die: - Globale Event-Listener registrieren - Observable-Subscriptions haben - Timer oder Intervalle verwenden - Externe Ressourcen wie WebSockets verwalten
Direkte DOM-Manipulationen über nativeElement können
problematisch sein, insbesondere bei serverseitigem Rendering oder in
Web Workers:
// Nicht optimal
@Directive({...})
export class UnsafeDirective {
constructor(private el: ElementRef) {
// Direkte DOM-Manipulation - problematisch bei SSR
this.el.nativeElement.style.color = 'red';
}
}
// Besser: mit Renderer2
@Directive({...})
export class SafeDirective {
constructor(private el: ElementRef, private renderer: Renderer2) {
// Plattformunabhängige DOM-Manipulation
this.renderer.setStyle(this.el.nativeElement, 'color', 'red');
}
}
// Noch besser: mit HostBinding
@Directive({...})
export class BestDirective {
@HostBinding('style.color') color = 'red';
}Der Renderer2-Service bietet eine plattformunabhängige Abstraktionsschicht für DOM-Manipulationen, was wichtig ist für: - Universelle Anwendungen (Server-Side Rendering) - WebWorker-Unterstützung - Testbarkeit - Sicherheit gegen XSS-Angriffe
Da Direktiven oft versteckt in Templates verwendet werden, ist eine gute Dokumentation besonders wichtig:
/**
* Ermöglicht das Ziehen und Ablegen eines Elements innerhalb seines Elterncontainers.
*
* @usageNotes
*
* #### Einfaches Beispiel
*
* ```html
* <div appDragDrop>Zieh mich!</div>
* ```
*
* #### Mit allen Optionen
*
* ```html
* <div
* appDragDrop
* [dragEnabled]="true"
* [boundToParent]="true"
* (positionChange)="onPositionChanged($event)">
* Zieh mich innerhalb des Elterncontainers!
* </div>
* ```
*
* @param dragEnabled - Aktiviert oder deaktiviert die Drag-Funktionalität
* @param boundToParent - Wenn true, kann das Element nicht über die Grenzen des Elterncontainers hinaus gezogen werden
* @event positionChange - Wird ausgelöst, wenn das Element an eine neue Position gezogen wurde
*/
@Directive({
selector: '[appDragDrop]',
standalone: true
})
export class DragDropDirective {
// Implementierung hier...
}Diese Dokumentation ist hilfreich für: - Andere Entwickler im Team - Zukünftige Wartung - Generierung von API-Dokumentation - IDE-Unterstützung (Tooltips und Autovervollständigung)
Befolge das Single-Responsibility-Prinzip, indem du Direktiven auf eine spezifische Aufgabe fokussierst:
// Schlecht: Eine Direktive mit zu vielen Verantwortlichkeiten
@Directive({
selector: '[appSuperDirective]'
})
export class SuperDirective {
// Validierung, Formatierung, Styling, Event-Handling, usw.
}
// Besser: Separate Direktiven für separate Anliegen
@Directive({
selector: '[appInputValidator]'
})
export class InputValidatorDirective { /* Nur Validierung */ }
@Directive({
selector: '[appInputFormatter]'
})
export class InputFormatterDirective { /* Nur Formatierung */ }
@Directive({
selector: '[appInputStyling]'
})
export class InputStylingDirective { /* Nur Styling */ }Diese Aufteilung verbessert: - Wiederverwendbarkeit - Testbarkeit - Lesbarkeit - Wartbarkeit
Bei Direktiven, die komplexe Operationen durchführen oder häufig ausgeführt werden, sollten Performanceaspekte beachtet werden:
@Directive({
selector: '[appPerformantDirective]'
})
export class PerformantDirective implements OnChanges {
@Input() complexData: any;
private processedData: any;
// Memoization: Speichern berechneter Werte
private memoizedResults = new Map<string, any>();
ngOnChanges(changes: SimpleChanges): void {
if (changes['complexData']) {
// Nur neu berechnen, wenn sich die Daten geändert haben
this.processComplexData();
}
}
private processComplexData(): void {
// Prüfen, ob wir bereits ein Ergebnis für diese Eingabe haben
const inputKey = JSON.stringify(this.complexData);
if (this.memoizedResults.has(inputKey)) {
this.processedData = this.memoizedResults.get(inputKey);
return;
}
// Teure Berechnung durchführen
this.processedData = /* komplexe Verarbeitung */;
// Ergebnis für zukünftige Verwendung speichern
this.memoizedResults.set(inputKey, this.processedData);
// Begrenzen der Cachegröße, um Speicherlecks zu vermeiden
if (this.memoizedResults.size > 100) {
const oldestKey = this.memoizedResults.keys().next().value;
this.memoizedResults.delete(oldestKey);
}
}
}Weitere Performance-Strategien: - Vermeiden von häufigen
DOM-Manipulationen - Debouncing von Event-Handlern bei häufigen Events -
Lazy Initialization von ressourcenintensiven Operationen - Verwendung
von ChangeDetectionStrategy.OnPush in Komponenten
Die Einführung der neuen Control Flow Syntax in Angular 17 stellt einen bedeutenden Fortschritt dar. Hier ist ein ausführlicher Vergleich:
| Feature | Klassische Syntax | Neue Syntax | Vorteile der neuen Syntax |
|---|---|---|---|
| Bedingte Anzeige | *ngIf="condition" |
@if (condition) {} |
Bessere Lesbarkeit, native Else/ElseIf-Unterstützung, typsicherer |
| Listen | *ngFor="let item of items; trackBy: trackFn" |
@for (item of items; track item.id) {} |
Obligatorisches Tracking, @empty-Block, bessere Lesbarkeit von Variablen |
| Verzweigung | [ngSwitch]="value" mit *ngSwitchCase |
@switch (value) { @case() {} } |
Übersichtlichere Struktur, weniger Boilerplate |
| Lazy Loading | Nicht nativ vorhanden | @defer (on trigger) {} |
Eingebautes progressives Rendering, bessere Performance |
| Type Checking | Begrenzt | Umfassend | Besserer IDE-Support, frühere Fehlererkennung |
| Lesbarkeit | Komplexe Mikrosyntax | Blockstruktur mit geschweiften Klammern | Intuitiver für Entwickler, ähnlicher zu anderen Programmiersprachen |
Komplexe Verzweigungen:
Klassische Syntax:
<div *ngIf="loading">Wird geladen...</div>
<div *ngIf="!loading && error">
Fehler: {{ error.message }}
</div>
<div *ngIf="!loading && !error && data.length === 0">
Keine Daten vorhanden.
</div>
<div *ngIf="!loading && !error && data.length > 0">
<!-- Daten anzeigen -->
</div>Neue Syntax:
@if (loading) {
<div>Wird geladen...</div>
} @else if (error) {
<div>Fehler: {{ error.message }}</div>
} @else if (data.length === 0) {
<div>Keine Daten vorhanden.</div>
} @else {
<div>
<!-- Daten anzeigen -->
</div>
}Listen mit leerem Zustand:
Klassische Syntax:
<div *ngIf="items.length === 0">
Keine Elemente vorhanden.
</div>
<ul *ngIf="items.length > 0">
<li *ngFor="let item of items; trackBy: trackById; let isLast = last"
[class.last-item]="isLast">
{{ item.name }}
</li>
</ul>Neue Syntax:
<ul>
@for (item of items; track item.id; let isLast = $last) {
<li [class.last-item]="isLast">
{{ item.name }}
</li>
} @empty {
<li class="empty-state">Keine Elemente vorhanden.</li>
}
</ul>Switch-Anweisungen:
Klassische Syntax:
<div [ngSwitch]="status">
<div *ngSwitchCase="'success'">
<div class="alert alert-success">
<strong>Erfolg!</strong> Der Vorgang wurde erfolgreich abgeschlossen.
</div>
</div>
<div *ngSwitchCase="'warning'">
<div class="alert alert-warning">
<strong>Warnung!</strong> Es gibt potenzielle Probleme.
</div>
</div>
<div *ngSwitchCase="'error'">
<div class="alert alert-danger">
<strong>Fehler!</strong> Es ist ein Problem aufgetreten.
</div>
</div>
<div *ngSwitchDefault>
<div class="alert alert-info">
<strong>Info:</strong> Status ist {{ status }}.
</div>
</div>
</div>Neue Syntax:
@switch (status) {
@case ('success') {
<div class="alert alert-success">
<strong>Erfolg!</strong> Der Vorgang wurde erfolgreich abgeschlossen.
</div>
}
@case ('warning') {
<div class="alert alert-warning">
<strong>Warnung!</strong> Es gibt potenzielle Probleme.
</div>
}
@case ('error') {
<div class="alert alert-danger">
<strong>Fehler!</strong> Es ist ein Problem aufgetreten.
</div>
}
@default {
<div class="alert alert-info">
<strong>Info:</strong> Status ist {{ status }}.
</div>
}
}Beim Übergang zur neuen Syntax gibt es einige Punkte zu beachten:
Mischung von Syntaxen: In der Übergangsphase können beide Syntaxen koexistieren, aber nicht gemischt werden:
<!-- NICHT mischen: -->
@if (condition) {
<div *ngFor="let item of items">{{ item }}</div>
}
<!-- Besser: Konsistent bleiben -->
@if (condition) {
@for (item of items) {
<div>{{ item }}</div>
}
}Bekannte Unterschiede:
@for erfordert immer ein track-Statement,
während *ngFor ohne trackBy funktioniert.$index vs. index).@if unterstützt nativ @else if, während
*ngIf verschachtelte Templates erfordert.Feature-Parität: Einige fortgeschrittene Funktionen könnten in der neuen Syntax etwas anders funktionieren. Prüfen Sie die API-Dokumentation für Details.
Das Migrieren einer Angular-Anwendung auf die neue Control Flow Syntax sollte strategisch angegangen werden. Hier ist ein detaillierter Migrationsplan:
Sicherstellen der Angular-Version: Aktualisieren Sie auf Angular 17 oder höher.
Aktivieren der Compiler-Optionen: In der
tsconfig.json die passenden Optionen setzen:
{
"compilerOptions": {
// ...
},
"angularCompilerOptions": {
"enableControlFlowSyntax": true
}
}Komponentenanalyse: Identifizieren Sie Komponenten für die Migration, beginnend mit:
Neue Komponenten mit neuer Syntax erstellen:
@Component({
standalone: true,
selector: 'app-new-component',
template: `
@if (loading) {
<app-loading-spinner />
} @else {
<app-content [data]="data" />
}
`
})
export class NewComponent { }Template-Migration für bestehende Komponenten:
ngIf zu
@if) auf einmalNach Direktiven-Typen migrieren:
*ngIf zu @if*ngFor zu @forngSwitch zu @switch@empty-Blocks für verbesserte leere Zustände@defer für verzögertes Laden@else if für klarere BedingungslogikUrsprünglicher Code:
<div class="user-profile" *ngIf="userLoaded; else loading">
<h2>{{ user.name }}</h2>
<div class="user-details">
<div *ngIf="user.email">Email: {{ user.email }}</div>
<div *ngIf="user.phone">Telefon: {{ user.phone }}</div>
</div>
<h3 *ngIf="user.orders && user.orders.length > 0">Bestellungen</h3>
<ul *ngIf="user.orders && user.orders.length > 0">
<li *ngFor="let order of user.orders; trackBy: trackByOrderId">
{{ order.id }} - {{ order.date | date }} - {{ order.total | currency }}
</li>
</ul>
<div *ngIf="!user.orders || user.orders.length === 0">
Keine Bestellungen vorhanden.
</div>
</div>
<ng-template #loading>
<div class="loading-spinner">Lade Benutzerdaten...</div>
</ng-template>Schrittweise Migration:
Schritt 1: Ersetzen des primären ngIf/else:
@if (userLoaded) {
<div class="user-profile">
<h2>{{ user.name }}</h2>
<div class="user-details">
<div *ngIf="user.email">Email: {{ user.email }}</div>
<div *ngIf="user.phone">Telefon: {{ user.phone }}</div>
</div>
<h3 *ngIf="user.orders && user.orders.length > 0">Bestellungen</h3>
<ul *ngIf="user.orders && user.orders.length > 0">
<li *ngFor="let order of user.orders; trackBy: trackByOrderId">
{{ order.id }} - {{ order.date | date }} - {{ order.total | currency }}
</li>
</ul>
<div *ngIf="!user.orders || user.orders.length === 0">
Keine Bestellungen vorhanden.
</div>
</div>
} @else {
<div class="loading-spinner">Lade Benutzerdaten...</div>
}Schritt 2: Ersetzen der verschachtelten ngIf:
@if (userLoaded) {
<div class="user-profile">
<h2>{{ user.name }}</h2>
<div class="user-details">
@if (user.email) {
<div>Email: {{ user.email }}</div>
}
@if (user.phone) {
<div>Telefon: {{ user.phone }}</div>
}
</div>
@if (user.orders && user.orders.length > 0) {
<h3>Bestellungen</h3>
<ul>
<li *ngFor="let order of user.orders; trackBy: trackByOrderId">
{{ order.id }} - {{ order.date | date }} - {{ order.total | currency }}
</li>
</ul>
} @else {
<div>Keine Bestellungen vorhanden.</div>
}
</div>
} @else {
<div class="loading-spinner">Lade Benutzerdaten...</div>
}Schritt 3: Ersetzen des ngFor:
@if (userLoaded) {
<div class="user-profile">
<h2>{{ user.name }}</h2>
<div class="user-details">
@if (user.email) {
<div>Email: {{ user.email }}</div>
}
@if (user.phone) {
<div>Telefon: {{ user.phone }}</div>
}
</div>
@if (user.orders && user.orders.length > 0) {
<h3>Bestellungen</h3>
<ul>
@for (order of user.orders; track order.id) {
<li>
{{ order.id }} - {{ order.date | date }} - {{ order.total | currency }}
</li>
}
</ul>
} @else {
<div>Keine Bestellungen vorhanden.</div>
}
</div>
} @else {
<div class="loading-spinner">Lade Benutzerdaten...</div>
}Schritt 4: Optimierung mit @empty und Refactoring:
@if (userLoaded) {
<div class="user-profile">
<h2>{{ user.name }}</h2>
<div class="user-details">
@if (user.email) {
<div>Email: {{ user.email }}</div>
}
@if (user.phone) {
<div>Telefon: {{ user.phone }}</div>
}
</div>
<h3>Bestellungen</h3>
<ul>
@for (order of user.orders; track order.id) {
<li>
{{ order.id }} - {{ order.date | date }} - {{ order.total | currency }}
</li>
} @empty {
<li class="no-orders">Keine Bestellungen vorhanden.</li>
}
</ul>
</div>
} @else {
<div class="loading-spinner">Lade Benutzerdaten...</div>
}Direktiven können besonders leistungsstark sein, wenn sie mit anderen Angular-Konzepten wie RxJS, Signals und zoneless Change Detection kombiniert werden:
// scroll-observe.directive.ts
import { Directive, ElementRef, EventEmitter, inject, OnDestroy, Output } from '@angular/core';
import { filter, fromEvent, map, Observable, pairwise, Subscription, throttleTime } from 'rxjs';
export enum ScrollDirection {
Up = 'UP',
Down = 'DOWN'
}
export interface ScrollInfo {
direction: ScrollDirection;
position: number;
delta: number;
}
@Directive({
selector: '[appScrollObserve]',
standalone: true
})
export class ScrollObserveDirective implements OnDestroy {
private el = inject(ElementRef);
private subscription = new Subscription();
@Output() scrollChange = new EventEmitter<ScrollInfo>();
constructor() {
// RxJS-Stream für Scroll-Events erstellen
const scrollEvents$: Observable<ScrollInfo> = fromEvent(this.el.nativeElement, 'scroll')
.pipe(
// Zu häufige Ereignisse throttlen (Performance-Optimierung)
throttleTime(10),
// Position extrahieren
map(() => this.el.nativeElement.scrollTop),
// Paare von aufeinanderfolgenden Werten erzeugen
pairwise(),
// Scroll-Informationen berechnen
map(([previous, current]): ScrollInfo => ({
direction: previous < current ? ScrollDirection.Down : ScrollDirection.Up,
position: current,
delta: Math.abs(current - previous)
})),
// Nur signifikante Änderungen (Performance-Optimierung)
filter(info => info.delta > 0)
);
// Stream abonnieren und Events weiterleiten
this.subscription.add(
scrollEvents$.subscribe(scrollInfo => {
this.scrollChange.emit(scrollInfo);
})
);
}
ngOnDestroy(): void {
// Ressourcen freigeben
this.subscription.unsubscribe();
}
}Verwendung der RxJS-basierten Direktive:
<div
class="scrollable-container"
appScrollObserve
(scrollChange)="onScrollChange($event)">
<!-- Scrollbarer Inhalt -->
</div>@Component({...})
export class ScrollComponent {
onScrollChange(info: ScrollInfo): void {
if (info.direction === ScrollDirection.Down && info.position > 300) {
// Beispiel: Header bei Scrollen nach unten ausblenden
this.showHeader = false;
} else if (info.direction === ScrollDirection.Up) {
// Header bei Scrollen nach oben einblenden
this.showHeader = true;
}
// Bei Näherung an das Ende mehr Daten laden
if (info.position + 300 >= this.containerHeight) {
this.loadMoreData();
}
}
}Mit Angular Signals können Direktiven noch reaktiver gestaltet werden:
// theme-toggle.directive.ts
import { Directive, effect, inject, input, output, signal } from '@angular/core';
import { ThemeService } from './theme.service';
@Directive({
selector: '[appThemeToggle]',
standalone: true,
host: {
'(click)': 'toggle()',
'[class.dark-mode]': 'isDarkMode()',
'[attr.aria-pressed]': 'isDarkMode()'
}
})
export class ThemeToggleDirective {
private themeService = inject(ThemeService);
// Input-Signal mit Default-Wert aus dem Service
prefersDark = input<boolean>(this.themeService.prefersDarkTheme());
// Internes State-Signal
private darkMode = signal<boolean>(this.prefersDark());
// Output-Signal für andere Komponenten
isDarkMode = output<boolean>(this.darkMode);
constructor() {
// Effect für Reaktion auf Input-Änderungen
effect(() => {
// Bei Änderung der Input-Präferenz das interne Signal aktualisieren
this.darkMode.set(this.prefersDark());
// Theme Service aktualisieren
this.themeService.setTheme(this.darkMode() ? 'dark' : 'light');
});
}
toggle(): void {
// Intern Signal umschalten
this.darkMode.update(current => !current);
// Theme Service aktualisieren
this.themeService.setTheme(this.darkMode() ? 'dark' : 'light');
}
}Diese Direktive verwendet: 1. Signal-basierte Inputs
(input<boolean>) 2. Signal-basierte Outputs
(output<boolean>) 3. Interne Signals für
State-Management 4. Angular Effects für reaktives Verhalten
Direktiven sind ein grundlegender und äußerst vielseitiger Teil des Angular-Frameworks. Sie ermöglichen es, HTML-Elemente mit zusätzlicher Funktionalität anzureichern und bilden damit das Rückgrat für moderne, reaktive Benutzeroberflächen. Mit der kontinuierlichen Weiterentwicklung von Angular hat sich auch die Art und Weise, wie wir Direktiven schreiben und verwenden, erheblich verbessert. Die Einführung von Standalone-Direktiven hat die Modularität verbessert, während die neuen Control Flow Direktiven die Templates lesbarer und typsicherer gemacht haben. Besonders bemerkenswert ist die Integration mit dem Angular Signals-System, das eine feinkörnigere Reaktivität ermöglicht und die Performance erheblich verbessern kann. Direktiven können nun effizienter auf Zustandsänderungen reagieren und ihre Host-Elemente entsprechend anpassen, ohne unnötige Berechnungen oder DOM-Manipulationen durchzuführen. Die @defer-Direktive stellt einen Paradigmenwechsel dar, indem sie es ermöglicht, Teile der Benutzeroberfläche basierend auf verschiedenen Bedingungen und Triggern progressiv zu laden. Dies kann die initiale Ladezeit drastisch verkürzen und die Benutzererfahrung verbessern, insbesondere bei komplexen Anwendungen. Für fortgeschrittene Angular-Entwickler eröffnet die Kombination von benutzerdefinierten Direktiven mit RxJS, Signals und zoneless Change Detection neue Möglichkeiten zur Optimierung der Anwendungsleistung und zur Verbesserung der Codeorganisation. Zusammenfassend lässt sich sagen, dass Direktiven, ob integriert oder benutzerdefiniert, weiterhin zu den mächtigsten Werkzeugen im Angular-Ökosystem gehören. Mit den kontinuierlichen Verbesserungen in der Syntax und Funktionalität werden sie für Angular-Entwickler noch wertvoller, um robuste, performante und wartbare Anwendungen zu erstellen. Die Investition in das Erlernen und Beherrschen von Direktiven zahlt sich durch verbesserte Codequalität, wiederverwendbare Abstraktion und eine bessere Benutzererfahrung aus.