Eine Angular-Anwendung entsteht nicht aus einer einzigen, monolithischen Code-Datei. Stattdessen setzt Angular auf ein komponentenbasiertes Architekturmodell, bei dem die Benutzeroberfläche in modulare, wiederverwendbare Einheiten zerlegt wird. Jede dieser Einheiten – eine Komponente – kapselt Struktur, Verhalten und Darstellung eines bestimmten UI-Elements. Dieser Ansatz ist keine Eigenheit von Angular, sondern spiegelt eine grundlegende Entwicklung im modernen Webdesign wider: Komplexität wird durch Komposition bewältigt.
Bevor theoretische Konzepte erläutert werden, steht die direkte Arbeit mit Code im Vordergrund. Ein neues Angular-Projekt mit einer ersten eigenen Komponente entsteht mit wenigen Terminal-Befehlen:
ng new my-first-app
cd my-first-app
ng generate component greetingDer Befehl ng generate component greeting nutzt die
Angular CLI, um automatisch vier zusammengehörige Dateien zu erzeugen.
Dieses Quartett bildet die Grundstruktur jeder Angular-Komponente:
src/app/greeting/
├── greeting.component.ts # TypeScript-Logik
├── greeting.component.html # Template-Struktur
├── greeting.component.css # Styling
└── greeting.component.spec.ts # Testspezifikation
Die TypeScript-Datei greeting.component.ts zeigt bereits
die wesentlichen Elemente einer Komponente:
import { Component } from '@angular/core';
@Component({
selector: 'app-greeting',
templateUrl: './greeting.component.html',
styleUrls: ['./greeting.component.css']
})
export class GreetingComponent {
// Hier kommt unsere Komponenten-Logik hin
}Der @Component-Dekorator ist ein TypeScript-Feature, das
der Klasse Metadaten hinzufügt. Diese Metadaten instruieren Angular über
die Verwendung der Komponente. Der selector definiert den
HTML-Tag, über den die Komponente später eingebunden wird – in diesem
Fall <app-greeting>. Damit verhält sich die
Komponente wie ein benutzerdefiniertes HTML-Element. Die Eigenschaft
templateUrl verweist auf die separate HTML-Datei mit der
Strukturdefinition, während styleUrls die für diese
Komponente verwendeten Stylesheets angibt.
Die Trennung in separate Dateien folgt dem Prinzip der Separation of Concerns: Logik, Struktur und Präsentation bleiben voneinander entkoppelt, was Wartbarkeit und Testbarkeit verbessert.
Eine statische Komponente ist wenig interessant. Angular-Komponenten leben von der Interaktion zwischen Datenmodell und Benutzeroberfläche. Die Komponenten-Klasse wird um Eigenschaften und Methoden erweitert:
export class GreetingComponent {
name: string = 'Welt';
changeName() {
this.name = 'Angular-Entwickler';
}
}Die Eigenschaft name bildet Teil des Datenmodells, die
Methode changeName() definiert Verhalten. Das Template
nutzt diese Elemente durch Angular’s Template-Syntax:
<div class="greeting-card">
<h2>Hallo, {{name}}!</h2>
<button (click)="changeName()">Ändere Namen</button>
</div>Zwei fundamentale Konzepte kommen hier zum Einsatz. Die Interpolation
mit doppelten geschweiften Klammern {{name}} bindet den
Wert der name-Eigenschaft in das Template ein. Angular
überwacht diese Bindung – ändert sich der Wert, aktualisiert das
Framework automatisch die Darstellung im DOM. Das Event-Binding
(click)="changeName()" verbindet das Klick-Ereignis mit der
Methode der Komponenten-Klasse. Die runden Klammern signalisieren einen
Datenfluss vom Template zur Komponente.
Das Styling in greeting.component.css demonstriert ein
weiteres wichtiges Konzept:
.greeting-card {
border: 2px solid #1976d2;
border-radius: 8px;
padding: 20px;
margin: 20px;
text-align: center;
background-color: #f5f5f5;
}
button {
background-color: #1976d2;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}Angular kapselt diese Styles automatisch. Sie gelten ausschließlich für diese spezifische Komponente und beeinflussen nicht den Rest der Anwendung. Diese Isolation basiert auf View Encapsulation und verhindert unerwünschte Stil-Überschneidungen – ein häufiges Problem in großen Webanwendungen.
Die Integration in die Hauptanwendung erfolgt durch Verwendung des
Selektors in app.component.html:
<div class="content">
<h1>Willkommen zur Angular-Komponenten Demo</h1>
<app-greeting></app-greeting>
</div>Nach dem Start mit ng serve ist die Anwendung unter
http://localhost:4200 erreichbar. Ein Klick auf den Button
demonstriert das reaktive Verhalten: Die Änderung der
name-Eigenschaft führt unmittelbar zur Aktualisierung der
Anzeige.
Was bei diesem einfachen Beispiel noch verborgen bleibt, ist der Mechanismus, der für die automatische Aktualisierung sorgt. Angular’s Change Detection überwacht Änderungen im Datenmodell und synchronisiert diese mit der Darstellung im DOM. Dieser Prozess läuft in mehreren Schritten ab:
Die Change Detection ist performanceoptimiert. Angular prüft nicht ständig alle Komponenten, sondern reagiert auf spezifische Auslöser: Benutzerinteraktionen, asynchrone Operationen wie HTTP-Requests oder Timer, und manuelle Trigger durch den Entwickler. Diese selektive Prüfung macht Angular-Anwendungen auch mit komplexen Komponentenbäumen effizient.
Einzelne Komponenten sind der Anfang. In realen Anwendungen kommunizieren Komponenten miteinander, bilden Hierarchien und tauschen Daten aus. Angular bietet dafür ein klares Kommunikationsmodell mit zwei Hauptrichtungen: Datenfluss von der Eltern- zur Kind-Komponente und Ereignisübertragung von Kind zu Eltern.
Eine neue Komponente demonstriert den Datenfluss nach unten:
ng generate component user-greetingDie UserGreetingComponent erhält Daten von außen über
das @Input()-Decorator:
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-user-greeting',
templateUrl: './user-greeting.component.html',
styleUrls: ['./user-greeting.component.css']
})
export class UserGreetingComponent {
@Input() userName: string = 'Gast';
}Das @Input()-Decorator markiert die
userName-Eigenschaft als Eingabe-Schnittstelle. Die
Eltern-Komponente kann über Property-Binding Werte übergeben:
<div class="greeting-card">
<h2>Hallo, {{name}}!</h2>
<button (click)="changeName()">Ändere Namen</button>
<app-user-greeting [userName]="name"></app-user-greeting>
</div>Die eckigen Klammern [userName]="name" signalisieren
Property-Binding. Der Wert der name-Eigenschaft aus der
Eltern-Komponente fließt in die userName-Eigenschaft der
Kind-Komponente. Diese Bindung ist reaktiv – ändert sich
name, erhält die Kind-Komponente automatisch den
aktualisierten Wert.
Der umgekehrte Datenfluss nutzt Event-Emitter. Eine Zähler-Komponente demonstriert dies:
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-counter',
templateUrl: './counter.component.html',
styleUrls: ['./counter.component.css']
})
export class CounterComponent {
count: number = 0;
@Output() countChanged = new EventEmitter<number>();
increment() {
this.count++;
this.countChanged.emit(this.count);
}
decrement() {
this.count--;
this.countChanged.emit(this.count);
}
}Das Template bindet die Methoden an Buttons:
<div class="counter">
<button (click)="decrement()">-</button>
<span>{{count}}</span>
<button (click)="increment()">+</button>
</div>Die Eltern-Komponente kann auf diese Ereignisse reagieren:
<app-counter (countChanged)="onCountChanged($event)"></app-counter>
<p>Aktueller Zählerstand: {{currentCount}}</p>Der EventEmitter ist ein Observable aus RxJS, Angular’s reaktiver
Programmierbibliothek. Die emit()-Methode sendet einen
Wert, die Eltern-Komponente empfängt ihn über Event-Binding. Das
spezielle $event-Objekt enthält den emittierten Wert.
Diese bidirektionale Kommunikation lässt sich schematisch darstellen:
graph TB
subgraph "Eltern-Komponente"
A[Daten: name]
end
subgraph "Kind-Komponente"
B[@Input userName]
C[@Output nameChanged]
end
A -->|Property Binding| B
C -->|Event Emitter| A
style A fill:#e1f5fe
style B fill:#c8e6c9
style C fill:#fff9c4
Angular-Komponenten durchlaufen einen definierten Lebenszyklus von der Initialisierung bis zur Zerstörung. Das Framework bietet Hook-Methoden, um an spezifischen Punkten dieses Zyklus Code auszuführen. Die wichtigsten Lifecycle-Hooks:
| Hook | Zeitpunkt | Verwendungszweck |
|---|---|---|
ngOnInit |
Nach Erstellung und Initialisierung der Input-Properties | Komponenteninitialisierung, Laden von Daten |
ngOnChanges |
Bei jeder Änderung der Input-Properties | Reaktion auf geänderte Eingabewerte |
ngAfterViewInit |
Nach Initialisierung der View | Zugriff auf Child-Komponenten, DOM-Manipulation |
ngOnDestroy |
Vor Zerstörung der Komponente | Aufräumen, Abmelden von Observables |
Eine Komponente, die alle Input-Änderungen protokolliert:
import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-user-profile',
templateUrl: './user-profile.component.html'
})
export class UserProfileComponent implements OnInit, OnChanges {
@Input() userId: number;
userData: any;
changeLog: string[] = [];
ngOnInit() {
console.log('Komponente initialisiert');
this.loadUserData();
}
ngOnChanges(changes: SimpleChanges) {
if (changes['userId']) {
const change = changes['userId'];
this.changeLog.push(
`userId geändert von ${change.previousValue} zu ${change.currentValue}`
);
if (!change.firstChange) {
this.loadUserData();
}
}
}
loadUserData() {
// Simulation eines HTTP-Requests
console.log(`Lade Daten für User ${this.userId}`);
}
}Die ngOnChanges-Methode erhält ein
SimpleChanges-Objekt mit Informationen über alle geänderten
Input-Properties. Jede Änderung enthält den aktuellen Wert
currentValue, den vorherigen Wert
previousValue und einen Boolean firstChange,
der angibt, ob dies die erste Änderung ist.
Der typische Lebenszyklus durchläuft diese Phasen:
Der Konstruktor wird für die grundlegende Klasseninitialisierung
verwendet, nicht für Komponentenlogik. Angular-spezifische
Initialisierung gehört in ngOnInit, da zu diesem Zeitpunkt
alle Dependencies injiziert und alle Input-Properties gesetzt sind.
Komponenten sollen wiederverwendbar und flexibel sein. Content Projection erlaubt es, Inhalte von außen in eine Komponente zu injizieren, ähnlich wie Slots in Web Components. Eine Card-Komponente demonstriert dieses Konzept:
import { Component } from '@angular/core';
@Component({
selector: 'app-card',
template: `
<div class="card">
<div class="card-header">
<ng-content select="[card-header]"></ng-content>
</div>
<div class="card-body">
<ng-content></ng-content>
</div>
<div class="card-footer">
<ng-content select="[card-footer]"></ng-content>
</div>
</div>
`,
styles: [`
.card {
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.card-header {
background-color: #f5f5f5;
padding: 12px;
border-bottom: 1px solid #ddd;
}
.card-body {
padding: 16px;
}
.card-footer {
background-color: #f5f5f5;
padding: 12px;
border-top: 1px solid #ddd;
}
`]
})
export class CardComponent {}Die Verwendung erfolgt durch Einfügen von Inhalten in die Komponenten-Tags:
<app-card>
<h3 card-header>Benutzerprofil</h3>
<p>Name: Max Mustermann</p>
<p>Email: max@example.com</p>
<button card-footer>Bearbeiten</button>
</app-card>Angular projiziert den Content an die entsprechenden
<ng-content>-Slots. Das select-Attribut
nutzt CSS-Selektoren, um spezifische Inhalte auszuwählen.
<ng-content> ohne select fungiert als
Standardslot für nicht-ausgewählte Inhalte.
Diese Technik ermöglicht hochflexible, wiederverwendbare Komponenten. Die Card-Komponente definiert Struktur und Styling, während der konkrete Inhalt von außen kommt. Das ist besonders wertvoll für UI-Bibliotheken und Design-Systeme.
Manchmal muss eine Komponente direkt auf ihre Child-Komponenten oder
DOM-Elemente zugreifen. Template-Referenzvariablen und der
@ViewChild-Dekorator bieten diese Möglichkeit.
Eine Template-Referenz wird mit dem Hash-Symbol definiert:
<input #nameInput type="text" placeholder="Name eingeben">
<button (click)="showValue(nameInput.value)">Wert anzeigen</button>Innerhalb des Templates ist nameInput eine Referenz auf
das DOM-Element. Für Zugriff aus der TypeScript-Klasse kommt
@ViewChild zum Einsatz:
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-input-demo',
template: `
<input #nameInput type="text" placeholder="Name eingeben">
<button (click)="focusInput()">Fokussieren</button>
`
})
export class InputDemoComponent implements AfterViewInit {
@ViewChild('nameInput') nameInput: ElementRef;
ngAfterViewInit() {
// View ist nun vollständig initialisiert
console.log('Input-Element:', this.nameInput.nativeElement);
}
focusInput() {
this.nameInput.nativeElement.focus();
}
}Der Zugriff auf ViewChild-Elemente ist erst nach
ngAfterViewInit sicher möglich, da Angular die View zu
diesem Zeitpunkt vollständig konstruiert hat. Der
ElementRef kapselt das native DOM-Element, zugänglich über
die nativeElement-Eigenschaft.
@ViewChild funktioniert auch mit Komponenten-Typen:
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { CounterComponent } from './counter.component';
@Component({
selector: 'app-counter-parent',
template: `
<app-counter #counter></app-counter>
<button (click)="resetCounter()">Zurücksetzen</button>
`
})
export class CounterParentComponent implements AfterViewInit {
@ViewChild('counter') counterComponent: CounterComponent;
ngAfterViewInit() {
console.log('Initialer Count:', this.counterComponent.count);
}
resetCounter() {
this.counterComponent.count = 0;
}
}Diese direkte Manipulation durchbricht das normale Datenfluss-Modell
und sollte sparsam eingesetzt werden. In den meisten Fällen ist
Kommunikation über @Input und @Output die
sauberere Lösung.
Angular’s Change Detection ist leistungsfähig, kann bei großen Komponentenbäumen aber zum Performance-Bottleneck werden. Die Standard-Strategie prüft bei jedem Event den gesamten Komponentenbaum. Die OnPush-Strategie optimiert dies:
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-user-card',
templateUrl: './user-card.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserCardComponent {
@Input() user: User;
}Mit OnPush prüft Angular die Komponente nur bei:
@Input-Referenz (nicht bei Mutation des
Objekts)ChangeDetectorRef.markForCheck()Diese Strategie erfordert immutables Datenmanagement. Statt Objekte zu mutieren, werden neue Instanzen erstellt:
// Falsch mit OnPush:
updateUser() {
this.user.name = 'Neuer Name';
}
// Richtig mit OnPush:
updateUser() {
this.user = { ...this.user, name: 'Neuer Name' };
}Der Performancegewinn kann erheblich sein. Große Listen mit hunderten Einträgen profitieren besonders:
Die Konzepte fügen sich in einer vollständigen Anwendung zusammen. Eine Todo-Verwaltung demonstriert Komponenten-Komposition, Datenfluss und State-Management:
ng generate component todo-manager
ng generate component todo-list
ng generate component todo-item
ng generate component todo-formDie Manager-Komponente hält den zentralen State:
import { Component } from '@angular/core';
interface Todo {
id: number;
title: string;
completed: boolean;
}
@Component({
selector: 'app-todo-manager',
templateUrl: './todo-manager.component.html',
styleUrls: ['./todo-manager.component.css']
})
export class TodoManagerComponent {
todos: Todo[] = [
{ id: 1, title: 'Angular Komponenten verstehen', completed: true },
{ id: 2, title: 'Komponenten-Kommunikation meistern', completed: false },
{ id: 3, title: 'Angular-Anwendung entwickeln', completed: false }
];
addTodo(title: string) {
if (title.trim()) {
const newId = Math.max(0, ...this.todos.map(t => t.id)) + 1;
this.todos = [...this.todos, { id: newId, title, completed: false }];
}
}
toggleTodo(id: number) {
this.todos = this.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
);
}
deleteTodo(id: number) {
this.todos = this.todos.filter(todo => todo.id !== id);
}
}Das Template orchestriert die Child-Komponenten:
<div class="todo-manager">
<h2>Aufgaben-Manager</h2>
<app-todo-form (addTodo)="addTodo($event)"></app-todo-form>
<app-todo-list
[todos]="todos"
(toggleTodo)="toggleTodo($event)"
(deleteTodo)="deleteTodo($event)">
</app-todo-list>
</div>Die Form-Komponente emittiert neue Todos:
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-todo-form',
templateUrl: './todo-form.component.html',
styleUrls: ['./todo-form.component.css']
})
export class TodoFormComponent {
@Output() addTodo = new EventEmitter<string>();
newTodoTitle = '';
onSubmit() {
this.addTodo.emit(this.newTodoTitle);
this.newTodoTitle = '';
}
}Das Template nutzt Two-Way-Binding mit ngModel:
<div class="todo-form">
<form (ngSubmit)="onSubmit()">
<input
type="text"
placeholder="Neue Aufgabe hinzufügen..."
[(ngModel)]="newTodoTitle"
name="todoTitle"
required>
<button type="submit" [disabled]="!newTodoTitle.trim()">Hinzufügen</button>
</form>
</div>Die Schreibweise [(ngModel)] kombiniert Property-Binding
und Event-Binding zu einer bidirektionalen Bindung. Sie ist Syntactic
Sugar für:
<input
[ngModel]="newTodoTitle"
(ngModelChange)="newTodoTitle = $event">Die List-Komponente leitet Ereignisse nur weiter:
import { Component, Input, Output, EventEmitter } from '@angular/core';
interface Todo {
id: number;
title: string;
completed: boolean;
}
@Component({
selector: 'app-todo-list',
templateUrl: './todo-list.component.html',
styleUrls: ['./todo-list.component.css']
})
export class TodoListComponent {
@Input() todos: Todo[] = [];
@Output() toggleTodo = new EventEmitter<number>();
@Output() deleteTodo = new EventEmitter<number>();
}Das Template iteriert mit *ngFor über die Todos:
<div class="todo-list">
<div *ngIf="todos.length === 0" class="empty-state">
Keine Aufgaben vorhanden. Fügen Sie eine neue Aufgabe hinzu!
</div>
<app-todo-item
*ngFor="let todo of todos"
[todo]="todo"
(toggle)="toggleTodo.emit($event)"
(delete)="deleteTodo.emit($event)">
</app-todo-item>
</div>Die strukturelle Direktive *ngFor ist Syntactic Sugar
für ein <ng-template>. Angular transformiert dies
intern zu:
<ng-template ngFor let-todo [ngForOf]="todos">
<app-todo-item [todo]="todo"></app-todo-item>
</ng-template>Die Item-Komponente ist eine reine Presentational Component:
import { Component, Input, Output, EventEmitter } from '@angular/core';
interface Todo {
id: number;
title: string;
completed: boolean;
}
@Component({
selector: 'app-todo-item',
templateUrl: './todo-item.component.html',
styleUrls: ['./todo-item.component.css']
})
export class TodoItemComponent {
@Input() todo: Todo;
@Output() toggle = new EventEmitter<number>();
@Output() delete = new EventEmitter<number>();
}Das Template bindet die Checkbox an den completed-Status:
<div class="todo-item" [class.completed]="todo.completed">
<label>
<input
type="checkbox"
[checked]="todo.completed"
(change)="toggle.emit(todo.id)">
{{ todo.title }}
</label>
<button (click)="delete.emit(todo.id)">Löschen</button>
</div>Die Syntax [class.completed] ist Class-Binding. Es fügt
die CSS-Klasse completed hinzu, wenn die Bedingung
todo.completed wahr ist. Dies ist präziser als
[ngClass] für einzelne Klassen.
Die Komponentenarchitektur folgt dem Smart/Presentational-Pattern:
Smart Components (Container) halten State und Geschäftslogik. Presentational Components sind zustandslos und rein darstellend. Diese Trennung verbessert Testbarkeit und Wiederverwendbarkeit.
Manchmal muss eine Anwendung Komponenten zur Laufzeit dynamisch erzeugen. Ein typisches Szenario sind Dialoge oder Modals. Angular bietet dafür die ComponentFactory:
import { Component, ViewChild, ViewContainerRef, ComponentFactoryResolver } from '@angular/core';
import { AlertComponent } from './alert.component';
@Component({
selector: 'app-root',
template: `
<button (click)="showAlert()">Alert anzeigen</button>
<ng-template #alertContainer></ng-template>
`
})
export class AppComponent {
@ViewChild('alertContainer', { read: ViewContainerRef })
container: ViewContainerRef;
constructor(private resolver: ComponentFactoryResolver) {}
showAlert() {
const factory = this.resolver.resolveComponentFactory(AlertComponent);
const componentRef = this.container.createComponent(factory);
componentRef.instance.message = 'Dies ist eine dynamische Komponente!';
}
}Der ViewContainerRef repräsentiert einen Container, in
den Komponenten injiziert werden können. Die
ComponentFactory erzeugt Instanzen einer Komponente. Die
Referenz auf die erzeugte Komponente erlaubt Zugriff auf ihre Properties
und Methoden.
Ab Angular 13 vereinfacht die neue API dies erheblich:
showAlert() {
const componentRef = this.container.createComponent(AlertComponent);
componentRef.instance.message = 'Dies ist eine dynamische Komponente!';
}Die ComponentFactoryResolver wird nicht mehr benötigt. Die API ist direkter und intuitiver.
Bei komplexeren Komponentenhierarchien wird die Weitergabe von Daten durch mehrere Ebenen umständlich. Services bieten eine elegantere Lösung für gemeinsam genutzte State oder Kommunikation über größere Distanzen im Komponentenbaum:
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class NotificationService {
private messageSource = new BehaviorSubject<string>('');
message$: Observable<string> = this.messageSource.asObservable();
sendMessage(message: string) {
this.messageSource.next(message);
}
}Der Service verwendet ein BehaviorSubject aus RxJS. Dies
ist ein spezieller Observable-Typ, der seinen aktuellen Wert speichert
und bei Subscription sofort emittiert. Komponenten können den Service
injizieren und Subscribe auf den Observable:
import { Component, OnInit } from '@angular/core';
import { NotificationService } from './notification.service';
@Component({
selector: 'app-notification-display',
template: `
<div class="notification" *ngIf="message">
{{ message }}
</div>
`
})
export class NotificationDisplayComponent implements OnInit {
message: string;
constructor(private notificationService: NotificationService) {}
ngOnInit() {
this.notificationService.message$.subscribe(msg => {
this.message = msg;
});
}
}Eine andere Komponente kann Nachrichten senden:
import { Component } from '@angular/core';
import { NotificationService } from './notification.service';
@Component({
selector: 'app-notification-sender',
template: `
<button (click)="notify()">Nachricht senden</button>
`
})
export class NotificationSenderComponent {
constructor(private notificationService: NotificationService) {}
notify() {
this.notificationService.sendMessage('Hallo von einer anderen Komponente!');
}
}Diese Komponenten können überall im Komponentenbaum platziert sein.
Der Service fungiert als Kommunikations-Hub. Das
providedIn: 'root' im Injectable-Dekorator macht den
Service zu einem Singleton – alle Komponenten teilen dieselbe
Instanz.
Die Async-Pipe vereinfacht den Subscribe-Code:
@Component({
selector: 'app-notification-display',
template: `
<div class="notification" *ngIf="message$ | async as message">
{{ message }}
</div>
`
})
export class NotificationDisplayComponent {
message$ = this.notificationService.message$;
constructor(private notificationService: NotificationService) {}
}Die Async-Pipe subscribed automatisch, handled Unsubscribe bei Komponenten-Zerstörung und triggert Change Detection bei neuen Werten. Dies reduziert Boilerplate und verhindert Memory Leaks durch vergessene Unsubscribes.