15 Forms in Angular

Angular bietet leistungsstarke und flexible Mechanismen zur Formularverarbeitung, die es Entwicklern ermöglichen, von einfachen Kontaktformularen bis hin zu komplexen mehrstufigen Workflows alles zu implementieren. Dieser Leitfaden behandelt die zwei Hauptansätze für Formulare in Angular: Template-driven Forms und Reactive Forms.

15.1 Grundlegende Konzepte

Bevor wir in die spezifischen Implementierungen eintauchen, ist es wichtig, einige grundlegende Konzepte zu verstehen:

15.2 Template-driven Forms

Template-driven Forms setzen auf deklarative Syntax im HTML-Template und sind ideal für einfache bis mittlere Formulare. Sie sind näher an traditionellem HTML und daher oft einfacher für Einsteiger.

15.2.1 Einrichtung

Um Template-driven Forms zu verwenden, müssen Sie das FormsModule importieren:

// app.module.ts oder feature.module.ts
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

@NgModule({
  imports: [
    FormsModule
  ],
  // ...
})
export class AppModule { }

15.2.2 Einfaches Beispiel

// user-form.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-user-form',
  templateUrl: './user-form.component.html',
})
export class UserFormComponent {
  user = {
    name: '',
    email: '',
    age: null
  };

  onSubmit() {
    console.log('Formular eingereicht:', this.user);
    // Hier würde die Logik zum Senden der Daten an den Server folgen
  }
}
<!-- user-form.component.html -->
<form #userForm="ngForm" (ngSubmit)="onSubmit()" class="form">
  <h2>Benutzerprofil</h2>
  
  <div class="form-group">
    <label for="name">Name</label>
    <input 
      type="text" 
      id="name" 
      name="name" 
      [(ngModel)]="user.name" 
      required 
      minlength="3"
      #nameField="ngModel"
      class="form-control">
    
    <div *ngIf="nameField.invalid && (nameField.dirty || nameField.touched)" class="error-messages">
      <div *ngIf="nameField.errors?.['required']">
        Name ist erforderlich.
      </div>
      <div *ngIf="nameField.errors?.['minlength']">
        Name muss mindestens 3 Zeichen lang sein.
      </div>
    </div>
  </div>
  
  <div class="form-group">
    <label for="email">E-Mail</label>
    <input 
      type="email" 
      id="email" 
      name="email" 
      [(ngModel)]="user.email" 
      required 
      email
      #emailField="ngModel"
      class="form-control">
    
    <div *ngIf="emailField.invalid && (emailField.dirty || emailField.touched)" class="error-messages">
      <div *ngIf="emailField.errors?.['required']">
        E-Mail ist erforderlich.
      </div>
      <div *ngIf="emailField.errors?.['email']">
        Bitte geben Sie eine gültige E-Mail-Adresse ein.
      </div>
    </div>
  </div>
  
  <div class="form-group">
    <label for="age">Alter</label>
    <input 
      type="number" 
      id="age" 
      name="age" 
      [(ngModel)]="user.age" 
      required
      min="18"
      max="120"
      #ageField="ngModel"
      class="form-control">
    
    <div *ngIf="ageField.invalid && (ageField.dirty || ageField.touched)" class="error-messages">
      <div *ngIf="ageField.errors?.['required']">
        Alter ist erforderlich.
      </div>
      <div *ngIf="ageField.errors?.['min']">
        Alter muss mindestens 18 sein.
      </div>
      <div *ngIf="ageField.errors?.['max']">
        Alter kann maximal 120 sein.
      </div>
    </div>
  </div>
  
  <button type="submit" [disabled]="userForm.invalid" class="submit-button">
    Speichern
  </button>
</form>

15.2.3 Wichtige Merkmale von Template-driven Forms

15.2.3.1 NgForm-Direktive

Die ngForm-Direktive wird automatisch auf jedes <form>-Element in Ihrer Anwendung angewendet, wenn das FormsModule importiert ist. Sie können darauf über eine Template-Referenzvariable zugreifen (wie #userForm="ngForm").

15.2.3.2 NgModel-Direktive

Die ngModel-Direktive verbindet ein Formularfeld mit einer Eigenschaft im Komponenten-Code:

15.2.3.3 Status- und CSS-Klassen

Angular fügt automatisch CSS-Klassen zu Formularfeldern hinzu, basierend auf ihrem Status:

Diese Klassen können für benutzerdefiniertes Styling verwendet werden:

.ng-invalid.ng-touched {
  border-color: red;
}

.ng-valid.ng-touched {
  border-color: green;
}

15.2.4 Verschachteltes Formular

Template-driven Forms können auch verschachtelte Objekte darstellen:

// nested-form.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-nested-form',
  templateUrl: './nested-form.component.html',
})
export class NestedFormComponent {
  customer = {
    name: '',
    address: {
      street: '',
      city: '',
      zipCode: ''
    }
  };

  onSubmit() {
    console.log('Formular eingereicht:', this.customer);
  }
}
<!-- nested-form.component.html -->
<form #customerForm="ngForm" (ngSubmit)="onSubmit()">
  <div ngModelGroup="customer">
    <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="zipCode" [(ngModel)]="customer.address.zipCode" required>
    </div>
  </div>
  
  <button type="submit" [disabled]="customerForm.invalid">Speichern</button>
</form>

15.3 Reactive Forms

Reactive Forms bieten einen programmatischen Ansatz zur Formularerstellung und -verwaltung. Sie sind besonders nützlich für komplexe Formulare, dynamische Felder und fortgeschrittene Validierungsszenarien.

15.3.1 Einrichtung

Um Reactive Forms zu verwenden, müssen Sie das ReactiveFormsModule importieren:

// app.module.ts oder feature.module.ts
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  imports: [
    ReactiveFormsModule
  ],
  // ...
})
export class AppModule { }

15.3.2 Einfaches Beispiel

// reactive-form.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-reactive-form',
  templateUrl: './reactive-form.component.html',
})
export class ReactiveFormComponent implements OnInit {
  userForm!: FormGroup;
  
  constructor(private fb: FormBuilder) {}
  
  ngOnInit() {
    this.userForm = this.fb.group({
      name: ['', [Validators.required, Validators.minLength(3)]],
      email: ['', [Validators.required, Validators.email]],
      age: [null, [
        Validators.required, 
        Validators.min(18), 
        Validators.max(120)
      ]]
    });
  }
  
  onSubmit() {
    if (this.userForm.valid) {
      console.log('Formular eingereicht:', this.userForm.value);
      // Hier würde die Logik zum Senden der Daten an den Server folgen
    } else {
      // Markiere alle Felder als berührt, um Validierungsfehler anzuzeigen
      this.markFormGroupTouched(this.userForm);
    }
  }
  
  // Hilfsfunktion, um alle Felder als berührt zu markieren
  markFormGroupTouched(formGroup: FormGroup) {
    Object.values(formGroup.controls).forEach(control => {
      control.markAsTouched();
      
      if ((control as FormGroup).controls) {
        this.markFormGroupTouched(control as FormGroup);
      }
    });
  }
  
  // Einfacher Zugriff auf Formularsteuerelemente
  get nameControl() { return this.userForm.get('name'); }
  get emailControl() { return this.userForm.get('email'); }
  get ageControl() { return this.userForm.get('age'); }
}
<!-- reactive-form.component.html -->
<form [formGroup]="userForm" (ngSubmit)="onSubmit()" class="form">
  <h2>Benutzerprofil (Reactive)</h2>
  
  <div class="form-group">
    <label for="name">Name</label>
    <input 
      type="text" 
      id="name" 
      formControlName="name"
      class="form-control">
    
    <div *ngIf="nameControl?.invalid && (nameControl?.dirty || nameControl?.touched)" class="error-messages">
      <div *ngIf="nameControl?.errors?.['required']">
        Name ist erforderlich.
      </div>
      <div *ngIf="nameControl?.errors?.['minlength']">
        Name muss mindestens 3 Zeichen lang sein.
      </div>
    </div>
  </div>
  
  <div class="form-group">
    <label for="email">E-Mail</label>
    <input 
      type="email" 
      id="email" 
      formControlName="email"
      class="form-control">
    
    <div *ngIf="emailControl?.invalid && (emailControl?.dirty || emailControl?.touched)" class="error-messages">
      <div *ngIf="emailControl?.errors?.['required']">
        E-Mail ist erforderlich.
      </div>
      <div *ngIf="emailControl?.errors?.['email']">
        Bitte geben Sie eine gültige E-Mail-Adresse ein.
      </div>
    </div>
  </div>
  
  <div class="form-group">
    <label for="age">Alter</label>
    <input 
      type="number" 
      id="age" 
      formControlName="age"
      class="form-control">
    
    <div *ngIf="ageControl?.invalid && (ageControl?.dirty || ageControl?.touched)" class="error-messages">
      <div *ngIf="ageControl?.errors?.['required']">
        Alter ist erforderlich.
      </div>
      <div *ngIf="ageControl?.errors?.['min']">
        Alter muss mindestens 18 sein.
      </div>
      <div *ngIf="ageControl?.errors?.['max']">
        Alter kann maximal 120 sein.
      </div>
    </div>
  </div>
  
  <button type="submit" [disabled]="userForm.invalid" class="submit-button">
    Speichern
  </button>
</form>

15.3.3 Wichtige Merkmale von Reactive Forms

15.3.3.1 FormControl, FormGroup und FormArray

15.3.3.2 FormBuilder-Service

Der FormBuilder-Service bietet syntaktischen Zucker für die Erstellung von Formularen:

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

// Ohne FormBuilder (äquivalent)
this.form = new FormGroup({
  name: new FormControl('', Validators.required),
  email: new FormControl('', [Validators.required, Validators.email])
});

15.3.3.3 Werte und Status

Reactive Forms bieten Observables für Änderungen an Werten und Status:

// Auf Wertänderungen reagieren
this.form.valueChanges.subscribe(value => {
  console.log('Formularwerte geändert:', value);
});

// Auf Statusänderungen reagieren
this.form.statusChanges.subscribe(status => {
  console.log('Formularstatus geändert:', status);
});

15.3.3.4 Werte aktualisieren

Es gibt mehrere Methoden, um Werte in einem Reactive Form zu aktualisieren:

// Alle Werte ersetzen (benötigt Werte für alle Felder)
this.form.setValue({
  name: 'Max Mustermann',
  email: 'max@example.com',
  age: 30
});

// Teilweise Werte aktualisieren
this.form.patchValue({
  name: 'Max Mustermann'
});

// Einzelnes Feld aktualisieren
this.form.get('name')?.setValue('Max Mustermann');

// Formular zurücksetzen (auf Initialwerte)
this.form.reset();

15.3.4 Verschachtelte Formulare

Reactive Forms eignen sich besonders gut für komplexe, verschachtelte Strukturen:

// nested-reactive-form.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-nested-reactive-form',
  templateUrl: './nested-reactive-form.component.html',
})
export class NestedReactiveFormComponent implements OnInit {
  customerForm!: FormGroup;
  
  constructor(private fb: FormBuilder) {}
  
  ngOnInit() {
    this.customerForm = this.fb.group({
      name: ['', [Validators.required, Validators.minLength(3)]],
      address: this.fb.group({
        street: ['', Validators.required],
        city: ['', Validators.required],
        zipCode: ['', [
          Validators.required,
          Validators.pattern(/^\d{5}$/)
        ]]
      })
    });
  }
  
  onSubmit() {
    if (this.customerForm.valid) {
      console.log('Formular eingereicht:', this.customerForm.value);
    }
  }
  
  // Getter für verschachtelte Formularkontrollen
  get name() { return this.customerForm.get('name'); }
  get street() { return this.customerForm.get('address.street'); }
  get city() { return this.customerForm.get('address.city'); }
  get zipCode() { return this.customerForm.get('address.zipCode'); }
}
<!-- nested-reactive-form.component.html -->
<form [formGroup]="customerForm" (ngSubmit)="onSubmit()">
  <div class="form-group">
    <label for="name">Name</label>
    <input id="name" formControlName="name">
    <div *ngIf="name?.invalid && name?.touched" class="error">
      Name ist erforderlich und muss mindestens 3 Zeichen lang sein.
    </div>
  </div>
  
  <div formGroupName="address">
    <h3>Adresse</h3>
    
    <div class="form-group">
      <label for="street">Straße</label>
      <input id="street" formControlName="street">
      <div *ngIf="street?.invalid && street?.touched" class="error">
        Straße ist erforderlich.
      </div>
    </div>
    
    <div class="form-group">
      <label for="city">Stadt</label>
      <input id="city" formControlName="city">
      <div *ngIf="city?.invalid && city?.touched" class="error">
        Stadt ist erforderlich.
      </div>
    </div>
    
    <div class="form-group">
      <label for="zipCode">PLZ</label>
      <input id="zipCode" formControlName="zipCode">
      <div *ngIf="zipCode?.invalid && zipCode?.touched" class="error">
        <span *ngIf="zipCode?.errors?.['required']">PLZ ist erforderlich.</span>
        <span *ngIf="zipCode?.errors?.['pattern']">PLZ muss aus 5 Ziffern bestehen.</span>
      </div>
    </div>
  </div>
  
  <button type="submit" [disabled]="customerForm.invalid">Speichern</button>
</form>

15.3.5 Dynamische Formulare mit FormArray

Ein besonders mächtiges Feature von Reactive Forms ist die Möglichkeit, dynamische Formulare mit FormArray zu erstellen:

// dynamic-form.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormArray, Validators } from '@angular/forms';

@Component({
  selector: 'app-dynamic-form',
  templateUrl: './dynamic-form.component.html',
})
export class DynamicFormComponent implements OnInit {
  orderForm!: FormGroup;
  
  constructor(private fb: FormBuilder) {}
  
  ngOnInit() {
    this.orderForm = this.fb.group({
      customerName: ['', Validators.required],
      items: this.fb.array([
        this.createItem()
      ])
    });
  }
  
  // Getter für die FormArray
  get items() {
    return this.orderForm.get('items') as FormArray;
  }
  
  // Methode zum Erstellen eines neuen Artikel-FormGroups
  createItem(): FormGroup {
    return this.fb.group({
      productName: ['', Validators.required],
      quantity: [1, [Validators.required, Validators.min(1)]],
      price: [0, [Validators.required, Validators.min(0.01)]]
    });
  }
  
  // Methode zum Hinzufügen eines neuen Artikels
  addItem() {
    this.items.push(this.createItem());
  }
  
  // Methode zum Entfernen eines Artikels
  removeItem(index: number) {
    this.items.removeAt(index);
  }
  
  // Berechnung der Gesamtsumme
  get totalPrice() {
    return this.items.controls
      .map(control => control.get('quantity')?.value * control.get('price')?.value)
      .reduce((acc, val) => acc + val, 0);
  }
  
  onSubmit() {
    if (this.orderForm.valid) {
      console.log('Bestellung eingereicht:', this.orderForm.value);
      console.log('Gesamtpreis:', this.totalPrice);
    }
  }
}
<!-- dynamic-form.component.html -->
<form [formGroup]="orderForm" (ngSubmit)="onSubmit()">
  <h2>Bestellformular</h2>
  
  <div class="form-group">
    <label for="customerName">Kundenname</label>
    <input id="customerName" formControlName="customerName">
    <div *ngIf="orderForm.get('customerName')?.invalid && orderForm.get('customerName')?.touched" class="error">
      Kundenname ist erforderlich.
    </div>
  </div>
  
  <h3>Artikel</h3>
  
  <div formArrayName="items">
    <div *ngFor="let item of items.controls; let i = index" [formGroupName]="i" class="item-row">
      <div class="form-group">
        <label [for]="'productName' + i">Produktname</label>
        <input [id]="'productName' + i" formControlName="productName">
      </div>
      
      <div class="form-group">
        <label [for]="'quantity' + i">Menge</label>
        <input [id]="'quantity' + i" type="number" formControlName="quantity">
      </div>
      
      <div class="form-group">
        <label [for]="'price' + i">Preis</label>
        <input [id]="'price' + i" type="number" step="0.01" formControlName="price">
      </div>
      
      <button type="button" class="remove-button" (click)="removeItem(i)" *ngIf="items.length > 1">
        Entfernen
      </button>
    </div>
    
    <button type="button" (click)="addItem()" class="add-button">
      Artikel hinzufügen
    </button>
  </div>
  
  <div class="total">
    <strong>Gesamt: {{ totalPrice | currency:'EUR' }}</strong>
  </div>
  
  <button type="submit" [disabled]="orderForm.invalid" class="submit-button">
    Bestellen
  </button>
</form>

15.4 Validierung

15.4.1 Eingebaute Validatoren

Angular bietet verschiedene Validatoren in @angular/forms. Die häufigsten sind:

15.4.2 Benutzerdefinierte Validatoren

Für spezielle Validierungsanforderungen können Sie benutzerdefinierte Validatoren erstellen:

// custom-validators.ts
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

// Validator-Funktion für übereinstimmende Passwörter
export function matchingPasswordsValidator(passwordKey: string, confirmPasswordKey: string): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const password = control.get(passwordKey);
    const confirmPassword = control.get(confirmPasswordKey);
    
    if (!password || !confirmPassword) {
      return null;
    }
    
    return password.value === confirmPassword.value ? null : { passwordsMismatch: true };
  };
}

// Validator-Funktion für eine deutsche Telefonnummer
export function germanPhoneValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    if (!control.value) {
      return null;
    }
    
    // Einfache Regex für deutsche Telefonnummern
    const isValid = /^(\+49|0)[0-9]{7,14}$/.test(control.value);
    
    return isValid ? null : { invalidGermanPhone: true };
  };
}

Und ihre Anwendung:

// registration-form.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { matchingPasswordsValidator, germanPhoneValidator } from './custom-validators';

@Component({
  selector: 'app-registration-form',
  templateUrl: './registration-form.component.html',
})
export class RegistrationFormComponent implements OnInit {
  registrationForm!: FormGroup;
  
  constructor(private fb: FormBuilder) {}
  
  ngOnInit() {
    this.registrationForm = this.fb.group({
      username: ['', [Validators.required, Validators.minLength(4)]],
      email: ['', [Validators.required, Validators.email]],
      phone: ['', [Validators.required, germanPhoneValidator()]],
      passwordGroup: this.fb.group({
        password: ['', [
          Validators.required, 
          Validators.minLength(8),
          // Komplexitäts-Validator könnte hier hinzugefügt werden
        ]],
        confirmPassword: ['', Validators.required]
      }, { validators: matchingPasswordsValidator('password', 'confirmPassword') })
    });
  }
  
  onSubmit() {
    if (this.registrationForm.valid) {
      console.log('Registrierung eingereicht:', this.registrationForm.value);
    }
  }
  
  // Hilfsmethode, um einfacher auf verschachtelte Validierungsfehler zuzugreifen
  hasPasswordMismatch() {
    return this.registrationForm.get('passwordGroup')?.errors?.['passwordsMismatch'];
  }
}

15.4.3 Asynchrone Validatoren

Asynchrone Validatoren sind nützlich, wenn eine Validierung Serverinteraktionen erfordert, z.B. um zu prüfen, ob ein Benutzername bereits existiert:

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

@Injectable({
  providedIn: 'root'
})
export class AsyncValidatorsService {
  constructor(private http: HttpClient) {}
  
  uniqueUsername(): AsyncValidatorFn {
    return (control: AbstractControl): Observable<ValidationErrors | null> => {
      if (!control.value) {
        return of(null);
      }
      
      return of(control.value).pipe(
        debounceTime(500), // Warten, bis der Benutzer aufhört zu tippen
        switchMap(username => 
          this.http.get<boolean>(`/api/check-username?username=${username}`).pipe(
            map(isAvailable => isAvailable ? null : { usernameExists: true }),
            catchError(() => of(null)) // Fehler abfangen und null zurückgeben
          )
        ),
        first() // Nur den ersten Wert nehmen und dann komplett sein
      );
    };
  }
}

Verwendung in einem Formular:

// registration-form.component.ts (erweitert)
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AsyncValidatorsService } from './async-validators.service';

@Component({
  selector: 'app-registration-form',
  templateUrl: './registration-form.component.html',
})
export class RegistrationFormComponent implements OnInit {
  registrationForm!: FormGroup;
  
  constructor(
    private fb: FormBuilder,
    private asyncValidators: AsyncValidatorsService
  ) {}
  
  ngOnInit() {
    this.registrationForm = this.fb.group({
      username: [
        '', 
        [Validators.required, Validators.minLength(4)], 
        [this.asyncValidators.uniqueUsername()]
      ],
      // ... andere Felder ...
    });
  }
  
  // ... Rest der Komponente ...
}

15.5 Formulardaten senden

15.5.1 HTTP-Anfragen

Um Formulardaten an einen Server zu senden, verwenden Sie den HttpClient-Service:

// form-submission.component.ts
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-form-submission',
  templateUrl: './form-submission.component.html',
})
export class FormSubmissionComponent {
  form: FormGroup;
  submitting = false;
  submitSuccess = false;
  errorMessage = '';
  
  constructor(
    private fb: FormBuilder,
    private http: HttpClient
  ) {
    this.form = this.fb.group({
      name: ['', Validators.required],
      email: ['', [Validators.required, Validators.email]],
      message: ['', Validators.required]
    });
  }
  
  onSubmit() {
    if (this.form.invalid) return;
    
    this.submitting = true;
    this.submitSuccess = false;
    this.errorMessage = '';
    
    this.http.post<any>('/api/contact', this.form.value)
      .subscribe({
        next: () => {
          this.submitSuccess = true;
          this.form.reset();
        },
        error: (error) => {
          this.errorMessage = error.message || 'Ein Fehler ist aufgetreten.';
        },
        complete: () => {
          this.submitting = false;
        }
      });
  }
}
<!-- form-submission.component.html -->
<form [formGroup]="form" (ngSubmit)="onSubmit()">
  <!-- Formularfelder -->
  
  <button type="submit" [disabled]="form.invalid || submitting">
    <ng-container *ngIf="submitting; else submitButton">
      Wird gesendet...
    </ng-container>
    <ng-template #submitButton>
      Absenden
    </ng-template>
  </button>
  
  <div *ngIf="submitSuccess" class="success-message">
    Vielen Dank! Ihre Nachricht wurde erfolgreich gesendet.
  </div>
  
  <div *ngIf="errorMessage" class="error-message">
    {{ errorMessage }}
  </div>
</form>

15.6 Best Practices

15.6.1 Formular-Wiederverwendbarkeit

Erstellen Sie wiederverwendbare Formularkomponenten:

// address-form.component.ts
import { Component, Input, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';

@Component({
  selector: 'app-address-form',
  templateUrl: './address-form.component.html',
})
export class AddressFormComponent implements OnInit {
  @Input() parentForm!: FormGroup;
  @Input() formGroupName = 'address';
  
  constructor(private fb: FormBuilder) {}
  
  ngOnInit() {
    // Erstelle und füge die Adressgruppe zum übergeordneten Formular hinzu
    const addressGroup = this.fb.group({
      street: ['', Validators.required],
      city: ['', Validators.required],
      zipCode: ['', [Validators.required, Validators.pattern(/^\d{5}$/)]]
    });
    
    this.parentForm.addControl(this.formGroupName, addressGroup);
  }
}

15.6.2 Validierungsfehlermeldungen zentralisieren

Erstellen Sie einen Service zur Zentralisierung von Fehlermeldungen:

// validation-messages.service.ts
import { Injectable } from '@angular/core';
import { AbstractControl } from '@angular/forms';

@Injectable({
  providedIn: 'root'
})
export class ValidationMessagesService {
  getErrorMessage(control: AbstractControl, fieldName: string): string {
    if (!control || !control.errors) {
      return '';
    }
    
    const errors = control.errors;
    
    if (errors['required']) {
      return `${fieldName} ist erforderlich.`;
    }
    
    if (errors['email']) {
      return `Bitte geben Sie eine gültige E-Mail-Adresse ein.`;
    }
    
    if (errors['minlength']) {
      return `${fieldName} muss mindestens ${errors['minlength'].requiredLength} Zeichen lang sein.`;
    }
    
    if (errors['pattern']) {
      return `${fieldName} hat ein ungültiges Format.`;
    }
    
    // Benutzerdefinierte Fehler
    if (errors['passwordsMismatch']) {
      return `Die Passwörter stimmen nicht überein.`;
    }
    
    // Fallback für unbekannte Fehler
    return 'Ungültiger Wert.';
  }
}

15.6.3 Formularstatus-Verfolgung

Erstellen Sie eine Komponente zur Anzeige des Formularstatus (nützlich während der Entwicklung):

// form-debug.component.ts
import { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-form-debug',
  template: `
    <div class="form-debug" *ngIf="showDebug">
      <h3>Formular-Debug-Info</h3>
      <pre>Formular-Status: {{ form.status }}</pre>
      <pre>Formular gültig: {{ form.valid }}</pre>
      <pre>Formular-Werte: {{ form.value | json }}</pre>
    </div>
  `,
  styles: [`
    .form-debug {
      margin-top: 20px;
      padding: 10px;
      background-color: #f5f5f5;
      border: 1px solid #ddd;
    }
  `]
})
export class FormDebugComponent {
  @Input() form!: FormGroup;
  @Input() showDebug = true; // In Produktionsumgebungen auf false setzen
}

15.7 Typensicherheit in Formularen

Angular bietet verbesserte Typisierung für Formulare, was die Typensicherheit und das Entwicklererlebnis verbessert:

// Typisiertes Reactive Form
interface UserForm {
  name: string;
  email: string;
  address: {
    street: string;
    city: string;
    zipCode: string;
  };
}

// In der Komponente
const form = this.fb.group<UserForm>({
  name: ['', Validators.required],
  email: ['', [Validators.required, Validators.email]],
  address: this.fb.group({
    street: ['', Validators.required],
    city: ['', Validators.required],
    zipCode: ['', Validators.required]
  })
});

// Typensicherer Zugriff
const name = form.controls.name.value; // string
const city = form.controls.address.controls.city.value; // string

15.8 Form Builder Verbesserungen

Angular bietet einen verbesserten FormBuilder mit besserer Typunterstützung:

// typensicherer FormBuilder
const userForm = this.fb.nonNullable.group({
  name: ['', Validators.required],
  email: ['', [Validators.required, Validators.email]],
  age: [0, [Validators.required, Validators.min(18)]]
});

// Hier wird ein FormControl<string> erzeugt, der niemals null sein kann
const nameControl = this.fb.nonNullable.control('');

Angular bietet robuste und flexible Möglichkeiten zur Formularverarbeitung, sei es mit Template-driven Forms für einfachere Anwendungsfälle oder mit Reactive Forms für komplexere Szenarien.

Bei der Entscheidung zwischen Template-driven und Reactive Forms sollten Sie folgende Faktoren berücksichtigen:

Mit den in diesem Leitfaden vorgestellten Techniken und Beispielen sollten Sie in der Lage sein, effektive und benutzerfreundliche Formulare für Ihre Angular-Anwendungen zu erstellen.