Angular bietet mit Reactive Forms eine einfache Möglichkeit, Benutzereingaben zu verarbeiten. Zu jedem guten Formular gehört auch die Validierung der Felder. Hierzu verwendet man idealerweise die von Angular zur Verfügung gestellten Validatoren wie z. B. required() oder min().

Für komplexere Validierungen gibt es Custom Validators. Diese Funktion lässt sich nutzen, um die Validierung für ein Feld abhängig von einem anderen Feld an- oder abzuschalten. Was sich dahinter verbringt, schauen wir uns im Folgenden genauer an.

Die Vorgehensweise betrachten wir am Beispiel ein Bestellformulars eines Onlineshops. Das Formular soll fünf Pflichtfelder enthalten:

  • Email-Adresse
  • Vor- und Nachname
  • Straße
  • PLZ
  • Stadt

Zusätzlich gibt es eine Checkbox „Abweichende Rechnungsadresse“. Ist die Checkbox markiert, so sollen drei weitere Felder (Straße, PLZ und Stadt) verpflichtend sein. Die Idee ist, dass die Felder der Rechnungsadresse einen Custom Validator erhalten. Der Custom Validator schaut nach dem Value der Checkbox. Abhängig des Values sind die Felder der Rechnungsadresse verpflichtend oder nicht.

Zur Erstellung des Formulars wird der FormBuilder verwendet. Außerdem muss das ReactiveFormsModule im Module importiert werden. Wir erstellen also zunächst das Formular in der Component, welche alle Controls hält:

export class AppComponent {
  form = this.fb.group({
    email: ['', [Validators.email, Validators.required]],
    fullName: ['', Validators.required],
    address: this.fb.group({
      street: ['', Validators.required],
      city: ['', Validators.required],
      zip: ['', Validators.required]
    }),
    abweichendeRechnungsadresse: [false],
    invoiceStreet: [''],
    invoiceZip: [''],
    invoiceCity: [''],
  });

  constructor(private fb: FormBuilder) {
  }

  onSubmit() {
    console.warn(this.form.value);
  }

}

// Abb. 1

Die Adressfelder sind in einer FormGroup. Während die Rechnungsadressfelder in der Parentgroup sind. Dies ist der Custom Validation geschultet, wie wir später sehen werden.

Das <form>-Element im HTML-Template ist wie folgt aufgebaut:

<form (ngSubmit)="onSubmit()" [formGroup]="form">

  <label>
    Email:
    <input formControlName="email" type="text">
  </label>

  <label>
    Vor- und Nachname:
    <input formControlName="fullName" type="text">
  </label>

  <div formGroupName="address">
    <p>Adresse</p>

    <label>
      Straße:
      <input formControlName="street" type="text">
    </label>

    <label>
      PLZ:
      <input formControlName="zip" type="text">
    </label>

    <label>
      Stadt:
      <input formControlName="city" type="text">
    </label>
  </div>

  <p>Rechnungsadresse</p>

  <div>
    <input formControlName="abweichendeRechnungsadresse" id="abweichendeRechnungsadresse" type="checkbox">
    <label for="abweichendeRechnungsadresse">Abweichende Rechnungsadresse</label>
  </div>

  <div>
    <label>
      Straße:
      <input formControlName="invoiceStreet" type="text">
    </label>

    <label>
      PLZ:
      <input formControlName="invoiceZip" type="text">
    </label>

    <label>
      Stadt:
      <input formControlName="invoiceCity" type="text">
    </label>
  </div>

  <button [disabled]="!form.valid" type="submit">Absenden</button>
</form>

<!-- Abb. 2 -->

Aufbau von Customer Validators

Als nächstes implementieren wir den Custom Validator. Hierzu werfen wir einen Blick in die Angular Dokumentation der Custom Validator. Hier sehen wir, dass der Validator eine Funktion ist, welche selbst eine Funktion vom Typ ValidatorFn zurückgibt. Die ValidatorFn erwartet einen AbstractControl — in unserem Fall abweichendeRechnungsadresse — und gibt ValidationErrors oder null zurück.

export function conditionalCustomValidator(conditionalControl: string): ValidatorFn {
  return (control: AbstractControl): { [key: string]: any } | null => {
    try {
      const required = control.parent.controls[conditionalControl].value ? true : false;
      return required ? Validators.required(control) : null;
    } catch (e) {
      return null;
    }
  };
}

// Abb. 3

Schauen wir uns die Funktion genauer an. Der Name des Custom Validators ist „conditionalCustomValidator“ und erwartet einen Parameter vom Typ string. Der Parameter wird im obigen Beispiel abweichendeRechnungsadresse sein. Hier muss man beachten, dass „abweichendeRechnungsadresse“ der Name des Controls ist, und nicht das Control selbst.

Die ValidatorFn, welche returned wird, erwartet ein AbstractControl. Das AbstractControl ist das Control, in welchem der Custom Validator aufgerufen wird. Um es konkret zu machen, erweitern wir das Formular aus Abb. 1:

invoiceStreet: ['', conditionalCustomValidator('abweichendeRechnungsadresse')],
invoiceZip: ['', conditionalCustomValidator('abweichendeRechnungsadresse')],
invoiceCity: ['', conditionalCustomValidator('abweichendeRechnungsadresse')],

// Abb. 4

Von jedem Control kann mit .parent auf die darüber liegende Ebene im FormGroup zugegriffen werden. Hier sehen wir auch, warum die Rechnungsadressfelder nicht in einer eigenen FormGroup sind.

Mit control.parent.controls['abweichendeRechnungsadresse'].value wird also auf den Wert der Checkbox (true oder false) zugegriffen. Abhängig vom Wert true oder false wird der Required-Validator oder null zurückgegeben.

Aufruf des Custom Validators

Zu beachten ist, dass der Custom Validator nur aufgerufen wird, wenn im entsprechenden Feld, z. B. invoiceStreet, eine Änderung gemacht wird. Für unseren Fall soll aber der Custom Validator aufgerufen werden, wenn die Checkbox markiert wird. Hierzu schreiben wir folgende Funktion:

updateCheckbox() {
  this.form.get('invoiceStreet').updateValueAndValidity();
  this.form.get('invoiceZip').updateValueAndValidity();
  this.form.get('invoiceCity').updateValueAndValidity();
}

// Abb. 5

Die Funktion wird beim Anklicken der Checkbox aufgerufen:

<input (change)="updateCheckbox()" 
    formControlName="abweichendeRechnungsadresse"
    id="abweichendeRechnungsadresse" 
    type="checkbox">

<!-- Abb. 6 -->

Der vollständige Code ist auf GitHub zu finden.

Abschließend noch drei Screenshots, welche das Verhalten visuell verdeutlichen. Der „Absenden“-Button is ausgegraut, wenn das Formular invalide ist:

Ein Screenshot zeigt das gültige Formular.
Abb. 7: Die Checkbox ist nicht markiert. Alle Pflichtfelder sind ausgefüllt und das Formular damit valide.
Ein Screenshot zeigt das ungültige Formular.
Abb. 8: Die Checkbox ist markiert; Die Rechnungsadressfelder sind dadurch Pflichtfelder, aber noch nicht ausgefüllt. Das Formular ist invalide.
Ein Screenshot zeigt das gültige Formular mit ausgefüllten Rechnungsadressfeldern.
Abb. 9: Die Checkbox ist markiert. Alle Pflichtfelder—inkl. Rechnungsadressfelder—sind ausgefüllt. Das Formular ist valide.