Angular-Templates zeigen Daten an. Oft müssen diese Daten vor der Darstellung transformiert werden – ein Datum formatieren, Text in Großbuchstaben konvertieren, Zahlen als Währung anzeigen. Diese Transformationen könnten in der Komponenten-Logik erfolgen, was jedoch Template und Logik mischt und Wiederverwendbarkeit reduziert.
Pipes lösen dieses Problem durch deklarative Transformation direkt im
Template. Ein Pipe ist eine Funktion, die einen Eingabewert nimmt,
transformiert und das Ergebnis zurückgibt. Die Syntax mit dem
Pipe-Operator | ist konzise und lesbar:
{{ wert | pipeName }}
{{ wert | pipeName:parameter1:parameter2 }}Diese Template-Syntax entspricht einem Funktionsaufruf:
pipeName(wert, parameter1, parameter2). Der Pipe-Operator
macht die Intention explizit – Transformation, nicht Berechnung. Die
Verkettung mehrerer Pipes erzeugt Transformation-Pipelines:
{{ datum | date:'fullDate' | uppercase }}Das Datum wird zuerst formatiert, dann in Großbuchstaben konvertiert.
Jeder Pipe empfängt die Ausgabe des vorherigen, ähnlich Unix-Pipes oder
funktionaler Programmierung mit compose.
Angular bietet eine Sammlung eingebauter Pipes für Standard-Transformationen. Diese decken Textformatierung, Zahlenformatierung, Datumsformatierung und Collection-Manipulation ab.
Die einfachsten Pipes transformieren Texte:
<p>{{ "hello world" | uppercase }}</p>
<!-- HELLO WORLD -->
<p>{{ "HELLO WORLD" | lowercase }}</p>
<!-- hello world -->
<p>{{ "hello world" | titlecase }}</p>
<!-- Hello World -->UpperCasePipe und LowerCasePipe ändern die
Groß-/Kleinschreibung. TitleCasePipe kapitalisiert jeden
Wortanfang. Diese Transformationen sind locale-unabhängig – sie
funktionieren konsistent über verschiedene Sprachen.
Die Pipes sind idempotent – mehrfache Anwendung ändert das Ergebnis
nicht. "HELLO" | uppercase | uppercase bleibt
"HELLO". Dies vereinfacht Reasoning über
Template-Logik.
Zahlen erfordern locale-spezifische Formatierung. Deutsche Nutzer
erwarten Kommas als Dezimaltrenner, US-Nutzer Punkte. Die
DecimalPipe handled dies automatisch:
<p>{{ 1234.5678 | number:'1.2-3' }}</p>
<!-- In de-DE: 1.234,568 -->
<!-- In en-US: 1,234.568 -->Das Format-String '1.2-3' definiert die Struktur:
1: Minimale Anzahl Integer-Stellen (mit Nullen
aufgefüllt)2: Minimale Anzahl Dezimalstellen3: Maximale Anzahl DezimalstellenAngular rundet nach mathematischen Regeln. 1.2345 mit
'1.2-2' wird zu 1.23, 1.2355 wird
zu 1.24.
Währungsformatierung nutzt CurrencyPipe:
<p>{{ 49.99 | currency:'EUR':'symbol':'1.2-2':'de' }}</p>
<!-- 49,99 € -->
<p>{{ 49.99 | currency:'USD':'code' }}</p>
<!-- USD 49.99 -->
<p>{{ 49.99 | currency:'GBP':'symbol-narrow' }}</p>
<!-- £49.99 -->Der erste Parameter ist der ISO-4217-Währungscode. Der zweite steuert
die Darstellung: 'symbol' zeigt das Währungssymbol,
'code' den Drei-Buchstaben-Code,
'symbol-narrow' kompakte Symbole ($ statt US$).
Prozent-Formatierung mit PercentPipe:
<p>{{ 0.8457 | percent:'1.1-2' }}</p>
<!-- In de-DE: 84,6 % -->
<!-- In en-US: 84.6% -->Die Pipe multipliziert automatisch mit 100 und fügt das
Prozentzeichen hinzu. 0.8457 wird zu 84,6%,
nicht 0,8457%.
Die DatePipe ist eine der komplexesten eingebauten
Pipes. Sie unterstützt zahlreiche Format-Strings, Zeitzonen und
Locales:
<p>{{ heute | date:'full':'':'de' }}</p>
<!-- Donnerstag, 13. März 2025 -->
<p>{{ heute | date:'short' }}</p>
<!-- 13.03.25, 14:30 (in de-DE) -->
<!-- 3/13/25, 2:30 PM (in en-US) -->
<p>{{ heute | date:'dd.MM.yyyy HH:mm' }}</p>
<!-- 13.03.2025 14:30 -->
<p>{{ heute | date:'mediumTime':'UTC' }}</p>
<!-- 14:30:45 UTC -->Die Pipe akzeptiert ISO-8601-Strings, Date-Objekte und Timestamps.
Format-Strings können vordefiniert sein ('full',
'short', 'medium') oder custom mit Tokens wie
dd, MM, yyyy, HH,
mm.
Zeitzonenzonen werden durch IANA-Namen
('America/New_York') oder Offsets ('UTC+2',
'GMT-5') spezifiziert. Ohne Zeitzone nutzt die Pipe die
Browser-Default-Timezone.
export class TimeComponent {
now = new Date();
utcNow = new Date().toISOString();
timestamp = Date.now();
}Im Template:
<p>Local: {{ now | date:'full' }}</p>
<p>UTC: {{ utcNow | date:'full':'UTC' }}</p>
<p>Timestamp: {{ timestamp | date:'short' }}</p>Die Pipe ist flexibel genug für die meisten Szenarien, von einfacher Datumsanzeige bis zu Zeitzone-bewusster Terminplanung.
Die SlicePipe extrahiert Teile aus Arrays oder
Strings:
<p>{{ [1, 2, 3, 4, 5] | slice:1:4 }}</p>
<!-- [2, 3, 4] -->
<p>{{ "Angular Pipes" | slice:0:7 }}</p>
<!-- Angular -->Die Indizes funktionieren wie JavaScript’s Array.slice()
– Start ist inklusiv, Ende exklusiv. Negative Indizes zählen vom Ende:
slice:-3 nimmt die letzten drei Elemente.
Die KeyValuePipe transformiert Objekte oder Maps in
iterierbare Arrays:
<div *ngFor="let item of userObject | keyvalue">
{{ item.key }}: {{ item.value }}
</div>export class UserComponent {
userObject = {
name: 'John Doe',
email: 'john@example.com',
age: 30
};
}Ohne KeyValuePipe wären Objekte nicht direkt mit
*ngFor iterierbar. Die Pipe konvertiert sie in ein Array
von {key, value}-Paaren. Die Reihenfolge ist standardmäßig
nach Keys sortiert, kann aber durch einen Comparator angepasst
werden:
<div *ngFor="let item of map | keyvalue:compareFn">
{{ item.key }}: {{ item.value }}
</div>compareFn(a, b) {
return a.value > b.value ? 1 : -1;
}Die JsonPipe serialisiert Objekte für Debugging:
<pre>{{ complexObject | json }}</pre>Dies rendert formatiertes JSON mit Einrückung. Nützlich für Entwicklung und Debugging, nicht für Produktion – JSON-Serialisierung ist teuer für große Objekte.
Die AsyncPipe ist eine der mächtigsten eingebauten
Pipes. Sie subscribed automatisch Observables oder wartet auf Promises
und gibt deren Werte aus:
<div *ngIf="user$ | async as user">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>export class UserProfileComponent {
user$ = this.userService.getCurrentUser();
constructor(private userService: UserService) {}
}Ohne AsyncPipe wäre manuelles Subscription-Management
nötig:
export class UserProfileComponent implements OnInit, OnDestroy {
user: User;
private subscription: Subscription;
ngOnInit() {
this.subscription = this.userService.getCurrentUser()
.subscribe(user => this.user = user);
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}Die AsyncPipe eliminiert diesen Boilerplate. Sie
subscribed beim Rendering, unsubscribed bei Component-Destruction, und
triggered Change Detection bei neuen Werten. Memory Leaks durch
vergessene Unsubscribes werden verhindert.
Die Pipe funktioniert mit Observables und Promises transparent:
<!-- Observable -->
<p>{{ temperature$ | async }}°C</p>
<!-- Promise -->
<p>{{ userData | async | json }}</p>Für mehrfache Verwendung desselben Observable ist die
as-Syntax effizienter:
<div *ngIf="data$ | async as data">
<h2>{{ data.title }}</h2>
<p>{{ data.description }}</p>
<small>{{ data.author }}</small>
</div>Dies subscribed nur einmal, statt dreimal für
data.title, data.description,
data.author.
Eingebaute Pipes decken Common Cases ab. Domain-spezifische
Transformationen erfordern Custom Pipes. Eine Pipe ist eine Klasse mit
@Pipe-Dekorator, die das
PipeTransform-Interface implementiert.
Ein einfaches Beispiel kürzt Texte:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'shorten',
standalone: true
})
export class ShortenPipe implements PipeTransform {
transform(value: string, maxLength: number = 50): string {
if (!value) return '';
if (value.length <= maxLength) {
return value;
}
return value.substring(0, maxLength) + '...';
}
}Die transform-Methode ist das Interface. Sie empfängt
den Eingabewert als ersten Parameter, optionale Argumente als weitere
Parameter. Der Return-Typ kann beliebig sein.
Die Verwendung im Template:
<p>{{ longText | shorten }}</p>
<p>{{ longText | shorten:100 }}</p>Das erste verwendet den Default (50), das zweite überschreibt mit 100. Parameter werden durch Doppelpunkte getrennt.
Eine komplexere Pipe für Highlight-Suche:
import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Pipe({
name: 'highlight',
standalone: true
})
export class HighlightPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {}
transform(text: string, search: string): SafeHtml {
if (!search || !text) {
return text;
}
const regex = new RegExp(search, 'gi');
const highlighted = text.replace(regex, match =>
`<mark>${match}</mark>`
);
return this.sanitizer.bypassSecurityTrustHtml(highlighted);
}
}Die Pipe injiziert DomSanitizer im Konstruktor.
Angular’s Dependency Injection funktioniert in Pipes wie in Komponenten.
Die HTML-Markierung wird explizit als vertrauenswürdig markiert – sonst
würde Angular sie sanitizen.
Im Template mit innerHTML:
<p [innerHTML]="description | highlight:searchTerm"></p>Die Pipe markiert Suchbegriffe mit <mark>-Tags.
Kombiniert mit CSS erzeugt dies visuelle Highlights.
Angular unterscheidet zwischen Pure und Impure Pipes. Diese Unterscheidung hat fundamentale Performance-Implikationen.
Pure Pipes (default) werden nur ausgeführt, wenn: - Der Input-Wert sich ändert (strikte Referenz-Gleichheit für Objekte/Arrays) - Parameter sich ändern
@Pipe({
name: 'pure',
pure: true // Default, kann weggelassen werden
})
export class PurePipe implements PipeTransform {
transform(value: string): string {
console.log('Pure pipe executed');
return value.toUpperCase();
}
}Bei folgendem Template:
<p>{{ text | pure }}</p>wird die Pipe nur ausgeführt, wenn text sich ändert.
Ändert sich ein anderes Property der Komponente, läuft die Pipe nicht
erneut.
Impure Pipes laufen bei jedem Change Detection Cycle:
@Pipe({
name: 'impure',
pure: false
})
export class ImpurePipe implements PipeTransform {
transform(value: string): string {
console.log('Impure pipe executed');
return value.toUpperCase();
}
}Jede Änderung irgendwo in der Komponente triggert die Pipe. Bei einem
Form-Input würde die Pipe bei jedem Tastendruck laufen, selbst wenn
value unverändert bleibt.
Der Unterschied wird bei Arrays kritisch:
@Pipe({
name: 'filterPure',
pure: true
})
export class FilterPurePipe implements PipeTransform {
transform(items: any[], search: string): any[] {
if (!search) return items;
return items.filter(item =>
item.name.toLowerCase().includes(search.toLowerCase())
);
}
}Diese Pure Pipe läuft nur, wenn items oder
search sich ändern. Wird ein Element zum Array hinzugefügt
mit push(), ändert sich die Referenz nicht – die Pipe läuft
nicht. Dies ist überraschend aber gewollt für Performance.
Die Lösung ist entweder eine Impure Pipe:
@Pipe({
name: 'filterImpure',
pure: false
})
export class FilterImpurePipe implements PipeTransform {
transform(items: any[], search: string): any[] {
console.log('Filter executed');
if (!search) return items;
return items.filter(item =>
item.name.toLowerCase().includes(search.toLowerCase())
);
}
}Oder Immutability:
addItem(item: Item) {
this.items = [...this.items, item]; // Neue Referenz
}Mit neuer Referenz erkennt die Pure Pipe die Änderung. Dies ist der empfohlene Ansatz – Immutability verbessert Predictability und ermöglicht Pure Pipes.
Die Performance-Implikationen sind erheblich. Bei 100 Items in einer Liste und einer Impure Filter-Pipe läuft die Filter-Funktion bei jedem Change Detection Cycle – potenziell hunderte Male pro Sekunde bei schnellen Interaktionen. Pure Pipes mit Immutability laufen nur bei tatsächlichen Änderungen.
Pipes können Services und andere Dependencies injizieren. Die moderne
inject()-Funktion vereinfacht dies:
import { Pipe, PipeTransform, inject } from '@angular/core';
import { DatePipe } from '@angular/common';
@Pipe({
name: 'relativeTime',
standalone: true
})
export class RelativeTimePipe implements PipeTransform {
private datePipe = inject(DatePipe);
transform(value: Date | string): string {
const date = new Date(value);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 1) return 'gerade eben';
if (diffMins < 60) return `vor ${diffMins} Minuten`;
const diffHours = Math.floor(diffMins / 60);
if (diffHours < 24) return `vor ${diffHours} Stunden`;
const diffDays = Math.floor(diffHours / 24);
if (diffDays < 7) return `vor ${diffDays} Tagen`;
return this.datePipe.transform(date, 'dd.MM.yyyy');
}
}Die Pipe nutzt DatePipe als Fallback für ältere Daten.
Dependency Injection macht Pipes testbar – Mock-Services können
injiziert werden.
Ein komplexeres Beispiel mit Observable-Return:
import { Pipe, PipeTransform, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { TranslateService } from './translate.service';
@Pipe({
name: 'translate',
standalone: true
})
export class TranslatePipe implements PipeTransform {
private translateService = inject(TranslateService);
transform(key: string, params?: any): Observable<string> {
return this.translateService.get(key, params);
}
}Mit AsyncPipe kombiniert:
<h1>{{ 'header.welcome' | translate | async }}</h1>
<p>{{ 'user.greeting' | translate:{name: userName} | async }}</p>Die Translate-Pipe gibt ein Observable zurück, AsyncPipe subscribed und gibt den String aus. Diese Komposition ermöglicht reaktive Übersetzungen – Sprachwechsel aktualisieren automatisch alle Texte.
Pipes sind pure Functions (wenn richtig implementiert), was sie einfach testbar macht. Ein Test benötigt keine Angular Testing Infrastructure:
import { ShortenPipe } from './shorten.pipe';
describe('ShortenPipe', () => {
let pipe: ShortenPipe;
beforeEach(() => {
pipe = new ShortenPipe();
});
it('should create', () => {
expect(pipe).toBeTruthy();
});
it('should shorten long text', () => {
const longText = 'This is a very long text that should be shortened';
const result = pipe.transform(longText, 20);
expect(result).toBe('This is a very long ...');
expect(result.length).toBe(23); // 20 + '...'
});
it('should not modify short text', () => {
const shortText = 'Short';
const result = pipe.transform(shortText, 20);
expect(result).toBe('Short');
});
it('should handle empty input', () => {
expect(pipe.transform('', 20)).toBe('');
expect(pipe.transform(null, 20)).toBe('');
});
it('should use default max length', () => {
const text = 'A'.repeat(60);
const result = pipe.transform(text);
expect(result.length).toBe(53); // 50 + '...'
});
});Pipes mit Dependencies benötigen Mocks:
import { RelativeTimePipe } from './relative-time.pipe';
import { DatePipe } from '@angular/common';
describe('RelativeTimePipe', () => {
let pipe: RelativeTimePipe;
let datePipeMock: jasmine.SpyObj<DatePipe>;
beforeEach(() => {
datePipeMock = jasmine.createSpyObj('DatePipe', ['transform']);
pipe = new RelativeTimePipe();
(pipe as any).datePipe = datePipeMock;
});
it('should return "gerade eben" for recent times', () => {
const now = new Date();
expect(pipe.transform(now)).toBe('gerade eben');
});
it('should return minutes for recent past', () => {
const fiveMinutesAgo = new Date(Date.now() - 5 * 60000);
expect(pipe.transform(fiveMinutesAgo)).toBe('vor 5 Minuten');
});
it('should fallback to DatePipe for old dates', () => {
const oldDate = new Date('2020-01-01');
datePipeMock.transform.and.returnValue('01.01.2020');
const result = pipe.transform(oldDate);
expect(datePipeMock.transform).toHaveBeenCalledWith(oldDate, 'dd.MM.yyyy');
expect(result).toBe('01.01.2020');
});
});Der Mock ersetzt die echte DatePipe. Tests bleiben isoliert und schnell.
Angular 16+ unterstützt Signals in Pipes. Eine Pipe kann ein Signal als Input nehmen und ein Signal zurückgeben:
import { Pipe, PipeTransform, Signal, computed } from '@angular/core';
@Pipe({
name: 'signalDouble',
standalone: true
})
export class SignalDoublePipe implements PipeTransform {
transform(signal: Signal<number>): Signal<number> {
return computed(() => signal() * 2);
}
}Im Template:
<p>Original: {{ count() }}</p>
<p>Doubled: {{ count | signalDouble }}</p>Das zurückgegebene Signal ist ein Computed, das automatisch aktualisiert wird, wenn das Input-Signal sich ändert. Dies integriert Pipes nahtlos in Signal-basierte Reaktivität.
Ein praktischeres Beispiel für Signal-Transformation:
import { Pipe, PipeTransform, Signal, computed } from '@angular/core';
@Pipe({
name: 'signalMap',
standalone: true
})
export class SignalMapPipe implements PipeTransform {
transform<T, R>(signal: Signal<T>, fn: (value: T) => R): Signal<R> {
return computed(() => fn(signal()));
}
}Diese generische Pipe erlaubt beliebige Transformationen:
<p>{{ users | signalMap: u => u.length }} users</p>
<p>{{ price | signalMap: p => p * 1.19 }} EUR (inkl. MwSt.)</p>Die Pipe ist eine Higher-Order-Function für Signals, ähnlich wie
map für Arrays.
Mehrere Pipes kombiniert erzeugen Transformation-Pipelines. Eine Search-Komponente demonstriert dies:
export class ProductSearchComponent {
searchTerm = signal('');
products = signal<Product[]>([]);
filteredProducts = computed(() => {
const term = this.searchTerm().toLowerCase();
return this.products().filter(p =>
p.name.toLowerCase().includes(term)
);
});
}Im Template:
<input [value]="searchTerm()" (input)="searchTerm.set($event.target.value)">
<div *ngFor="let product of filteredProducts() | slice:0:10">
<h3>{{ product.name | titlecase }}</h3>
<p>{{ product.price | currency:'EUR' }}</p>
<small>{{ product.description | shorten:100 }}</small>
</div>Die Pipeline: Filter via Computed → Slice für Pagination → TitleCase für Namen → Currency für Preise → Shorten für Beschreibungen. Jede Transformation ist isoliert, testbar und wiederverwendbar.
Eine Datentabelle mit Multi-Criteria Sorting:
@Pipe({
name: 'sort',
standalone: true
})
export class SortPipe implements PipeTransform {
transform<T>(array: T[], field: keyof T, order: 'asc' | 'desc' = 'asc'): T[] {
if (!array || !field) return array;
const sorted = [...array].sort((a, b) => {
const aVal = a[field];
const bVal = b[field];
if (aVal === bVal) return 0;
const comparison = aVal > bVal ? 1 : -1;
return order === 'asc' ? comparison : -comparison;
});
return sorted;
}
}Verwendung:
<table>
<thead>
<tr>
<th (click)="toggleSort('name')">Name</th>
<th (click)="toggleSort('price')">Price</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let product of products | sort:sortField:sortOrder">
<td>{{ product.name }}</td>
<td>{{ product.price | currency }}</td>
</tr>
</tbody>
</table>Die Pipe erstellt eine neue Array-Instanz für Immutability.
Click-Handler ändern sortField und sortOrder,
die Pipe re-rendert automatisch.
Pipes sind ein elegantes Werkzeug für Template-Transformation. Sie halten Komponenten-Logik sauber, fördern Wiederverwendung und machen Templates deklarativer. Die Unterscheidung zwischen Pure und Impure Pipes ist essentiell für Performance. Custom Pipes mit Dependency Injection ermöglichen komplexe, testbare Transformationen. Die Integration mit Signals in modernem Angular erweitert Pipes auf reaktive Szenarien, während die Verkettung multiple Transformationen komponiert.