8 Angular Komponenten

8.1 Der praktische Einstieg

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 greeting

Der 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.

8.2 Interaktivität durch Datenbindung

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.

8.3 Der Mechanismus dahinter

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.

8.4 Komponenten-Hierarchien und Datenfluss

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-greeting

Die 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

8.5 Komponenten-Lebenszyklus

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.

8.6 Content Projection mit ng-content

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.

8.7 Template-Referenzen und ViewChild

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.

8.8 Change Detection Strategien

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:

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:

8.9 Eine praxisnahe Todo-Anwendung

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-form

Die 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.

8.10 Dynamische Komponenten

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.

8.11 Komponenten-Kommunikation über Services

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.