14 Forms in Angular

14.1 Zwei Paradigmen für Formulare

Formulare sind das primäre Mittel der Datenerfassung in Webanwendungen. Angular bietet zwei fundamental unterschiedliche Ansätze zur Formularimplementierung: Template-driven Forms und Reactive Forms. Diese Dichotomie ist keine bloße Designentscheidung, sondern reflektiert zwei konkurrierende Philosophien über die Rolle von Templates versus TypeScript-Code.

Template-driven Forms orientieren sich an klassischem HTML. Die Logik lebt überwiegend im Template, Validierung wird durch HTML-Attribute ausgedrückt, das Two-Way-Binding mit ngModel synchronisiert View und Model automatisch. Dieser Ansatz ist deklarativ und intuitiv für Entwickler mit HTML-Hintergrund.

Reactive Forms kehren diese Prioritäten um. Die Formular-Struktur wird imperativ in TypeScript definiert, das Template wird zur View-Layer reduziert. Reactive Forms bieten explizite Kontrolle über Zustand, Validierung und Updates. Sie sind verbose aber mächtig, komplex aber testbar.

Die Wahl zwischen beiden hängt von Komplexität, Team-Präferenzen und Testbarkeits-Anforderungen ab. Template-driven Forms eignen sich für einfache Szenarien – Login-Formulare, Kontaktformulare, simple CRUD-Operationen. Reactive Forms glänzen bei Komplexität – dynamische Felder, cross-field Validierung, komplexe State-Management-Requirements.

14.2 Template-driven Forms verstehen

Template-driven Forms bauen auf Angular’s Two-Way-Binding. Das FormsModule erweitert Standard-HTML-Elemente mit Angular-Direktiven. Die ngForm-Direktive wird automatisch auf jedes <form>-Element angewendet und tracked dessen State. Die ngModel-Direktive bindet Input-Elemente an Komponenten-Properties.

Ein einfaches Beispiel demonstriert das Pattern:

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-user-form',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form #userForm="ngForm" (ngSubmit)="onSubmit()">
      <label for="name">Name</label>
      <input 
        id="name" 
        name="name" 
        [(ngModel)]="user.name" 
        required 
        minlength="3"
        #nameField="ngModel">
      
      @if (nameField.invalid && nameField.touched) {
        <div class="error">
          @if (nameField.errors?.['required']) {
            <span>Name erforderlich</span>
          }
          @if (nameField.errors?.['minlength']) {
            <span>Mindestens 3 Zeichen</span>
          }
        </div>
      }
      
      <label for="email">E-Mail</label>
      <input 
        id="email" 
        name="email" 
        type="email"
        [(ngModel)]="user.email" 
        required 
        email
        #emailField="ngModel">
      
      @if (emailField.invalid && emailField.touched) {
        <div class="error">
          @if (emailField.errors?.['required']) {
            <span>E-Mail erforderlich</span>
          }
          @if (emailField.errors?.['email']) {
            <span>Ungültige E-Mail</span>
          }
        </div>
      }
      
      <button type="submit" [disabled]="userForm.invalid">
        Speichern
      </button>
    </form>
  `
})
export class UserFormComponent {
  user = {
    name: '',
    email: ''
  };
  
  onSubmit() {
    console.log('Submitted:', this.user);
  }
}

Die Template-Referenz #userForm="ngForm" gibt Zugriff auf die Form-Direktive. Die ngForm-Direktive aggregiert alle ngModel-Controls innerhalb des Forms und trackt deren Combined State. Das Form ist valid, wenn alle Controls valid sind. Das Form ist touched, wenn ein Control touched wurde.

Die Template-Referenz #nameField="ngModel" gibt Zugriff auf das individuelle Control. Dies ermöglicht granulare Validierungs-Feedback. Die Bedingung nameField.invalid && nameField.touched stellt sicher, dass Fehler erst nach Benutzerinteraktion angezeigt werden, nicht beim initialen Render.

Die name-Attribute sind essentiell. Sie registrieren Controls beim Form. Fehlt das name-Attribut, tracked ngForm das Control nicht. Das Two-Way-Binding [(ngModel)] ist syntaktischer Zucker für [ngModel]="user.name" (ngModelChange)="user.name = $event".

14.2.1 CSS-Klassen für visuelles Feedback

Angular fügt automatisch CSS-Klassen zu Form-Controls basierend auf State:

Klasse Bedeutung Verwendung
ng-valid / ng-invalid Validierungs-State Grüne/rote Border
ng-pristine / ng-dirty Änderungs-State User hat Wert geändert
ng-untouched / ng-touched Interaktions-State User hat Control fokussiert
ng-pending Async Validation läuft Loading-Indicator

Diese Klassen ermöglichen State-basiertes Styling:

input.ng-invalid.ng-touched {
  border-color: #dc3545;
  background-color: #fff5f5;
}

input.ng-valid.ng-touched {
  border-color: #28a745;
}

input.ng-pending {
  background-image: url('loading-spinner.gif');
  background-position: right center;
  background-repeat: no-repeat;
}

Die Kombination .ng-invalid.ng-touched ist Standard – zeige Fehler nur für berührte Felder. Dies verhindert eine Wall-of-Red beim initialen Render.

14.2.2 Verschachtelte Forms mit ngModelGroup

Template-driven Forms können strukturierte Daten repräsentieren durch ngModelGroup:

@Component({
  template: `
    <form #customerForm="ngForm" (ngSubmit)="onSubmit()">
      <input name="name" [(ngModel)]="customer.name" required>
      
      <div ngModelGroup="address">
        <input name="street" [(ngModel)]="customer.address.street" required>
        <input name="city" [(ngModel)]="customer.address.city" required>
        <input name="zip" [(ngModel)]="customer.address.zip" required pattern="[0-9]{5}">
      </div>
      
      <button [disabled]="customerForm.invalid">Submit</button>
    </form>
  `
})
export class CustomerFormComponent {
  customer = {
    name: '',
    address: {
      street: '',
      city: '',
      zip: ''
    }
  };
}

Die ngModelGroup-Direktive gruppiert Controls logisch. Das customerForm.value-Objekt reflektiert die Struktur:

{
  name: "John Doe",
  address: {
    street: "Main St",
    city: "New York",
    zip: "10001"
  }
}

Die Gruppierung ist nicht nur semantisch – sie ermöglicht auch Group-Level-Validierung durch Custom-Direktiven.

14.3 Reactive Forms für explizite Kontrolle

Reactive Forms invertieren die Kontrolle. Die Form-Struktur wird in TypeScript definiert, nicht im Template abgeleitet. Das Template bindet an die programmatisch erstellte Struktur. Diese Separation ermöglicht Unit-Testing der Formular-Logik ohne DOM-Abhängigkeit.

Der Kern-Baustein ist FormControl:

const nameControl = new FormControl('initial value', [
  Validators.required,
  Validators.minLength(3)
]);

Ein FormControl kapselt den Wert, die Validatoren und den State eines einzelnen Input. Die API ist explizit:

nameControl.setValue('new value');
nameControl.value // 'new value'
nameControl.valid // true/false
nameControl.errors // { required: true } oder null
nameControl.markAsTouched();
nameControl.touched // true

FormGroup sammelt mehrere Controls:

const userForm = new FormGroup({
  name: new FormControl('', Validators.required),
  email: new FormControl('', [Validators.required, Validators.email])
});

Die Struktur ist hierarchisch. Ein FormGroup kann andere FormGroups enthalten. Die Validierung propagiert – ein FormGroup ist nur valid, wenn alle Children valid sind.

Der FormBuilder-Service reduziert Boilerplate:

import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';

@Component({
  selector: 'app-user-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="userForm" (ngSubmit)="onSubmit()">
      <input formControlName="name">
      @if (name.invalid && name.touched) {
        <div class="error">
          @if (name.errors?.['required']) {
            <span>Name erforderlich</span>
          }
          @if (name.errors?.['minlength']) {
            <span>Mindestens {{ name.errors?.['minlength'].requiredLength }} Zeichen</span>
          }
        </div>
      }
      
      <input formControlName="email" type="email">
      @if (email.invalid && email.touched) {
        <div class="error">
          @if (email.errors?.['required']) {
            <span>E-Mail erforderlich</span>
          }
          @if (email.errors?.['email']) {
            <span>Ungültige E-Mail</span>
          }
        </div>
      }
      
      <button [disabled]="userForm.invalid">Submit</button>
    </form>
  `
})
export class UserFormComponent {
  private fb = inject(FormBuilder);
  
  userForm = this.fb.group({
    name: ['', [Validators.required, Validators.minLength(3)]],
    email: ['', [Validators.required, Validators.email]]
  });
  
  get name() { return this.userForm.get('name')!; }
  get email() { return this.userForm.get('email')!; }
  
  onSubmit() {
    if (this.userForm.valid) {
      console.log('Submitted:', this.userForm.value);
    }
  }
}

Die Template-Syntax ist sparsamer. [formGroup]="userForm" bindet das Form-Objekt. formControlName="name" bindet an das spezifische Control. Keine name-Attribute nötig – die Binding erfolgt über die programmatische Struktur.

Die Getter get name() vereinfachen Template-Zugriff. Ohne sie wäre jede Referenz userForm.get('name')! – verbose und repetitiv. Die Non-Null-Assertion ! ist sicher, da wir wissen dass das Control existiert.

14.3.1 Reaktive Updates und Observables

Reactive Forms exposen Observables für State-Changes:

export class ReactiveFormComponent {
  userForm = this.fb.group({
    name: [''],
    email: ['']
  });
  
  constructor() {
    this.userForm.valueChanges.subscribe(value => {
      console.log('Form value changed:', value);
    });
    
    this.userForm.statusChanges.subscribe(status => {
      console.log('Form status changed:', status);
    });
    
    this.userForm.get('name')!.valueChanges.subscribe(name => {
      console.log('Name changed:', name);
    });
  }
}

Die valueChanges-Observable emittiert bei jeder Value-Änderung. Die statusChanges-Observable emittiert bei Validierungs-Changes. Diese Observables ermöglichen reaktive Side-Effects – etwa automatisches Speichern, Live-Search, Cross-Field-Validierung.

Ein praktisches Beispiel ist Debouncing für API-Calls:

this.searchForm.get('query')!.valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(query => this.searchService.search(query))
).subscribe(results => {
  this.searchResults = results;
});

Das Pattern ist common: warte 300ms nach der letzten Änderung, ignoriere Duplikate, cancele vorherige Requests, update Results. Ohne Observables wäre dies erheblich komplexer.

14.3.2 Werte programmatisch setzen

Reactive Forms bieten explizite APIs für Value-Updates:

// Alle Werte setzen (alle Controls müssen provided werden)
this.userForm.setValue({
  name: 'John Doe',
  email: 'john@example.com'
});

// Partielle Werte setzen
this.userForm.patchValue({
  name: 'John Doe'
});

// Einzelnes Control
this.userForm.get('name')!.setValue('John Doe');

// Reset zu Initial-Values
this.userForm.reset();

// Reset mit neuen Werten
this.userForm.reset({
  name: '',
  email: ''
});

setValue() ist strict – alle Controls müssen Werte erhalten, sonst Error. patchValue() ist lenient – nur provided Properties werden gesetzt. Der Unterschied ist wichtig beim Laden von Server-Daten, die nicht die komplette Form-Struktur matchen.

14.3.3 Verschachtelte Forms mit FormGroup

Verschachtelte Strukturen werden durch nested FormGroups modelliert:

customerForm = this.fb.group({
  name: ['', Validators.required],
  address: this.fb.group({
    street: ['', Validators.required],
    city: ['', Validators.required],
    zip: ['', [Validators.required, Validators.pattern(/^\d{5}$/)]]
  })
});

Im Template mit formGroupName:

<form [formGroup]="customerForm">
  <input formControlName="name">
  
  <div formGroupName="address">
    <input formControlName="street">
    <input formControlName="city">
    <input formControlName="zip">
  </div>
</form>

Der Zugriff auf nested Controls nutzt Dot-Notation:

get street() { return this.customerForm.get('address.street')!; }

Die Struktur des value-Objekts reflektiert die Hierarchie:

{
  name: "John Doe",
  address: {
    street: "Main St",
    city: "New York",
    zip: "10001"
  }
}

14.4 Dynamische Forms mit FormArray

FormArray managed eine dynamische Liste von Controls. Dies ist essentiell für variabel lange Listen – Order Items, Phone Numbers, Email Addresses.

Ein Order-Form mit dynamischen Items:

orderForm = this.fb.group({
  customerName: ['', Validators.required],
  items: this.fb.array([
    this.createItem()
  ])
});

createItem(): FormGroup {
  return this.fb.group({
    product: ['', Validators.required],
    quantity: [1, [Validators.required, Validators.min(1)]],
    price: [0, [Validators.required, Validators.min(0.01)]]
  });
}

get items() {
  return this.orderForm.get('items') as FormArray;
}

addItem() {
  this.items.push(this.createItem());
}

removeItem(index: number) {
  this.items.removeAt(index);
}

get totalPrice() {
  return this.items.controls.reduce((sum, control) => {
    const quantity = control.get('quantity')!.value;
    const price = control.get('price')!.value;
    return sum + (quantity * price);
  }, 0);
}

Im Template mit formArrayName:

<form [formGroup]="orderForm">
  <input formControlName="customerName">
  
  <div formArrayName="items">
    @for (item of items.controls; track $index; let i = $index) {
      <div [formGroupName]="i">
        <input formControlName="product">
        <input formControlName="quantity" type="number">
        <input formControlName="price" type="number" step="0.01">
        
        <button type="button" (click)="removeItem(i)">Remove</button>
      </div>
    }
  </div>
  
  <button type="button" (click)="addItem()">Add Item</button>
  
  <div>Total: {{ totalPrice | currency }}</div>
</form>

Die @for-Direktive iteriert über items.controls. Jedes Control ist ein FormGroup mit product/quantity/price. Das [formGroupName]="i" bindet an den Index im Array.

FormArray ist nicht nur für homogene Listen. Es kann auch gemischte Control-Typen enthalten:

const mixedArray = this.fb.array([
  this.fb.control('simple value'),
  this.fb.group({ name: [''], value: [0] }),
  this.fb.array([this.fb.control('')])
]);

Dies ist selten nötig aber möglich für highly dynamic Forms.

14.5 Validierung verstehen

Angular’s Validierungs-System ist funktional. Ein Validator ist eine Funktion, die ein AbstractControl nimmt und entweder null (valid) oder ein ValidationErrors-Objekt (invalid) zurückgibt.

Die eingebauten Validators sind pure Functions:

Validators.required(control: AbstractControl): ValidationErrors | null
Validators.minLength(length: number): ValidatorFn
Validators.pattern(pattern: string | RegExp): ValidatorFn

Validators.required ist eine Funktion. Validators.minLength ist eine Factory, die eine ValidatorFn zurückgibt. Dies ermöglicht Parametrisierung.

Custom Validators folgen demselben Pattern:

import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

export function phoneValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    if (!control.value) return null;
    
    const phoneRegex = /^[\d\s\-\+\(\)]+$/;
    const valid = phoneRegex.test(control.value);
    
    return valid ? null : { invalidPhone: true };
  };
}

Die Verwendung:

phoneForm = this.fb.group({
  phone: ['', [Validators.required, phoneValidator()]]
});

Die Error-Keys (invalidPhone) sind custom und müssen im Template gehandled werden:

@if (phone.errors?.['invalidPhone']) {
  <span>Ungültige Telefonnummer</span>
}

14.5.1 Cross-Field Validation

Manche Validierung hängt von mehreren Fields ab. Password-Confirmation ist das klassische Beispiel:

export function passwordMatchValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const password = control.get('password');
    const confirm = control.get('confirmPassword');
    
    if (!password || !confirm) return null;
    
    if (password.value !== confirm.value) {
      confirm.setErrors({ passwordMismatch: true });
      return { passwordMismatch: true };
    }
    
    // Clear error if passwords match
    if (confirm.hasError('passwordMismatch')) {
      confirm.setErrors(null);
    }
    
    return null;
  };
}

Der Validator wird auf dem FormGroup registriert, nicht auf individuellen Controls:

registrationForm = this.fb.group({
  password: ['', [Validators.required, Validators.minLength(8)]],
  confirmPassword: ['', Validators.required]
}, { validators: passwordMatchValidator() });

Das Error-Objekt ist auf dem Group-Level:

get passwordMismatch() {
  return this.registrationForm.errors?.['passwordMismatch'];
}

Alternativ kann der Error auf einem Child-Control gesetzt werden, wie im Validator-Beispiel. Die Wahl hängt von UX-Anforderungen ab.

14.5.2 Asynchrone Validation

Manche Validierung erfordert Server-Calls. Username-Uniqueness ist ein typisches Szenario. Asynchrone Validators returnen ein Observable<ValidationErrors | null>:

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { map, catchError, debounceTime, switchMap, first } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class UsernameValidator {
  private http = inject(HttpClient);
  
  uniqueUsername(): AsyncValidatorFn {
    return (control: AbstractControl): Observable<ValidationErrors | null> => {
      if (!control.value) {
        return of(null);
      }
      
      return of(control.value).pipe(
        debounceTime(300),
        switchMap(username =>
          this.http.get<{ available: boolean }>(`/api/check-username?username=${username}`).pipe(
            map(response => response.available ? null : { usernameTaken: true }),
            catchError(() => of(null))
          )
        ),
        first()
      );
    };
  }
}

Die Usage:

constructor(private usernameValidator: UsernameValidator) {}

form = this.fb.group({
  username: [
    '',
    [Validators.required, Validators.minLength(3)],
    [this.usernameValidator.uniqueUsername()]
  ]
});

Asynchrone Validators sind das dritte Array-Argument. Sie laufen nur, wenn alle synchronen Validators passen. Dies verhindert unnötige API-Calls.

Während der Async-Validation ist der Control-Status PENDING. Das Template kann dies visualisieren:

<input formControlName="username">

@if (username.pending) {
  <span class="spinner">Checking...</span>
}

@if (username.errors?.['usernameTaken'] && username.touched) {
  <span class="error">Username already taken</span>
}

Das Debouncing im Validator verhindert Requests bei jedem Keystroke. switchMap cancelt vorherige Requests. first() completed die Observable nach dem ersten Emit. catchError verhindert, dass Network-Errors die Form blockieren.

14.6 Form Submission und HTTP-Integration

Formulare enden typischerweise mit einem HTTP-POST an den Server:

import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, Validators } from '@angular/forms';
import { finalize } from 'rxjs/operators';

@Component({
  template: `
    <form [formGroup]="contactForm" (ngSubmit)="onSubmit()">
      <input formControlName="name">
      <input formControlName="email">
      <textarea formControlName="message"></textarea>
      
      <button [disabled]="contactForm.invalid || submitting">
        @if (submitting) {
          Sending...
        } @else {
          Send
        }
      </button>
      
      @if (submitSuccess) {
        <div class="success">Message sent successfully!</div>
      }
      
      @if (submitError) {
        <div class="error">{{ submitError }}</div>
      }
    </form>
  `
})
export class ContactFormComponent {
  private fb = inject(FormBuilder);
  private http = inject(HttpClient);
  
  contactForm = this.fb.group({
    name: ['', Validators.required],
    email: ['', [Validators.required, Validators.email]],
    message: ['', Validators.required]
  });
  
  submitting = false;
  submitSuccess = false;
  submitError = '';
  
  onSubmit() {
    if (this.contactForm.invalid) return;
    
    this.submitting = true;
    this.submitSuccess = false;
    this.submitError = '';
    
    this.http.post('/api/contact', this.contactForm.value)
      .pipe(finalize(() => this.submitting = false))
      .subscribe({
        next: () => {
          this.submitSuccess = true;
          this.contactForm.reset();
        },
        error: (error) => {
          this.submitError = error.message || 'Submission failed';
        }
      });
  }
}

Die submitting-Flag disabled den Button während des Requests. finalize() setzt die Flag zurück, egal ob Success oder Error. Die Success-Callback reset das Form und zeigt Bestätigung. Die Error-Callback zeigt die Fehlermeldung.

Für optimistische Updates kann das Form sofort gereset werden:

onSubmit() {
  const data = this.contactForm.value;
  this.contactForm.reset();
  
  this.http.post('/api/contact', data).subscribe({
    error: () => {
      // Restore form state on error
      this.contactForm.patchValue(data);
    }
  });
}

Dies gibt sofortiges Feedback. Bei Failure wird der State restored.

14.7 TypeScript-Integration und Type Safety

Angular 14+ verbesserte die Type-Safety für Reactive Forms erheblich. Controls sind jetzt generisch typisiert:

interface UserFormModel {
  name: string;
  email: string;
  age: number;
}

const form = this.fb.group<UserFormModel>({
  name: ['', Validators.required],
  email: ['', [Validators.required, Validators.email]],
  age: [0, [Validators.required, Validators.min(18)]]
});

// Type-safe access
const name: string = form.controls.name.value;
const age: number = form.controls.age.value;

Der Compiler validiert Feld-Namen. form.controls.invalidField ist ein Compile-Error. Die Werte sind korrekt typisiert – kein any mehr.

Für nicht-nullable Controls gibt es nonNullable:

const form = this.fb.nonNullable.group({
  name: ['', Validators.required],
  email: ['', [Validators.required, Validators.email]]
});

// Type: string, never null
const name = form.controls.name.value;

Dies ist sicherer als nullable Defaults. form.reset() setzt zu den Initial-Values zurück, nicht null.

Verschachtelte Strukturen sind ebenfalls typisiert:

interface AddressModel {
  street: string;
  city: string;
  zip: string;
}

interface CustomerModel {
  name: string;
  address: AddressModel;
}

const form = this.fb.group<CustomerModel>({
  name: [''],
  address: this.fb.group<AddressModel>({
    street: [''],
    city: [''],
    zip: ['']
  })
});

// Type-safe nested access
const street: string = form.controls.address.controls.street.value;

Die verbesserte Type-Safety reduziert Runtime-Errors erheblich. Refactorings werden sicherer – Rename eines Fields bricht die Compilation, nicht erst zur Runtime.

14.8 Patterns für wiederverwendbare Forms

Komplexe Anwendungen profitieren von wiederverwendbaren Form-Komponenten. Eine Address-Form-Komponente kann in verschiedenen Contexts genutzt werden:

import { Component, Input, OnInit, inject } from '@angular/core';
import { FormGroup, FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';

@Component({
  selector: 'app-address-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <div [formGroup]="addressGroup">
      <input formControlName="street" placeholder="Street">
      <input formControlName="city" placeholder="City">
      <input formControlName="zip" placeholder="ZIP">
    </div>
  `
})
export class AddressFormComponent implements OnInit {
  @Input() parentForm!: FormGroup;
  @Input() controlName = 'address';
  
  private fb = inject(FormBuilder);
  addressGroup!: FormGroup;
  
  ngOnInit() {
    this.addressGroup = this.fb.group({
      street: ['', Validators.required],
      city: ['', Validators.required],
      zip: ['', [Validators.required, Validators.pattern(/^\d{5}$/)]]
    });
    
    this.parentForm.addControl(this.controlName, this.addressGroup);
  }
}

Die Parent-Komponente integriert es:

@Component({
  template: `
    <form [formGroup]="customerForm" (ngSubmit)="onSubmit()">
      <input formControlName="name">
      
      <app-address-form 
        [parentForm]="customerForm"
        controlName="billingAddress">
      </app-address-form>
      
      <app-address-form 
        [parentForm]="customerForm"
        controlName="shippingAddress">
      </app-address-form>
      
      <button>Submit</button>
    </form>
  `
})
export class CustomerFormComponent {
  customerForm = this.fb.group({
    name: ['', Validators.required]
  });
}

Die Address-Components fügen sich selbst zum Parent-Form hinzu. Das Result ist ein integriertes Form mit allen Validations.

Ein fortgeschritteneres Pattern nutzt ControlValueAccessor für vollständige Form-Control-Integration. Dies erlaubt Custom-Components, die sich wie native Form-Controls verhalten – mit ngModel und formControlName kompatibel.

Angular Forms bieten zwei komplementäre Ansätze für Datenerfassung. Template-driven Forms sind schnell und intuitiv für einfache Szenarien. Reactive Forms bieten explizite Kontrolle, Testbarkeit und Skalierbarkeit für Komplexität. Die Validierungs-API ist extensible durch Custom Validators, sowohl synchron als auch asynchron. FormArray ermöglicht dynamische Form-Strukturen. Die Type-Safety-Verbesserungen in neueren Versionen reduzieren Fehler und verbessern Developer Experience. Die Wahl zwischen Template-driven und Reactive hängt von Requirements ab, aber beide integrieren nahtlos in Angular’s Reactive Architecture.