Formulare sind der Ort, an dem Eingaben, Regeln und Fehler der API zusammentreffen. Die Regeln gehören dem Server (K2 2.3.8): Das Formular prüft nur, was es der Person sofort sagen kann, und zeigt, was die API ablehnt. Diese Lektion baut zwei Formulare der Kladde: Kontakt anlegen und ändern, und die öffentliche Buchung mit freien Zeiten. Das Kontaktformular entsteht zweimal, mit Reactive Forms und mit Signal Forms, damit Sie beide Wege vergleichen können.

Stand der Prüfung. Der Code stammt aus dem Wegwerfprojekt außerhalb des Repositorys (Node 24.22.3, Angular 22.2.6, TypeScript 6.0.3, Vitest 5.1.0), auf dem Stand von 0203 (erzeugter Client). Gelaufen sind npm run build und npm test -- --no-watch (Exit-Code 0; Endstand 32 Tests in 7 Dateien, davon 8 für das Kontaktformular, 3 für die Buchung). Die Tests laufen in jsdom und antworten mit HttpTestingController; kein Browser, keine laufende API, kein Screenreader. Signal Forms tragen auf der Seite der Dokumentation das Etikett „New“ und keinen Stabilitätsvermerk; ob sie stabil sind, ist offen. Die Namen (form, FormField, FormRoot, required, email, maxLength, submission, onInvalid, errorSummary, focusBoundControl) wurden gegen die Seiten „Signal Forms“, „Models“ und „Form submission“ gelesen und im Projekt kompiliert. Der asynchrone Validator wurde nur als Funktion getestet. Wo K2 Feldnamen anders nennt: Stand K2 prüfen (2.3.8).

Typisierte Reactive Forms

Ein FormControl leitet seinen Typ vom Anfangswert ab; ein FormControl('') ist FormControl<string | null>, weil reset() es auf null setzt. Mit nonNullable: true setzt reset() den Anfangswert zurück. Der NonNullableFormBuilder wendet das auf alle Steuerelemente an; inject(NonNullableFormBuilder) liefert ihn. form.value ist Partial<...> (deaktivierte Steuerelemente fehlen), getRawValue() liefert alle Felder ohne undefined. Das passt zum erzeugten Typ ContactRequest (0203).

src/app/kontakte/kontakt-formular.ts (Auszug):

TypeScript
protected readonly form = this.fb.group({
  name: ['', [Validators.required, Validators.maxLength(100)]],
  email: ['', [Validators.required, Validators.email]],
  company: [''],
});

protected speichern(): void {
  this.meldung.set('');
  if (this.form.invalid) {
    this.form.markAllAsTouched();
    this.zumErstenFehler();
    return;
  }
  this.sendet.set(true);
  this.speichernAufruf().subscribe({ /* next, error: siehe unten */ });
}

maxLength(100) kommt aus ContactRequest im Dokument (maxLength am Feld name). Das Formular wiederholt diese Regel, weil die Person sie sofort sehen soll; maßgeblich bleibt die API. Ändern sich die Grenzen im Vertrag, ändert sich das Formular nicht von selbst: Das ist die Grenze erzeugter Typen (0203), sie tragen die Form, nicht die Regeln.

Das Formular dient zum Anlegen und, mit einer Eingabe kontakt = input<ContactResponse>(), zum Ändern: Ein effect ruft form.patchValue(kontakt), und speichernAufruf() wählt aendern(id, werte) (PUT) oder anlegen(werte) (POST). Die Seite kontakte/neu steht in app.routes.ts vor kontakte/:id (0201), sonst wäre „neu“ eine Id.