GEDOPLAN
Webprogrammierung

Angular Formulare – Komponenten und Validatoren, Teil 1

Webprogrammierung
checklist 2077020 640

Ein großer Anteil in Businessanwendungen wird sicherlich den Formularen zufallen. Von der einfachen Eingabe eines Textes bis hin zu aufwendigen GUI-Komponenten haben sie doch alle eines gemeinsam: am Ende müssen die erfassten Daten im gewünschten Format in der gewünschten Qualität im Datenmodel landen. Den einzig richtigen Weg gibt es nicht, schauen wir uns also einige der möglichen Varianten an

checklist 2077020 640

Als einfaches Beispiel soll uns eine Kundennummer dienen die im Datenmodel ein festes Format aufzuweisen hat: 6stellig numerisch mit dem Präfix: „C-„. Die Grundlage für alles weitere stellt folgendes reaktives Formular dar.

Template

<form [formGroup]="form" (ngSubmit)="submit()">
    <input type="text" formControlName="customernumber1" />
</form>

Component

@Component({
  selector: 'ged-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {

  constructor(private fb: FormBuilder) { }

  form = this.fb.group({
    customernumber1: ['C-123456']
  });

  submit() {
    console.log(this.form.value);
  }
}

app.component.ts

Validierung

Eine einfache Validierung in Angular kann technisch gesehen eine einfache Funktion sein, die als Parameter einen Wert vom Typ AbstractControl entgegennimmt und einen booleschen Wert zurückliefert (alternativ ein Observable). Das stößt allerdings schnell an seine Grenzen, wenn wir Angular Services nutzen wollen die bekanntlich per Injektion zur Verfügung gestellt werden. Also implementieren wir das Ganze am besten gleich als Service:

@Injectable({
  providedIn: 'root'
})
export class CustomernumberValidatorService {

  private static readonly PATTERN = /^(C-)*\d{6}$/;

  validate(ctrl: AbstractControl) {
    const value = ctrl.value;
    const valid = CustomernumberValidatorService.PATTERN.test(value);
    if (value && !valid) {
      return {
        pattern: 'Ungültiges Format'
      };
    }
  }
}

customernumber-validator.service.ts

Per inject holen wir uns nun diesen Validator in unsere Komponente und verwenden sie bei der Erstellung unserer FormGroup. Optional können wir noch angeben, wann die Validierung / Übertragung ins Model erfolgen soll (Default: ‚change‘)

@Component({...})
export class AppComponent {

  constructor(private customernumberValidator: CustomernumberValidatorService) {}


  form = this.fb.group({
    customernumber1: [
        'C-123456', 
         { 
             validators: [this.customernumberValidator.validate],                 
             updateOn: 'blur' 
         }
        ]
  });

app.component.ts

Für die Template-Driven-Nutzer wird zusätzlich eine entsprechende Wrapper-Direktive benötigt. Hier ist insbesondere die Provider-Deklaration spannend, über die wir Angular mitteilen, das diese Direktive ein Validator ist und zusammen mit den anderen Validatoren ablaufen soll. Das verwendete Interface ist hingegen unspektakulär. Die Implementierung für unser Beispiel:

@Directive({
  selector: '[gedCustomernumberValidator]',
  providers: [
    { provide: NG_VALIDATORS, useExisting: forwardRef(() => CustomernumberValidatorDirective), multi: true }
  ]
})
export class CustomernumberValidatorDirective implements Validator {

  constructor(private customernumverValidatorService: CustomernumberValidatorService) { }

  validate(control: AbstractControl): ValidationErrors {
    return this.customernumverValidatorService.validate(control);
  }

  registerOnValidatorChange?(fn: () => void): void {
    // nothing to do
  }
}

customernumber-formatter.directive.ts (nur für Template-Driven)

Entsprechende Fehlermeldungen lassen sich dann über die Attribute „valid“ und „errors“ des FormControls verarbeiten. Zugriff darauf erhält man entweder durch eine entsprechende Template-Variable (Template-Driven) oder den Zugriff über die FormGroup (Model-Driven)

 
<input type="text" [(ngModel)]="templateModelValue" #templateModel="ngModel" />
{{templateModel.control}}

<input type="text" formControlName="customernumber1" />
{{form.controls['customernumber1']}}
  

Hilfreich ist hier sicherlich die Ausgabe der Meldungen zu vereinheitlichen und z.B. eine eigene Komponente zu entwickeln die dafür sorgt das alle Meldungen am Textfeld ausgeben werden.

@Component({
  selector: 'ged-validation-error-marker',
  template: '<span *ngIf="visible"> {{ message }} </span>',
  styleUrls: ['./validation-error-marker.component.scss']
})
export class ValidationErrorMarkerComponent{
  @Input()
  control: FormControl;

  get visible() {
    return this.control && !this.control.valid;
  }

  get message() {
    const errors = this.control.errors;
    return Object.keys(errors).map(errorKey => `${errorKey} : ${errors[errorKey]}`).join(', ');
  }
}

Verwendung

<input type="text" formControlName="customernumber2"/>
<ged-validation-error-marker 
    [control]="form.controls['customernumber2']">
</ged-validation-error-marker>
2019 11 21 10 11 06 nginput

Bei großen Formularen kommt es bekanntlich auf jede Zeile an und zu Recht mag man bei der gerade gezeigten Variante bemängeln, dass der Name des FormControl hier doppelt angegeben werden muss. Zudem muss in Sachen Styling hier berücksichtigt werden das die Eingabekomponente und die Ausgabe der Fehlermeldungen zwei HTML-Komponenten sind. Überredet… eine Lösung für die angesprochenen Punkte, ohne die Flexibilität zu verlieren, könnte folgende Komponente sein:

@Component({
  selector: 'ged-input',
  templateUrl: './input.component.html',
  styleUrls: ['./input.component.scss']
})
export class InputComponent{
  @ContentChild(NgControl, { static: false })
  control: NgControl;
}
<div class="input">
  <ng-content></ng-content>
  <ged-validation-error-marker [control]="control"></ged-validation-error-marker>
</div>

Die Verwendung ist dann zum einen flexibel, weil wir beliebige Eingabekomponenten verwenden können, übersichtlich, da wir keinen eigenen Layout-Container um unsere Eingaben legen müssen und weniger Fehleranfällig, da der Name des FormControlls nur noch einmal angegeben werden muss und per ContetChild-Injection innerhalb der Komponente zur Verfügung gestellt wird und dazu noch beliebig erweiterbar, z.B. um die Ausgabe eines Labels:

Verwendung

    <ged-input>
      <input type="text" formControlName="customernumber4"/>
    </ged-input>

Zeit für einen Kaffee, also machen wir einen 2-Teiler daraus. Bis dahin:

Auf GitHub? Klar. https://github.com/GEDOPLAN/ng-input

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Bitte füllen Sie dieses Feld aus.
Bitte füllen Sie dieses Feld aus.
Bitte gib eine gültige E-Mail-Adresse ein.
Sie müssen den Bedingungen zustimmen, um fortzufahren.

Autor

Diesen Artikel teilen

LinkedIn
Xing

Gibt es noch Fragen?

Fragen beantworten wir sehr gerne! Schreibe uns einfach per Kontaktformular.

Kurse

weitere Blogbeiträge

IT-Training - GEDOPLAN
Webprogrammierung

Angular, testen mit Karma/Jasmine

Anwendungen zu testen ist ein leidiges Thema und wird gerne aus Zeitgründen vernachlässigt. Jeder der schon mal ein größeres Refactoring…
road 368719 640
Jakarta EE (Java EE)

Startup-Event in CDI 4

Während Plattformen wie Quarkus einen proprietären Event feuern, wenn einen Anwendung gestartet ist, kannte der Standard Jakarta EE dies bislang…

Work Life Balance. Jobs bei Gedoplan

We are looking for you!

Lust bei GEDOPLAN mitzuarbeiten? Wir suchen immer Verstärkung – egal ob Entwickler, Dozent, Trainerberater oder für unser IT-Marketing! Schau doch einfach mal auf unsere Jobseiten! Wir freuen uns auf Dich!

Work Life Balance. Jobs bei Gedoplan

We are looking for you!

Lust bei GEDOPLAN mitzuarbeiten? Wir suchen immer Verstärkung – egal ob Entwickler, Dozent, Trainerberater oder für unser IT-Marketing! Schau doch einfach mal auf unsere Jobseiten! Wir freuen uns auf Dich!