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 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>
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: