Reactive Forms offers a robust way to manage form inputs, validations, and user interactions. In a previous article, we discussed managing the disabled
property using ControlValueAccessor
for custom form controls. This time, let's take it a step further by exploring how to implement custom validators for these controls to ensure your forms are not only flexible but also secure and reliable.
What Are Custom Validators?
Validators in Angular are functions that check the value of a form control and determine if it is valid or invalid based on specific criteria. Angular provides built-in validators like Validators.required
and Validators.email
, but there are scenarios where you need custom validation logic. This is particularly important when working with custom form controls that implement ControlValueAccessor
.
Why Use Custom Validators with ControlValueAccessor?
When you create custom form controls using ControlValueAccessor
, integrating validation logic ensures that these controls work seamlessly within your forms. Without proper validation, you might expose your application to potential issues, such as allowing incorrect or insecure data to be submitted.
Implementing Custom Validators
Let's start by implementing a basic custom validator. Suppose we have a custom form control for an input that accepts usernames, and we want to validate that the username is at least 5 characters long.
import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";
// Custom Validator to check the length of the username
export function usernameValidator(minLength: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value;
if (value && value.length < minLength) {
return {
usernameTooShort: {
requiredLength: minLength,
actualLength: value.length,
},
};
}
return null;
};
}
This usernameValidator
function returns a ValidatorFn
, which checks if the control's value meets the minimum length requirement. If it doesn't, it returns an error object; otherwise, it returns null
.
Applying the Validator in a Reactive Form
Next, let's see how to apply this custom validator in a Reactive Form that includes a custom control implementing ControlValueAccessor
.
import { Component } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
import { usernameValidator } from "./validators/username-validator";
@Component({
selector: "app-root",
template: `
<form [formGroup]="form">
<app-custom-input formControlName="username"></app-custom-input>
<div *ngIf="form.get('username')?.errors?.usernameTooShort">
Username must be at least
{{ form.get("username")?.errors?.usernameTooShort.requiredLength }}
characters long.
</div>
</form>
`,
})
export class AppComponent {
form = new FormGroup({
username: new FormControl("", [usernameValidator(5)]),
});
}
In this example, the username
form control uses the usernameValidator
, ensuring that the username meets the length requirement. If the validation fails, an error message is displayed.
Integrating Validators with ControlValueAccessor
To ensure that your custom control works correctly with validation, you need to make sure it calls the validation functions appropriately. Angular automatically handles this when you register your control with a form group, but if your control has complex logic, you might need to trigger validation manually.
Here's how you can update the CustomInputComponent
to trigger validation:
import { Component, forwardRef, Input } from "@angular/core";
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
NG_VALIDATORS,
Validator,
AbstractControl,
ValidationErrors,
} from "@angular/forms";
@Component({
selector: "app-custom-input",
template: `<input [disabled]="isDisabled" (input)="onInput($event)" />`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true,
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CustomInputComponent),
multi: true,
},
],
})
export class CustomInputComponent implements ControlValueAccessor, Validator {
@Input() isDisabled = false;
private value: string = "";
private onChange: (value: any) => void;
private onTouched: () => void;
writeValue(value: any): void {
this.value = value;
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.isDisabled = isDisabled;
}
onInput(event: Event): void {
const input = event.target as HTMLInputElement;
this.value = input.value;
this.onChange(this.value);
}
validate(control: AbstractControl): ValidationErrors | null {
return control.value ? null : { required: true };
}
}
In this example, the CustomInputComponent
implements the Validator
interface, allowing it to provide its own validation logic. The validate
method checks if the control's value is valid and returns an error if it isn't.
Handling Validation Errors
To display validation errors, you can conditionally render error messages in the template based on the form control's error state. This was shown earlier with the usernameTooShort
error, but you can expand this to handle multiple validators.
Combining Multiple Validators
You can easily combine multiple validators for a single control. For instance, you might want to validate both the length of the username and ensure it's not a restricted keyword:
export function restrictedUsernameValidator(
restrictedNames: string[]
): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value;
if (value && restrictedNames.includes(value)) {
return { usernameRestricted: { value } };
}
return null;
};
}
const combinedValidators = [
usernameValidator(5),
restrictedUsernameValidator(["admin", "root"]),
];
Apply these validators to the form control:
form = new FormGroup({
username: new FormControl("", combinedValidators),
});
Dynamic Validation
Sometimes, you need to change the validation rules based on other form values or user input. This can be achieved by updating the form control's validator dynamically:
this.form.get("username")?.setValidators([usernameValidator(8)]);
this.form.get("username")?.updateValueAndValidity();
This approach allows you to adjust the validation logic as needed, ensuring the form remains valid under different conditions.
Conclusion
Implementing custom validators with ControlValueAccessor
in Angular ensures that your custom form controls are not only flexible but also secure and reliable. By following these best practices, you can create robust form components that seamlessly integrate with Angular’s Reactive Forms, providing a consistent and user-friendly experience across your application.
By leveraging custom validators, you ensure that your forms meet the specific needs of your application while maintaining the high standards of data integrity and user input validation.
Top comments (1)
Important topic to cover could be async Angular form validators. Angular forms have pretty poor support for that.