Angular Reactive Forms: Automatische Validation-Hints
angular-freelancer.de
Posted on November 21, 2024
Die Validierung von Formularfeldern gehört für jeden Angular Developer zum Alltag. Das Anzeigen von Hinweisen in Formularen kann aber schnell zu viel repetetivem Boilerplate-Code führen - und den mag bekanntlich niemand.
Deshalb zeige ich in diesem Beitrag, wie man diesen Boilerplate-Code vermeidet und die Anzeige der Validierungshinweise streamlined - und sogar nachträglich einfach auf bestehenden Reactive Forms anwenden kann.
Setup
Bevor wir mit der Implementierung beginnen, richten wir ein neues Angular-Projekt ein. Ich nutze in diesem Beispiel Angular 18.
Initialisieren des Projekts:
ng new form-autovalidation-demo --style=scss --routing=false
Ins Projektverzeichnis wechseln:
cd form-autovalidation-demo
Nun erstellen wir die Komponenten / Directives die wir für die Demonstration benötigen:
ng generate component components/login-form
ng generate component components/form-validation-hint
ng generate directive directives/form-auto-validation-hint
ng generate directive directives/form-auto-validation-hint-location
Jetzt haben wir unser Grundgerüst:
- Eine LoginFormComponent, die Repräsentativ für alle weiteren Formulare steht.
- Eine FormValidationHinComponent, welche die Darstellung der Validiation-Hints übernimmt.
- Eine FormAutoValidationHintDirective sowie eine FormAutoValidationHintLocationDirective, welche die Automatisierung der Anzeige übernehmen und es ermöglichen, Fehler an einer spezifischen Stelle und nicht nur Direct am FormControl anzuzeigen.
Ein kleiner Hinweis: Fürs Aussehen habe ich Tailwind mittels dieser Zeile in der index.html inkludiert:
<script src="https://cdn.tailwindcss.com"></script>
Für das richtige Einbinden in produktive Applikationen bitte entsprechend das NPM-Package nutzen.
Das Design der Login Form habe ich von TailwindUI übernommen.
Das Login-Formular
In der LoginFormComponent erstellen wir ein einfaches Formular mit Reactive Forms, welches die Eingabe von E-Mail und Passwort ermöglicht und die Eingaben validiert:
login-form.component.ts
@Component({
selector: 'app-login-form',
standalone: true,
imports: [ReactiveFormsModule],
templateUrl: './login-form.component.html',
styleUrl: './login-form.component.scss'
})
export class LoginFormComponent {
readonly #fb: FormBuilder = inject(FormBuilder);
protected readonly form: FormGroup;
constructor() {
this.form = this.#fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6), Validators.maxLength(32)]],
});
}
protected onSubmit(): void {
if (this.form.valid) {
console.log('Form Submitted:', this.form.value);
} else {
this.form.markAllAsTouched();
}
}
}
login-form.component.html
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<h2 class="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">Login</h2>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form class="space-y-6" [formGroup]="form" (ngSubmit)="onSubmit()">
<div>
<label for="email" class="block text-sm/6 font-medium text-gray-900">Email address</label>
<div class="mt-2">
<input id="email" name="email" type="email" formControlName="email" autocomplete="email" required class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm/6">
</div>
</div>
<div>
<div class="flex items-center justify-between">
<label for="password" class="block text-sm/6 font-medium text-gray-900">Password</label>
</div>
<div class="mt-2">
<input id="password" name="password" type="password" formControlName="password" autocomplete="current-password" required class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm/6">
</div>
</div>
<div>
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" [disabled]="form.invalid">Sign in</button>
</div>
</form>
</div>
</div>
Implementierung der FormValidationHintComponent
Die FormValidationHintComponent zeigt später die Validierungshinweise eines FormControl basierend auf dessen Validierungsstatus an.
form-validation-hint.component.ts
@Component({
selector: 'app-form-hint',
standalone: true,
imports: [],
template: `
@if (hints().length > 0) {
<div class="flex flex-column gap-4 my-2">
@for (hint of hints(); track $index) {
<span class="text-red-600 text-sm leading-4">{{ hint }}</span>
}
</div>
}
`,
})
export class FormHintComponent {
readonly hints = input<string[]>([]);
}
Implementierung der Directives
Folgend werden die Directives implementiert, die später die automatisierte Anzeige der Validierungsnachrichten ermöglicht.
FormAutoValidationHintLocationDirective
Diese Directive dient als Helfer für die noch zu implementiertende FormAutoValidationHintDirective. Sie markiert ausschließlich den Container, an welchen der Validierungshinweis angehangen werden soll.
form-auto-validation-hint-location.directive.ts
@Directive({
selector: '[appFormAutoValidationHintLocation]',
standalone: true,
exportAs: 'hintLocation'
})
export class FormAutoValidationHintLocationDirective {
readonly hintLocationContainerRef = inject(ViewContainerRef);
}
Die Directive kann durch das exportAs
in Templates referenziert werden und stellt die ViewContainerRef nach außen zu Verfügung. Diese wird später dann durch die FormAutoValidationHintDirective genutzt.
FormAutoValidationHintDirective
Diese Directive übernimmt die hauptsächliche Arbeit und ist dafür zuständig, Validierungsfehler eines FormControls in lesbare Hinweise zu verwandeln und diese mittels der bereits definierten FormValidationHintComponent
anzuzeigen.
form-auto-validation-hint.directive.ts
const VALIDATION_HINTS: { [key: string]: string } = {
required: 'Dieses Feld ist erforderlich.',
email: 'Bitte geben Sie eine gültige E-Mail-Adresse ein.',
minlength: 'Das Feld muss mindestens {requiredLength} Zeichen lang sein.',
maxlength: 'Das Feld darf maximal {requiredLength} Zeichen lang sein.',
};
@Directive({
selector: '[appFormAutoValidationHint]',
standalone: true
})
export class FormAutoValidationHintDirective implements OnInit {
readonly #control = inject(NgControl);
readonly #viewContainerRef = inject(ViewContainerRef);
readonly #injector = inject(Injector);
#valueChanges: Signal<any> | undefined;
#statusChanges: Signal<FormControlStatus | undefined> | undefined;
#formControl: FormControl | undefined;
#hintRef: ComponentRef<FormHintComponent> | undefined;
readonly location = input<FormAutoValidationHintLocationDirective | undefined>(undefined);
ngOnInit(): void {
if(!this.#control) {
throw Error("Es konnte kein FormControl identifiziert werden, für welches Validierungs-Hints angezeigt werden sollen. Bitte sicherstellen, dass die Directive nur an FormControls eingesetzt wird.")
}
// Value Changes und Status Changes sind im Constructor noch nicht verfügbar, deshalb müssen wir im ngOnInit arbeiten
this.#formControl = this.#control as unknown as FormControl;
runInInjectionContext(this.#injector, () => {
if(this.#formControl) {
this.#valueChanges = toSignal(this.#formControl.valueChanges);
this.#statusChanges = toSignal(this.#formControl.statusChanges)
}
effect(() => {
// Bei Statusänderung und Wertänderung müssen die Hinweise aktualisiert werden
if(this.#valueChanges) this.#valueChanges();
let valid = false;
if(this.#statusChanges) valid = this.#statusChanges() === "VALID";
this.updateHints(valid);
});
});
}
private updateHints(valid: boolean): void {
if(!this.#hintRef) {
this.#hintRef = (this.location()?.hintLocationContainerRef ?? this.#viewContainerRef).createComponent(FormHintComponent);
}
if(valid) {
this.#hintRef.setInput("hints", []);
} else {
const hints = this.evaluateCurrentHints();
this.#hintRef.setInput("hints", hints);
}
}
private evaluateCurrentHints(): string[] {
const errors = this.#formControl?.errors
if(!errors) {
return [];
}
return Object.keys(errors).map(ek => {
const messageTemplate = VALIDATION_HINTS[ek];
const params = errors[ek];
return messageTemplate ? this.replaceParamPlaceholder(messageTemplate, params) : `Bitte überprüfen Sie ihre Eingaben. Unbekannter Fehler: ${ek}`;
});
}
private replaceParamPlaceholder(template: string, params?: {[key: string]: any}): string {
return template.replace(/\{(\w+)\}/g, (_, key) => params ? (params[key] ?? '') : '');
}
}
Die Directive greift via Injection auf das Form Control zu und reagiert auf Status- und Value-Änderungen. Hierbei greift Sie auf die Errors des Controls zu und nutzt die Namen der Errors um die Fehlertexte (in VALIDATION_HINTS definiert, können später erweitert oder von Außen definiert werden, je nachdem was die individuellen Anforderungen verlangen) zu identifizieren. Sollten die Fehlertexte Platzhalter enthalten, werden diese mit den Werten aus den Error-Details ersetzt, sofern diese Dort vorhanden sind.
Beispiel:
Das Passwort muss 6-32 Zeichen lang sein. Die Eingabe "Pa55W" führt zum folgenden Validierungsfehler:
{
"minlength": {
"requiredLength": 6,
"actualLength": 1
}
}
Die Information der requiredLength wird somit aus dem Error-Object in den Text eingesetzt.
Nutzung der Directiven
Folgend nutzen wir die implementierten Directives im Login Formular, um die Validierungen der Formulare sichtbar zu machen.
Anzeige direkt am FormControl
Um die Validierungshinweise direkt an einem FormControl anzuzeigen, muss diese nur an dem FormControl angebracht werden. Folgend am Beispiel der E-Mail im Login-Formular:
login-form.component.html
<!-- ... -->
<input appFormAutoValidationHint id="email" name="email" type="email" formControlName="email" autocomplete="email" required class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm/6">
<!-- ... -->
Anzeige an beliebiger Stelle
Um die Validierungshinweise an anderer Stelle anzuzeigen, müssen wir der FormAutoValidationHintDirective
sagen, an welcher Stelle der Hinweis angezeigt werden soll. Hierzu nutzen wir die FormAutoValidationHintLocationDirective
und weißen die Referenz auf diese der FormAutoValidationHintDirective
zu. Folgend am Beispiel des Passworts im Login-Formular:
login-form.component.html
<!-- ... -->
<div>
<div class="flex items-center justify-between">
<label for="password" class="block text-sm/6 font-medium text-gray-900">Password</label>
</div>
<div class="mt-2">
<input appFormAutoValidationHint [location]="loc" type="password" name="password" type="password" formControlName="password" autocomplete="current-password" required class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm/6">
</div>
</div>
<div appFormAutoValidationHintLocation #loc="hintLocation">
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm enabled:hover:bg-indigo-500 enabled:focus-visible:outline enabled:focus-visible:outline-2 enabled:focus-visible:outline-offset-2 enabled:focus-visible:outline-indigo-600 disabled:opacity-75" [disabled]="form.invalid">Sign in</button>
</div>
<!-- ... -->
Die Validierungshinweise des Passworts werden nun unterhalb des Buttons angezeigt.
Halbautomatisch auf bestehende Formulare applizieren
Wenn Ihre Applikation bereits über Formulare verfügt, die noch keine Hinweise enthält, Sie diese aber konsistent und einfach verteilen möchten, können Sie das relativ einfach erreichen. Hierzu müssen wir die FormAutoValidationHintDirective
um zwei Selector erweitern.
form-auto-validation-hint.directive.ts
@Directive({
selector: '[appFormAutoValidationHint], [formControl], [formControlName]',
standalone: true
})
Durch die Selektoren formControl
und formControlName
wird die Directive automatisch überall dort angewandt, wo FormControls via Reactive Forms genutzt werden. Die Directive muss nun nur in den einzelnen Components oder der Globalen Configuration als Provider angegeben werden. Nun werden die Hinweise in jedem Formular angezeigt. Ggf. muss Ihr Styling jedoch noch angepasst werden.
Fazit
Mit dieser Anpassung werden Validierungsfehler direkt am jeweiligen Eingabeelement angezeigt, ohne dass zusätzlicher Template-Code notwendig ist. Der Ansatz kombiniert die Stärken von Angular-Direktiven und Reactive Forms, um sowohl Entwicklerfreundlichkeit als auch Wartbarkeit zu maximieren.
Probieren Sie es aus und passen Sie die Logik bei Bedarf an Ihre spezifischen Anforderungen an!
Brauchen Sie Unterstützung? Kommen Sie gerne auf mich zu!
Posted on November 21, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.