Ein Angular-Bildschirm besteht aus Komponenten: einer Klasse für den Zustand, einer Vorlage (HTML) für die Darstellung und einem Stylesheet. Der Zustand liegt in Signals; ändern sie sich, aktualisiert
Angular genau die Stellen der Vorlage, die sie lesen. Diese Lektion baut daraus die erste Kladde-Seite: die Kontaktliste aus festen Daten mit einem Filterfeld. Die API kommt in 0202. Sie arbeiten im Projekt aus
0101 (web/kladde-angular, Präfix kl).
Stand der Prüfung. Der Code der Lektion (Dienst, Komponente, Vorlage; dazu je ein Test für Liste und Wurzel, hier nicht abgedruckt, 0401) lief in einem Projekt mit Angular 22.2, TypeScript 6.0.3 und Node 24.22.3:
ng buildExit-Code 0,ng test --no-watchExit-Code 0 (2 Dateien, 2 Tests bestanden, Vitest 5.1.0); mit.startsWithstatt.includesim Filter Exit-Code 1. Die Snippets zueffectundlinkedSignalübersetzten in der Klasse der nächsten Lektion (ng build, Exit-Code 0; die Tests liefen mit demeffectgrün); sie gehören nicht zur Lösung. Ebenfalls ausgeführt: fehlendesimports(NG8001) und fehlendestrack(NG5002) brachen den Bau mit Exit-Code 1. Nicht ausgeführt: ein Browser, ein Screenreader, die Bedienung mit der Tastatur; die Sätze dazu sind Folgerungen aus dem HTML, nicht Beobachtungen. Aussagen über Angular stammen von angular.dev (Seiten zu Komponenten, Bindungen, Signals, Steuerfluss, Zoneless, Style Guide, am Schreibtag gelesen); Zitate sind im Original englisch.
Die Komponente
Der Kopf der Komponente KontaktListe (aus ng generate component kontakte/kontakt-liste, 0101):
@Component({
imports: [],
selector: 'kl-kontakt-liste',
styleUrl: './kontakt-liste.scss',
templateUrl: './kontakt-liste.html',
})
export class KontaktListe {}
Der Selektor ist der Elementname in anderen Vorlagen: <kl-kontakt-liste />. templateUrl und styleUrl verweisen auf Dateien neben der Klasse („relative to the directory in which the component resides“);
kurze Vorlagen dürfen mit template: in die Klasse. Komponenten sind standalone: Wer eine andere Komponente in seiner Vorlage benutzt, trägt sie in imports ein. Die Seite „Components“ sagt: „In Angular versions
before 19.0.0, the standalone option defaults to false“; heute brauchen Sie dazu nichts zu schreiben, und die CLI erzeugt keine NgModules. Die Wurzel App (0101) holt die Liste mit imports: [KontaktListe] in ihre Vorlage. Vergessen Sie den Eintrag in imports, bricht der Bau mit NG8001: 'kl-kontakt-liste' is not a known element ab (Exit-Code 1); der Fehler kommt vor dem Browser.
Bindungen
Die Vorlage ist HTML mit vier Ergänzungen (Seite „Binding dynamic text, properties and attributes“):
| Schreibweise | Richtung | Beispiel |
|---|---|---|
{{ ausdruck }} |
Klasse nach Text | {{ kontakt.name }} |
[eigenschaft]="ausdruck" |
Klasse nach Eigenschaft des Elements oder der Komponente | [value]="filter()" |
[attr.name]="ausdruck" |
Klasse nach Attribut; null oder undefined entfernt es |
[attr.aria-label]="beschriftung()" |
(ereignis)="aufruf" |
Element nach Klasse | (input)="filterGeaendert($event)" |
Zu {{ }} sagt die Seite: „Expression values are converted to a string“ und „null and undefined render as an empty string“. [class.aktiv]="ausdruck" schaltet eine Klasse, [style.height.px]="ausdruck" einen
Stil. Die Vorlage kann nur lesen, was die Klasse sichtbar macht: Der Style Guide empfiehlt protected für Mitglieder, die nur die Vorlage braucht, und readonly für Unveränderliches.
Signals: signal, computed, effect, linkedSignal
Ein Signal ist, nach der Seite „Signals“, „a wrapper around a value that notifies interested consumers when that value changes“. Man liest es, indem man es aufruft:
const anzahl = signal(0);
console.log(anzahl()); // lesen
anzahl.set(3); // ersetzen
anzahl.update((v) => v + 1); // aus dem alten Wert berechnen
computed leitet einen Wert aus anderen Signals ab: const doppelt = computed(() => anzahl() * 2). Die Seite hebt hervor, dass ein computed lazy und gemerkt ist (die Ableitung läuft beim ersten Lesen,
danach kommt der Wert aus dem Zwischenspeicher, bis sich eine Abhängigkeit ändert), dass es nur lesbar ist (doppelt.set(3) ist ein Übersetzungsfehler) und dass man darin „safely perform computationally
expensive derivations … such as filtering arrays“ darf. Nur Signals, die bei der Berechnung wirklich gelesen werden, zählen als Abhängigkeit. Für die Kladde: Der Filter ist ein signal, die sichtbare Liste ein
computed aus Kontakten, Filter und Sortierung.
effect führt Code aus, wenn sich gelesene Signals ändern. Die Seite sagt, man nehme ihn „for those specific use cases“, in denen man auf Änderungen reagiert und eine Programmoberfläche anspricht, die nichts von
Signals weiß; Ableitungen gehören nicht hinein („Synchronous or asynchronous derivations are recommended when we want to react to state changes“: also computed oder resource). Ein vertretbarer Fall in der Kladde ist, den Filter im sessionStorage des Browsers zu spiegeln (im Konstruktor oder Feld der Klasse):
constructor() {
effect(() => sessionStorage.setItem('kl-filter', this.filter()));
}
Die Faustregel: Wert aus Werten ist computed; Reaktion nach außen ist effect.
linkedSignal ist ein beschreibbares Signal, das von anderen abhängt. Die Seite „Dependent state with linkedSignal“: „The linkedSignal function lets you create a signal to hold some state that is intrinsically
linked to some other state.“ Ihr Beispiel ist eine Auswahl, die auf den ersten Eintrag zurückfällt, wenn sich die Liste ändert; für die Kladde:
protected readonly ersterTreffer = linkedSignal(() => this.treffer()[0]);
Steuerfluss in der Vorlage
Die Vorlage hat eingebaute Blöcke (Seite „Control flow“):
@if (filter() !== '') {
<button type="button" (click)="filter.set('')">Filter löschen</button>
}
@for (kontakt of treffer(); track kontakt.id) {
<li>{{ kontakt.name }}</li>
} @empty {
<li>Keine Kontakte gefunden.</li>
}
@switch (sortierung()) {
@case ('name') { Name }
@case ('company') { Firma }
}
@ifhat@else ifund@else;@if (ausdruck; as name)legt das Ergebnis in eine Variable.@forbrauchttrack(ohne bricht der Bau mitNG5002: @for loop must have a "track" expressionab): Der Ausdruck „allows Angular to maintain a relationship between your data and the DOM nodes“. Nehmen Sie, wie die Seite rät, eine Eigenschaft, die jeden Eintrag eindeutig kennzeichnet (hierid).$indextaugt nur für Listen, die sich nie ändern, der Eintrag selbst als Schlüssel ist laut Seite der letzte Ausweg.@emptyläuft bei leerer Sammlung;continueundbreakgibt es nicht.@switchvergleicht mit===, ohne Durchfallen; aufeinanderfolgende@caseteilen sich einen Block.
Dienste mit inject()
Ein Dienst ist eine Klasse mit eigener Aufgabe, die Komponenten sich holen. Die CLI erzeugt ihn mit @Service() (0101); der Style Guide empfiehlt, ihn mit der Funktion inject zu holen statt über Konstruktor-Parameter:
private readonly daten = inject(KontaktDaten);
protected readonly kontakte = signal(this.daten.liste());
Für Sie als .NET-Entwicklerin: Das entspricht der Konstruktorinjektion, mit
einem Singleton im Anwendungsbereich als Voreinstellung (@Service stellt nach der Seite die Klasse „at the root injector“ bereit).
OnPush und zoneless
Zwei Begriffe aus Angular-Texten und was sie für Ihren Code heißen. Die Seite „Skipping component subtrees“ sagt: „OnPush is the default change detection strategy in Angular (since v22)“; eine
OnPush-Komponente wird nur geprüft, wenn eine Eingabe sich ändert, ein Ereignis in ihr auftritt, sie ausdrücklich zur Prüfung markiert wird oder ein Signal sich ändert, das ihre Vorlage liest. Zoneless (Seite
„Zoneless“, Standard seit Fassung 21) heißt: Angular verlässt sich auf genau diese Meldungen und nicht auf die Bibliothek zone.js. Beides passt zu Signals. Ihre Regel für den Kurs: Zustand in Signals halten,
nichts in Klassenfelder schreiben, das die Vorlage liest, ohne dass es ein Signal ist. Dann aktualisiert sich die Anzeige.
Barrierefreiheit in dieser Seite
Das Filterfeld hat ein <label for> mit der passenden id: Ein Platzhalter ist keine Beschriftung.
Dass sich die Trefferzahl beim Tippen ändert, erfährt ein Screenreader noch nicht; 0103 löst das mit aria-live.
Übung 1
Die Kontaktliste aus festen Daten mit Filterfeld. Im Projekt aus 0101:
- Legen Sie
src/app/kontakte/contact-summary.tsan: eineinterface ContactSummarymitid: number,name,companyundtags: string[](so heißen die Felder der API; Stand K2 prüfen, obidbei Ihnen eine Zahl ist). - Erzeugen Sie den Dienst
KontaktDaten(ng generate service kontakte/kontakt-daten) mit einer Methodeliste(), die vier erfundene Kontakte zurückgibt. - Schreiben Sie die Komponente
KontaktListe: ein Signalkontakteaus dem Dienst, ein Signalfilter, eincomputedtreffer(Filter nach Name oder Firma, ohne Rücksicht auf Groß- und Kleinschreibung), ein Signalsortierung(Name oder Firma) mit einer Taste zum Wechseln. - Die Vorlage: Überschrift, beschriftetes Feld, Taste „Filter löschen“ nur bei nicht leerem Filter, die Liste mit
@forund@empty, die Sortierung mit@switchim Beschriftungstext der Taste. - Hängen Sie
<kl-kontakt-liste />inapp.htmlundKontaktListeinimportsvonApp. Ersetzen Sie die Prüfung inapp.spec.ts(Hello, kladde-angular, 0101) durchexpect(el.querySelectorAll('li').length).toBe(4). Bauen Sie und starten Sienpx ng serve(die API brauchen Sie hier nicht).
So prüfen Sie: Vier Zeilen erscheinen; „muster“ lässt zwei übrig (Groß- und Kleinschreibung egal), „zzz“ zeigt „Keine Kontakte gefunden.“; die Taste „Filter löschen“ erscheint und verschwindet; die Sortierung wechselt.
npx ng build und npx ng test --no-watch enden mit 0. Bedienen Sie alles nur mit Tab, Leertaste und Eingabetaste.
Lösung 1
src/app/kontakte/contact-summary.ts:
export interface ContactSummary {
id: number;
name: string;
company: string;
tags: string[];
}
src/app/kontakte/kontakt-daten.ts (Namen und Firmen sind erfunden):
import { Service } from '@angular/core';
import { ContactSummary } from './contact-summary';
@Service()
export class KontaktDaten {
liste(): ContactSummary[] {
return [
{ id: 1, name: 'Anna Beispiel', company: 'Beispiel AG', tags: ['kunde'] },
{ id: 2, name: 'Ben Muster', company: 'Muster GmbH', tags: ['kunde', 'wichtig'] },
{ id: 3, name: 'Clara Probst', company: 'Probst & Söhne', tags: [] },
{ id: 4, name: 'Dario Test', company: 'Muster GmbH', tags: ['lieferant'] },
];
}
}
src/app/kontakte/kontakt-liste/kontakt-liste.ts:
import { Component, computed, inject, signal } from '@angular/core';
import { KontaktDaten } from '../kontakt-daten';
@Component({
imports: [],
selector: 'kl-kontakt-liste',
styleUrl: './kontakt-liste.scss',
templateUrl: './kontakt-liste.html',
})
export class KontaktListe {
private readonly daten = inject(KontaktDaten);
protected readonly kontakte = signal(this.daten.liste());
protected readonly filter = signal('');
protected readonly sortierung = signal<'name' | 'company'>('name');
protected readonly treffer = computed(() => {
const suche = this.filter().trim().toLowerCase();
const feld = this.sortierung();
return this.kontakte()
.filter((k) => `${k.name} ${k.company}`.toLowerCase().includes(suche))
.sort((a, b) => a[feld].localeCompare(b[feld], 'de'));
});
protected filterGeaendert(ereignis: Event): void {
this.filter.set((ereignis.target as HTMLInputElement).value);
}
protected sortierungWechseln(): void {
this.sortierung.update((alt) => (alt === 'name' ? 'company' : 'name'));
}
}
src/app/kontakte/kontakt-liste/kontakt-liste.html:
<h1>Kontakte</h1>
<label for="kontakt-filter">Filter nach Name oder Firma</label>
<input id="kontakt-filter" type="search" [value]="filter()" (input)="filterGeaendert($event)" />
@if (filter() !== '') {
<button type="button" (click)="filter.set('')">Filter löschen</button>
}
<button type="button" (click)="sortierungWechseln()">
Sortiert nach:
@switch (sortierung()) {
@case ('name') { Name }
@case ('company') { Firma }
}
</button>
<ul>
@for (kontakt of treffer(); track kontakt.id) {
<li>{{ kontakt.name }} ({{ kontakt.company }})</li>
} @empty {
<li>Keine Kontakte gefunden.</li>
}
</ul>
Warum es so geschrieben ist: [value]="filter()" hält das Feld nach „Filter löschen“ leer. .sort ändert das
Array, das .filter gerade neu angelegt hat, und nicht kontakte(). Typische Fehler:
NG8001: 'kl-kontakt-liste' is not a known element:KontaktListefehlt inimportsvonApp.- Die Liste ändert sich nicht beim Tippen:
filterist kein Signal, oderfilterGeaendertsetzt es nicht. NG5002:@forohnetrack.
Was Sie jetzt können
- Sie schreiben eine Komponente, tragen andere in
importsein und binden Text, Eigenschaften, Attribute und Ereignisse. - Sie halten Zustand in
signal, leiten ihn mitcomputedab, setzeneffectnur für Reaktionen nach außen und kennen den Unterschied zulinkedSignal. - Sie steuern die Vorlage mit
@if,@for(immer mit eindeutigemtrack) und@switchund holen Dienste mitinject(). - Sie wissen, was OnPush und zoneless für Ihren Code bedeuten: Zustand gehört in Signals.
- Sie haben die Kontaktliste aus festen Daten mit Filterfeld gebaut.
Weiterlesen
- Components: Selektor,
imports, Vorlage und Stile. - Binding dynamic text, properties and attributes:
{{ }},[ ],[attr.], Klassen und Stile. - Signals:
signal,computed,effect. - Dependent state with linkedSignal: beschreibbarer, abhängiger Zustand.
- Control flow:
@if,@for,@switchundtrack. - Creating and using services:
@Service,inject. - Skipping component subtrees: wann eine OnPush-Komponente geprüft wird.
Die Kladde: src/app/kontakte hält contact-summary.ts, kontakt-daten.ts und die Komponente kontakt-liste mit Test. Als Nächstes zerlegen Sie sie in eigene Bausteine mit Ein- und Ausgaben (0103).