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