Form validation is a critical aspect of any web application, ensuring that user inputs adhere to the expected format and preventing incorrect data submission. In Angular, you often need to display validation error messages dynamically, and in multi-language applications, providing these messages in the user's preferred language is essential.
Why Use a Custom Directive?
Directives in Angular are powerful tools for extending the behavior of DOM elements. Using a directive to handle form validation allows us to decouple error message handling from the form logic, making the code more modular and easier to maintain. Additionally, when used in combination with a translation service like Transloco, we can seamlessly handle multi-language support for error messages without duplicating code in components.
Directive Code: FormErrorDirective
import { Directive, DoCheck, ElementRef, HostBinding, inject, Input, Renderer2 } from '@angular/core';
import { NgControl } from '@angular/forms';
import { TranslocoService } from '@ngneat/transloco';
@Directive({
selector: '[formControlName], [formControl]',
standalone: true,
})
export class FormErrorDirective implements DoCheck {
@Input() formControlName!: string;
private elementRef: ElementRef = inject(ElementRef);
private renderer: Renderer2 = inject(Renderer2);
private translocoService: TranslocoService = inject(TranslocoService);
private errorElement!: HTMLElement;
constructor(private ngControl: NgControl) {}
ngDoCheck(): void {
this.updateErrorDisplay();
}
@HostBinding('class.form-element__error')
get inValidClass(): boolean {
return (this.ngControl.control?.touched && this.ngControl.control.invalid) || false;
}
private updateErrorDisplay(): void {
const control = this.ngControl.control;
if (control?.untouched && control.root.invalid) {
return;
}
const errors = control?.errors;
if (errors) {
this.displayError(this.getErrorMessage(errors));
} else {
this.clearError();
}
}
private getErrorMessage(errors: any): string {
const firstErrorKey = Object.keys(errors)[0];
return this.translocoService.translate(
`${firstErrorKey}`,
errors[firstErrorKey]
);
}
private displayError(errorMessage: string): void {
if (!this.errorElement) {
this.createElement();
}
this.renderer.setProperty(this.errorElement, 'innerHTML', errorMessage);
this.renderer.appendChild(
this.elementRef.nativeElement.parentNode,
this.errorElement
);
}
private clearError(): void {
if (this.errorElement) {
this.renderer.removeChild(
this.elementRef.nativeElement.parentNode,
this.errorElement
);
this.errorElement = null!;
}
}
private createElement(): void {
this.clearError();
this.errorElement = this.renderer.createElement('span');
this.renderer.addClass(this.errorElement, 'form-element__error-message');
}
}
Step-by-Step Breakdown
@Directive Decorator
The @Directive decorator defines the directive and its selector, which targets form controls using formControlName or formControl.
NgControl and Form Control Binding
The NgControl allows us to access the form control's state (validity, touched, etc.), while @HostBinding applies a CSS class when the control is invalid and touched.
Error Handling
The ngDoCheck lifecycle hook is used to detect changes in the form control's validation state. The updateErrorDisplay function checks for errors, and if found, it translates and displays the corresponding error message.
Dynamic Translations
Using Transloco, we translate error messages based on the form validation keys (e.g., required, minlength, etc.) and their respective values (e.g., minimum length of characters).
DOM Manipulation with Renderer2
To safely manipulate the DOM, we inject Renderer2 and use it to create or remove error message elements in the form. This ensures the code remains efficient and compatible across different platforms and environments (such as server-side rendering).
Using the Directive
To use the FormErrorDirective in a form, apply it to any form control as follows:
<form [formGroup]="form">
<label for="username">Username</label>
<input id="username" formControlName="username">
!!-- Directive Automatically insert error code at this place
when this form control has an invalid state
and remove it when it is again valid --!!
</form>
Conclusion
By creating this custom directive, you can easily manage form validation errors in Angular while keeping your code modular and maintainable. Translating error messages dynamically with Transloco ensures your application is user-friendly in any supported language.
