15 Pipes für Datentransformation

15.1 Transformation direkt im Template

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.

15.2 Eingebaute Pipes für häufige Szenarien

Angular bietet eine Sammlung eingebauter Pipes für Standard-Transformationen. Diese decken Textformatierung, Zahlenformatierung, Datumsformatierung und Collection-Manipulation ab.

15.2.1 Text-Transformation

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.

15.2.2 Zahlen-Formatierung

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:

Angular 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%.

15.2.3 Datum und Zeit

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.

15.2.4 Collections und Strukturen

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.

15.2.5 Asynchrone Werte mit AsyncPipe

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.

15.3 Eigene Pipes entwickeln

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.

15.4 Pure versus Impure Pipes

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.

15.5 Pipes mit Dependencies

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.

15.6 Testing von Pipes

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.

15.7 Pipes in modernem Angular

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.

15.8 Praktische Patterns

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.