Angular Formulare – Formatter und Parser, Teil 2

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

Als einfaches Beispiel dient uns wie zuvor das Beispiel der Kundennummer Format: 6stellig numerisch mit dem Präfix: „C-„. Im ersten Teil haben wir für einen Validator gesorgt der dieses Format überprüft. In diesem Teil gehen wir einen Schritt weiter. Wir wollen dem Benutzer ein etwas toleranteres Eingabeverhalten bieten: Die Kundennummer darf auch nur aus 6 numerischen Werten bestehen. Angular sollte in diesem Fall natürlich dafür sorgen das unser Format ( C-XXXXXX ) trotzdem eingehalten wird.

Pragmatisch

Eine sehr einfache pragmatische Lösung ist es mittels Event-Listener die eingegebenen Werte anzupassen, falls benötigt. Dazu ist nicht viel nötig, außer eine entsprechende Methode innerhalb der Komponente-klasse

Template

 <input type="text" formControlName="customernumber1" 
         (blur)="formatNumber('customernumber1')" />

Komponente

  formatNumber(formControlName) {
    const control = this.form.controls[formControlName];
    if (control.value && control.valid && !/^C-.*?/.test(control.value)) {
      control.patchValue('C-' + control.value);
    }
  }

Erfüllt seinen Zweck und mag für einige Anwendungsfälle ausreichen. Diese Lösung ist aber kaum wiederverwendbar, kapselt es die Logik zur Formatierung doch in einer konkreten Komponente. Selbst, wenn wir dies in einen separaten Service auslagern bleibt der fade Beigeschmack das es hier eine elegantere Lösung gibt

Formatter als Direktive

Warum also die Logik nicht aus der Komponente herauslösen und das Ganze in eine separate Direktive verpacken

@Directive({
  selector: '[gedCustomernumberFormatter]'
})
export class CustomernumberFormatterDirective {

  constructor(private el: ElementRef, 
              private renderer: Renderer2,
              private control: NgControl) { }

  @HostListener('blur', ['$event.target.value'])
  onBlur(val: string) {
    if (this.control.valid && val && !/^C-.*/.test(val)) {
      this.renderer.setProperty(this.el.nativeElement, 'value', 'C-' + val);
    }
  }
}

Eine klassische Direktive. Wir registrieren uns per HostListener-Binding an das Blur-Event des Eingabefeldes und formatieren den übergebenen Wert. Kleines Gimmick: wir injizieren uns das zugrundeliegende FormControl, um die Formatierung der Eingabe nur dann vorzunehmen wenn die Validierung auch erfolgreich war

Verwendung

<input type="text" formControlName="customernumber2" gedCustomernumberFormatter />

Formatter als ValueAccessor

Mit den Lösungen oben haben wir für die meisten der Situationen das passende Mittel zu Hand. Eine kleine Anpassung unserer Anforderung führt aber zur Erforderniss einer Alternative: der Anwender soll unsere Präfix („C-„), das später Teil des Datensatzes sein muss, nicht sehen . Allgemein ausgedrückt: der eingegeben / sichtbare Wert soll sich vom Wert der im Datenmodel vorliegt unterscheiden. Von einer pragmatischen Lösung wie oben innerhalb der konkreten Komponente, gibt es auch hier einen eleganten Weg: einen eigenen ControlValueAccessor der das Bindeglied zwischen Komponenten-Klasse und UI darstellt

@Directive({
  selector: '[gedCustomernumberFormatterAccessor]',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomernumberFormatterAccessorDirective),
      multi: true
    }
  ]
})
export class CustomernumberFormatterAccessorDirective implements ControlValueAccessor {

  constructor(private el: ElementRef, private renderer: Renderer2) { }

  updateValue: any;
  touched: any;

  writeValue(obj: any): void {
    obj = obj.replace('C-', '');
    this.renderer.setProperty(this.el.nativeElement, 'value', obj);
  }

  registerOnChange(fn: any): void {
    this.updateValue = fn;
  }

  @HostListener('blur', ['$event'])
  onInput(event: any) {
    this.touched();
    let val = event.target.value;

    if (!/^C-/.test(val)) {
      val = 'C-' + val;
    } else {
      this.renderer.setProperty(this.el.nativeElement, 'value', val.replace('C-', ''));
    }

    this.updateValue(val);
  }

  registerOnTouched(fn: any): void {
    this.touched = fn;
  }
}


Die spannenden Stellen Zusammengefasst:

  • providers-Array, wir registrieren unsere Komponente als NG_VALUE_ACCESSOR, ähnlich wie bei den Validatoren um Angular mitzuteilen, das es sich bei dieser Komponente um ein Control handelt
  • Interface ControlValueAccesor, mit den Methoden
    • writeValue, Model > View, hier entfernen wir das Präfix, falls hier initiale Daten von der FormControl übergeben werden
    • registerOnChange, Change-Methode registrieren die wir später bei Änderungen triggern werden (wird von Angular aufgerufen, wenn die Komponente der FormGroup hinzugefügt wird)
  • HostListener, wenn der Focus das Textfeld verlässt sorgen wir dafür, dass:
    • das „C-“ Präfix zum übertragenden Wert hinzugefügt wird, falls nicht vorhanden
    • das „C-“ Präfix im Textfeld entfernt wird, falls der Benutzer es eingeben hat (optional)
    • der geänderte Wert (Benutzereingabe + Präfix) an die vorher registrierte Change-Methode übergeben wird

Verwendung

<input type="text" formControlName="customernumber3" gedCustomernumberFormatterAccessor />

Cool. Das erlaubt es uns (gekapselt und wiederverwendbar) Benutzereingaben bei der weiteren Verarbeitung zu formatieren, ohne das der Benutzer dies wissen / sehen muss.

Gibt es das auch bei GitHub? Klar. https://github.com/GEDOPLAN/ng-input

Über Dominik Mathmann
Dominik Mathmann arbeitet als Berater, Entwickler und Trainer bei der GEDOPLAN GmbH. Auf Basis seiner langjährigen Erfahrung in der Implementierung von Java-EE-Anwendungen leitet er Seminare, hält Vorträge und unterstützt Kunden bei der Konzeption und Realisierung von Webanwendungen vom Backend bis zum Frontend. Sein derzeitiger Schwerpunkt liegt dabei auf der Entwicklung von JSF-Anwendungen. Er sieht die stärker werdende Position von JavaScript-Frameworks jedoch positiv und beschäftigt sich darüber hinaus mit Webframeworks wie Angular.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden /  Ändern )

Google Foto

Du kommentierst mit Deinem Google-Konto. Abmelden /  Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden /  Ändern )

Verbinde mit %s

%d Bloggern gefällt das: