| Typ | Konzept |
|---|---|
| Quellen | Quelle - Angular Mastery Workshop Quelle - Recherche - Angular heute 2026 |
| Erstellt | 2026-09-26 |
| Aktualisiert | 2026-09-26 |
| Tags | angular, webentwicklung, benutzeroberfläche |
Der zentrale Baustein einer Angular-Oberfläche: eine TypeScript-Klasse mit HTML-Template und eigenen Styles. Komponenten zeigen Daten an, reagieren auf Benutzeraktionen und sind zu einem Baum verschachtelt. Sie kommunizieren über Inputs und Outputs und durchlaufen einen festen Lebenszyklus.
Code-Beispiele aus Quelle - Angular Mastery Workshop (Folie 155–342), von den Folienbildern abgeschrieben (Stand Angular 12, siehe die Widersprüche unten).
Aufbau
Eine Komponente besteht aus Logik (TypeScript-Klasse), Template (HTML) und Styles (CSS/SCSS). Über den Selektor wird sie als eigenes HTML-Tag verwendet. Template und Styles können inline stehen oder in eigenen Dateien liegen (Folie 155–156):
import { Component, Input, Output } from '@angular/core';
import { Subject } from 'rxjs';
import { User } from '../model/user';
@Component({
selector: 'user-item', // wird als <user-item> verwendet
template: `
<h1>{{user.name}} {{user.surname}}</h1>
<p>{{user.address}}</p>
<button (click)="edit(user.id)">Edit</button>
`,
styles: [
`:host { display: flex; border: 1px solid #000; padding: 20px; }`
]
})
export class UserItemComponent {
@Input() user: User;
@Output() editUser = new Subject<string>();
edit(id: string) {
this.editUser.next(id);
}
}
Eine Oberfläche ist ein Komponentenbaum, z.B. Kundenansicht → Liste → Suche, Filter, Eintrag, Detail (Folie 154). Jede Webanwendung hat laut Kurs im Kern nur zwei Aufgaben: Daten passend anzeigen und Daten auf Benutzeraktion hin ändern (Folie 194).
Daten anzeigen – Schritt für Schritt
Der Kurs baut eine Kundenkomponente Folie für Folie auf (Folie 195–212). Zuerst nur ein HTML-Template mit Selektor und Inline-Styles, dann eine öffentliche Eigenschaft customer mit Testdaten (öffentliche Eigenschaften und Methoden sind im Template verfügbar). Das Endergebnis:
import { Component } from '@angular/core';
const MOCK_CUSTOMER = {
name: 'John',
surname: 'Snow',
isVip: true,
tags: ['king in the north', 'bastard', 'targaryen']
}
@Component({
selector: 'my-org-customer-item',
template: `
<div [ngClass]="{ vip: customer.isVip }">
<h1>Hello {{customer.name}} {{customer.surname}}</h1>
<ul>
<li *ngFor="let tag of customer.tags">{{tag}}</li>
</ul>
</div>
`,
styles: [
' h1 { color: blue; } ',
' div.vip { border: 1px solid gold }'
]
})
export class CustomerComponent {
customer = MOCK_CUSTOMER;
constructor() {}
}
{{ … }}Interpolation zeigt Text an. Sie ist der beste Weg für Text in einem Element, für Attribute sollte man sie nicht verwenden (Folie 199).*ngForwiederholt das ganze Element (hier<li>) für jeden Eintrag und deklariert die lokale Variabletag.*ngForstammt aus demCommonModule, das importiert sein muss (Folie 205–206).[ngClass]ist ein Property-Binding. Mit[ ]kann man Objekte und Arrays übergeben, Interpolation kann nur Strings.ngClasssetzt mehrere Klassen auf einmal:{ class1: condition1, class2: … }(Folie 210–212).- Styles gelten nur für diese Komponente, Angular setzt dazu automatisch Präfixe. Man muss sich also nicht um eindeutige Klassennamen kümmern (Folie 209).
Ereignisse
Mit ( ) bindet man an DOM-Ereignisse. Der Handler ist eine öffentliche Methode der Klasse und bekommt beliebige Daten aus dem Template oder mit $event das DOM-Ereignis selbst (Folie 217):
<button (click)="addToCart(order)">Add to cart</button>
<text-area (focus)="onFocus()"></text-area>
<input (keyup.enter)="onEnter($event)" />
<input (blur)="onBlur()" />
<input (keyup)="onKey($event)" />
<div (mouseenter)="highlight(true)" (mouseleave)="highlight(false)"></div>
@Component({
selector: 'my-org-example',
template: '...'
})
export class ExampleComponent {
addToCart(order: Order) { /* ... */ }
onFocus() { /* ... */ }
onEnter(event: KeyEvent) { /* ... */ }
onBlur() { /* ... */ }
onKeyup(event: KeyEvent) { /* ... */ }
highlight(isHighlight: boolean) { /* ... */ }
}
(Auf der Folie heisst es einmal hihlight statt highlight, im Template onKey und in der Klasse onKeyup. Den Typ KeyEvent gibt es im DOM nicht, gemeint ist KeyboardEvent.)
Kommunikation: Input und Output
Eltern geben Daten mit @Input() nach unten, Kinder melden Ereignisse mit @Output() nach oben. Beispiel: eine wiederverwendbare Tag-Liste im SharedModule (Folie 222–224).
Verwendung im Eltern-Template:
<my-org-tag-list
[tags]="customer.tags"
(add)="addTag($event)"
(remove)="removeTag($event)">
</my-org-tag-list>
Die Kind-Komponente mit ihrem (vereinfachten) Template tag-list.component.html:
<input
[value]="newTag"
(keyup)="updateNewTag($event)"
(keydown.enter)="addTag()"
/>
<button
[disabled]="newTag.length === 0"
(click)="addTag()">
Add tag
</button>
<div *ngFor="let tag of tags">
{{tag}}
<button (click)="removeTag(tag)">
Remove
</button>
</div>
import { Component, Input, OnInit, Output } from '@angular/core';
import { Subject } from 'rxjs';
@Component({
selector: 'my-org-tag-list',
templateUrl: './tag-list.component.html', // Folie: template (Tippfehler)
styleUrls: ['./tag-list.component.scss'],
})
export class TagListComponent implements OnInit {
@Input() tags: string[];
@Output() add = new Subject<string>();
@Output() remove = new Subject<string>();
newTag: string; // lokaler Zustand
ngOnInit() {
this.newTag = '';
}
updateNewTag(event: KeyboardEvent) {
this.newTag = (event.target as HTMLInputElement).value;
}
addTag() {
if (this.newTag.length) {
this.add.next(this.newTag);
this.newTag = '';
}
}
removeTag(tag: string) {
this.remove.next(tag);
}
}
Die Eltern-Komponente ändert ihre Daten darauf ohne das alte Array zu verändern (immutable). Im Template kommt $event an, in der Methode weiss man, dass es ein String ist:
@Component({ /* ... */ })
export class CustomerItemComponent {
customer: Customer = { name: 'John', surname: 'Snow', tags: ['the king in the north', 'bastard'] };
addTag(tagToAdd: string) {
this.customer = {
...this.customer,
tags: [...this.customer.tags, tagToAdd]
}
}
removeTag(tagToRemove: string) {
this.customer = {
...this.customer,
tags: this.customer.tags.filter(tag => tag !== tagToRemove)
}
}
}
(Auf der Folie steht nach tagToAdd] ein Semikolon statt }, das wäre ein Syntaxfehler.)
Outputs werden meist mit EventEmitter statt Subject gebaut. Beides funktioniert, weil EventEmitter von Subject erbt. Heute gibt es zudem signalbasierte Funktionen (input(), output(), model()), deren Einführungsversion in der Recherche nicht eigens belegt ist.
Lebenszyklus
Angular ruft Methoden mit dem Präfix ng zu festen Zeitpunkten auf (Folie 226–238):
| Hook | Wann | Wofür |
|---|---|---|
ngOnChanges |
zuerst und bei jeder Änderung eines Inputs | abgeleitete Werte einmal berechnen und speichern |
ngOnInit |
einmal, wenn Inputs gesetzt sind | alle Initialisierung (nicht im Konstruktor) |
ngDoCheck |
bei jedem Durchlauf der Change Detection | Änderungen erkennen, die Angular selbst nicht sieht; teuer |
ngAfterContentInit/Checked |
nach dem Einfügen von projiziertem Inhalt | siehe Content Projection |
ngAfterViewInit/Checked |
nach Aufbau der eigenen Ansicht | Zugriff auf Kind-Elemente mit @ViewChild |
ngOnDestroy |
zuletzt, vor dem Entfernen | aufräumen: Timer, Abonnements, sonst Speicherlecks |
Jeder Hook hat ein Interface in @angular/core. Das Interface ist nicht Pflicht, die Methode würde auch ohne aufgerufen, aber es hilft (Folie 227):
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'my-org-tag-list',
// ...
})
export class TagListComponent implements OnInit {
newTag: string;
constructor() {} // Konstruktor leer lassen
ngOnInit() {
this.newTag = ''; // an das Template gebundene Werte hier initialisieren
}
}
ngOnChanges bekommt ein SimpleChanges-Objekt mit einem Eintrag pro Input (Folie 229):
import { Component, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'my-org-tag-list',
// ...
})
export class TagListComponent implements OnChanges {
@Input() tags: string[]
constructor() {}
ngOnChanges(changes: SimpleChanges) {
changes.tags.previousValue; // undefined
changes.tags.currentValue; // ['king in the north', 'bastard']
changes.tags.firstChange; // true
changes.tags.isFirstChange(); // true
}
}
Performance-Beispiel: abgeleiteten Wert in ngOnChanges speichern statt eine Funktion im Template aufzurufen. Die Funktion liefe bei jedem Durchlauf der Change Detection, in komplexen Apps viele Male pro Sekunde (Folie 230):
@Component({
selector: 'my-org-order-list',
template: `
// display orders ...
Total GOOD perf: {{totalOrderValue | currency }} CHF
Total BAD perf: {{getTotalOrderValue() | currency }} CHF
`
})
export class OrderListComponent implements OnChanges {
@Input() orders: Order[];
totalOrderValue = 0;
ngOnChanges(changes: SimpleChanges) { // GUT: nur wenn sich orders ändert
if (changes.orders) {
this.totalOrderValue = orders.reduce((total, order) => total + order.value, 0);
}
}
// vs ...
getTotalOrderValue(): number { // SCHLECHT: bei jedem Tick
return orders.reduce((total, order) => total + order.value, 0);
}
}
(Auf der Folie fehlt beide Male this. vor orders.)
ngOnDestroy: Ein Timer, der nicht gestoppt wird, läuft nach dem Entfernen der Komponente weiter, z.B. nach einer Navigation oder durch *ngIf. Das ist ein Speicherleck (Folie 232):
import { Component, OnDestroy, OnInit } from '@angular/core';
@Component({
selector: 'my-org-timer',
template: `<p>Elapsed {{elapsed}} seconds</p>`
})
export class TimerComponent implements OnInit, OnDestroy {
private intervalId;
elapsed = 0;
ngOnInit() {
this.intervalId = setInterval(() => this.elapsed++, 1000);
}
ngOnDestroy() {
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
}
Reaktive Variante mit RxJS: takeUntil beendet das Abonnement, sobald destroy feuert (Folie 233):
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Observable, Subject, timer } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'my-org-timer-reactive',
template: `<p>Elapsed {{elapsed}} seconds</p>`
})
export class TimerComponent implements OnInit, OnDestroy {
private destroy = new Subject<void>()
elapsed: number;
ngOnInit() {
timer(0, 1000)
.pipe(takeUntil(this.destroy))
.subscribe(value => this.elapsed = value)
}
ngOnDestroy() {
this.destroy.next();
this.destroy.complete();
}
}
Noch einfacher mit der async-Pipe, die sich selbst abmeldet:
@Component({
selector: 'my-org-timer-reactive',
template: `<p>Elapsed {{elapsed | async}} seconds</p>`
})
export class TimerComponent implements OnInit {
elapsed: Observable<number>;
ngOnInit() { this.elapsed = timer(0, 1000); }
}
ngDoCheck läuft bei jedem Durchlauf, auch wenn ngOnChanges nicht aufgerufen wird. Mit OnPush wird ngOnChanges nur aufgerufen, wenn sich die Referenz des Inputs ändert. user.name = 'changed name' im Elternteil löst es also nicht aus (Folie 236):
import { Component, OnInit, Input, ChangeDetectionStrategy, DoCheck, OnChanges } from '@angular/core';
@Component({
selector: 'my-org-user',
template: `Here is the user name: {{ user.name }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnInit {
@Input() user;
ngOnChanges(){
console.log("CHANGES") // nur bei neuem user-Objekt
}
ngDoCheck(){
console.log("DO CHECK") // immer
}
}
export interface User {
name: string;
}
Folie 236 sagt, ohne OnPush werde ngOnChanges bei jeder Änderung einer Eigenschaft des Objekts aufgerufen („deep equals checking“). Das stimmt nicht: ngOnChanges reagiert in beiden Strategien nur auf eine neue Referenz. Der Unterschied von OnPush liegt darin, dass die Komponente selbst seltener geprüft und neu gezeichnet wird (Einordnung Claude, nach der Angular-Dokumentation zu ngOnChanges).
Content-Hooks reagieren auf Inhalt, der von aussen in die Komponente projiziert wird, z.B. Menüeinträge in ein Menü (Folie 237):
<my-org-menu>
<my-org-menu-item></my-org-menu-item>
</my-org-menu>
View-Hooks: Erst nach dem Aufbau der Ansicht hat man Zugriff auf Elemente daraus (Folie 238):
@Component({
selector: 'my-org-customer-list',
template: `<input #searchInput matInput />`
})
export class CustomerListComponent implements OnInit {
@ViewChild('searchInput', { static: false, read: MatInput }) searchInput: MatInput;
ngAfterViewInit() { this.searchInput.focus(); } // Folie: ngOnViewInit (Tippfehler)
}
(Die Klasse müsste AfterViewInit statt OnInit implementieren.)
Direktiven
Es gibt drei Arten (Folie 329): Komponenten (Direktiven mit Template, technisch sind Komponenten Direktiven), Attribut-Direktiven (ändern Verhalten oder Aussehen eines Elements) und strukturelle Direktiven (ändern den Aufbau des DOM, erkennbar am *).
Die eingebauten im Einsatz (Folie 159):
@Component({
selector: 'my-org-user',
template: `
<my-org-spinner *ngIf="loading"></my-org-spinner> <!-- strukturell -->
<my-org-user-list *ngIf="!loading">
<my-org-user-item *ngFor="let user of users" [user]="user"></my-org-user-item>
</my-org-user-list>
<button mat-raised-button (click)="createNewUser()">Create new user</button> <!-- Attribut -->
`
})
export class UserComponent {
// ...
}
Eigene Attribut-Direktive: [ ] im Selektor bedeutet Attribut-Selektor. ElementRef gibt Zugriff auf das Element (Folie 330):
import { Directive, ElementRef } from '@angular/core';
@Directive({
selector: '[myOrgCoin]',
})
export class CoinDirective {
constructor(el: ElementRef) {
el.nativeElement.style.textAlign = 'right';
}
}
<p myOrgCoin>{{ customer.lifetimeOrderValue | coinFormat }}</p>
Host-Interaktion: @HostBinding bindet eine Eigenschaft des Elements, @HostListener hört auf dessen Ereignisse (Folie 331–333):
import { Directive, ElementRef, HostBinding, HostListener } from '@angular/core';
@Directive({
selector: '[blueOnClick]'
})
export class BlueOnClickDirective {
@HostBinding('style.background-color') backgroundColor;
@HostListener('click', ['$event'])
onClick($event) {
this.backgroundColor = 'blue';
}
constructor(private el: ElementRef) {
this.backgroundColor = el.nativeElement.style['background-color'];
}
}
<div blueOnClick style="background-color: green; width: 400px; height: 400px">
</div>
<!-- Klick färbt das grüne Quadrat blau -->
Eigene strukturelle Direktive, ein nachgebautes ngIf. TemplateRef ist die Vorlage, ViewContainerRef der Ort, an dem sie eingefügt wird (Folie 335–337):
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[myNgIf]'
})
export class MyNgIfDirective {
@Input() set myNgIf(condition: boolean) {
if (condition) {
this.view.createEmbeddedView(this.template);
} else {
this.view.clear();
}
}
constructor(private view: ViewContainerRef,
private template: TemplateRef<any>) {
}
}
<h1 *myNgIf="true">Game of Thrones</h1> <!-- wird angezeigt -->
<h1 *myNgIf="false">Game of Thrones</h1> <!-- wird nicht angezeigt -->
Ohne * wird die Direktive nicht als strukturelle behandelt und funktioniert nicht.
Pipes
Pipes formen Daten im Template um. „Pure“ Pipes speichern ihr Ergebnis und rechnen nur bei neuen Eingaben neu (Folie 161–162, 326):
<h1>{{'user.view.title' | translate}}</h1> <!-- ngx-translate oder eigene Pipe -->
<user-list>
<user-item *ngFor="let user of users | async" [user]="user"></user-item> <!-- Stream abonnieren -->
</user-list>
<p>{{'user.view.last-updated' | translate }} {{lastUpdated | date : 'longDate' }}</p> <!-- mit Parameter -->
Eingebaute Pipes (Folie 327): AsyncPipe, CurrencyPipe, DatePipe, DecimalPipe, I18nPluralPipe, I18nSelectPipe, JsonPipe, KeyValuePipe, LowerCasePipe, PercentPipe, SlicePipe, TitleCasePipe, UpperCasePipe (dazu veraltete Deprecated…-Varianten).
Eigene Pipe (Folie 328). Aus 1000000 wird 1 000 000.00:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'coinFormat', // Verwendung: value | coinFormat
})
export class CoinFormatPipe implements PipeTransform {
transform(value?: number): string {
if (value === null || value === undefined) {
return '0.00';
} else {
return value.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$& ');
}
}
}
Der reguläre Ausdruck findet jede Ziffer, auf die eine durch drei teilbare Anzahl Ziffern bis zum Dezimalpunkt folgt, und hängt ein Leerzeichen an.
@ViewChild und Content Projection
@ViewChild gibt direkten Zugriff auf eine Kind-Komponente, hier über die Template-Variable #searchQuery, weil es mehrere matInput geben könnte (Folie 340):
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatInput } from '@angular/material';
@Component({
selector: 'my-org-customers',
template: `
<input #searchQuery matInput type="text" />
`
})
export class CustomersComponent implements OnInit {
@ViewChild('searchQuery', { static: true, read: MatInput }) searchQueryInput: MatInput;
ngOnInit() {
this.searchQueryInput.focus();
}
}
(static: true macht das Element schon in ngOnInit verfügbar. Mit static: false wie auf Folie 238 erst in ngAfterViewInit. Der Import lautet seit Angular 9 @angular/material/input, Einordnung Claude.)
Content Projection: Alles zwischen den Tags einer Komponente wird in ihr <ng-content> eingesetzt (Folie 341–342):
<mat-card>
<mat-card-header>
<div mat-card-avatar><img src="avatar.png" /></div>
<mat-card-title>John Snow</mat-card-title>
<mat-card-subtitle>King in the North</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p>John was a ...</p>
</mat-card-content>
<mat-card-actions>
<button mat-button>Edit</button>
<button mat-button>Remove</button>
</mat-card-actions>
</mat-card>
Mit select lässt sich Inhalt gezielt verteilen, nach Element, CSS-Klasse oder Attribut:
@Component({
selector: 'my-org-main-component',
template: `
<mat-card>
<ng-content></ng-content>
</mat-card>
<div class="action">
<ng-content select="[mat-button]"></ng-content>
</div>
`
})
export class MainComponent {}
<my-org-main-component>
<p>Some content that will be projected into ng-content of main component</p>
<button mat-button>Do something</button> <!-- landet im div.action -->
</my-org-main-component>
Change Detection
Nach jedem Ereignis prüft Angular, ob sich angezeigte Daten geändert haben, und aktualisiert das DOM. Den Anstoss gibt Zone.js: Es „patcht“ Browser-APIs wie setTimeout oder Promise und merkt so, wann etwas passiert ist. Man kann es meistens ignorieren. Nur aus Performance-Gründen lässt man z.B. setInterval ausserhalb der Zone laufen (Folie 353–354). Mit OnPush prüft Angular eine Komponente nur, wenn sich die Referenz eines Inputs ändert oder ein Ereignis in ihr auftritt. Das ist schneller, setzt aber Unveränderlichkeit voraus (Folie 236).
Kurs: Listen und Bedingungen mit *ngFor und *ngIf aus dem CommonModule. Zone.js arbeitet unsichtbar, OnPush ist eine Optimierung. Komponenten gehören zu einem NgModule (Folie 205, 236, 354).
Heute: Seit v17 gibt es eine eingebaute Syntax (Control Flow): @if (…) { … } @else { … }, @for (tag of tags; track tag) { … }, @switch und @defer. Seit v19 sind Komponenten standardmässig standalone. Seit v21 laufen neue Projekte ohne Zone.js („zoneless“), angestossen durch Signals und Ereignisse. Seit v22 ist OnPush der Standard, bestehende Komponenten können per Migration auf „Eager“ bleiben (Quelle - Recherche - Angular heute 2026).
Das Kunden-Beispiel sähe mit der neuen Syntax so aus (Einordnung Claude):
<div [class.vip]="customer.isVip">
<h1>Hello {{customer.name}} {{customer.surname}}</h1>
<ul>
@for (tag of customer.tags; track tag) {
<li>{{tag}}</li>
}
</ul>
</div>
Verwandt
- Angular – Überblick über alle Bausteine
- Reaktive Programmierung – Streams,
| asyncund Signals - State Management – wo der Zustand von Komponenten liegt
- Softwaretest – Komponententests mit TestBed
- Angular-Architektur – in welchem Teil der App eine Komponente liegt
