Develop Custom Validators

Learn how to create custom validators to validate complex requirements.

When building complex forms, the available built-in validators could be insufficient to fulfill the sophisticated requirements. Custom validators provide much more flexibility to achieve our goals in these cases.

Press + to interact

What are custom validators?

In its simplest form, a custom validator is a function that returns null if everything is fine or an ValidationErrors object type if the validation found any error.

Press + to interact
import { AbstractControl } from '@angular/forms';
export function onlyPositiveNumbers(control: AbstractControl): ValidationErrors | null {
if (control?.value < 0) {
// The control's value contains a negative number
return { invalidNumber: true }
}
// No errors found
return null;
}

The form control is passed to the function as a parameter, and it can be of any kind: FormControl, FormGroup, or FormArray. That is why, typically, the base class AbstractControl is the type used for the validator's parameter to keep a higher level of abstraction.

Create a custom validator: First version

Let’s create a custom validator to check our users’ age and accept subscriptions to our website if they’re older than 18.

Note: Angular automatically sets the control’s state to VALID if the validator returns null, or INVALID if it returns an object.

The error object structure is pretty straightforward and comes in the { [key: string]: any; } form. The choice for the error’s key is open, but we should always choose a meaningful name. For our age validation example, the error object will be { invalidAge: true }.

Press + to interact
export function minAgeValidator(control:AbstractControl) : ValidationErrors | null {
const minAcceptedAge = 18;
const userAge = control.value;
if (userAge && (userAge < minAcceptedAge || isNaN(userAge))) {
return { 'invalidAge': true };
}
return null;
}

Once we have a custom validator available, we can apply it in a reactive form to process a field's value accordingly.

Press + to interact
import { minAgeValidator } from './min-age-validator';
@Component({
// ...
})
export class SampleComponent {
constructor(private fb: FormBuilder) { }
const registrationForm = this.fb.group({
username = this.fb.control('', Validators.required),
age = this.fb.control('', minAgeValidator)
});
}

To show an error message in the template, the invalidAge validation error key can be used in combination with the hasError() method.

Press + to interact
<mat-form-field>
<mat-label>Age</mat-label>
<input type="number" min="0" matInput formControlName="age" />
<mat-error *ngIf="age?.hasError('invalidAge')">
{{ "You must be older than " + minAge + " yo to register." }}
</mat-error>
</mat-form-field>

However, even if our validator works perfectly fine, it has a substantial limitation. Changing the age limit is impossible, drastically reducing the code reusability and the DRY (Don’t Repeat Yourself) principle. Let’s refactor the current solution by adding the possibility of passing a parameter to establish the age limit.

Create an improved custom validator: Second version

A custom validator can also be a factory function—a function returning another function—that represents the actual validator applying specific validation rules in order to receive control.

Let’s refactor the minAgeValidator by adding a number parameter to our age threshold. This way, the age value is provided outside, typically by the component implementing the reactive form. The threshold can now vary dynamically according to the context of use—no need to hardcode it in the validator’s code anymore.

Press + to interact
export function minAgeValidator(minAcceptedAge: number): ValidatorFn {
return (control:AbstractControl) : ValidationErrors | null => {
const userAge = control.value;
if (userAge && (userAge < minAcceptedAge || isNaN(userAge))) {
return { 'invalidAge': true };
}
return null;
}
}

When we use the validator, we can call it as a normal function, passing the specified age limit value as a parameter.

Press + to interact
import { minAgeValidator } from './min-age-validator';
@Component({
// ...
})
export class SampleComponent {
readonly minAge = 21;
constructor(private fb: FormBuilder) { }
const registrationForm = this.fb.group({
username = this.fb.control('', Validators.required),
age = this.fb.control('', minAgeValidator(minAge))
});
}

Complete solution

In the code below, we have the complete solution for the concepts discussed in this lesson. In the code, we can find the improved case for the custom validator where we pass the age as a parameter, ensuring higher reusability.

Note: Feel free to extend the validator checks and test different conditions for the given form.

import { AbstractControl, ValidatorFn, ValidationErrors } from '@angular/forms';

export function minAgeValidator(minAcceptedAge: number): ValidatorFn {
    return (control:AbstractControl) : ValidationErrors | null => {

        const userAge = control.value;

        if (userAge && (userAge < minAcceptedAge || isNaN(userAge))) {
            return { 'invalidAge': true };
        }

        return null;        
    }
}
Complete solution