Angular Custom Validators
Forms are a critical part of web applications, and proper validation is essential for ensuring data integrity and improving user experience. While Angular provides several built-in validators like required
, minLength
, and pattern
, you'll often need custom validation logic specific to your application. This is where custom validators come in.
What Are Custom Validators?
Custom validators in Angular are functions that check form control values against specific rules and return validation errors when the rules aren't met. They allow you to implement business-specific validation logic that goes beyond basic validations.
Types of Custom Validators
In Angular, you can create two types of custom validators:
- Sync Validators - Execute immediately and return results
- Async Validators - Return a Promise or Observable for validations that need to wait (like server-side checks)
Creating a Basic Custom Validator
Let's start by creating a simple custom validator that ensures a password contains at least one special character:
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export function specialCharValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/.test(control.value);
return hasSpecialChar ? null : { specialChar: true };
};
}
This validator:
- Takes a form control as input
- Tests if it contains a special character using a regular expression
- Returns
null
if valid (contains special character) - Returns an error object
{ specialChar: true }
if invalid
Using Custom Validators in Reactive Forms
To use this validator in a reactive form:
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { specialCharValidator } from './validators/special-char.validator';
@Component({
selector: 'app-signup-form',
templateUrl: './signup-form.component.html'
})
export class SignupFormComponent {
signupForm: FormGroup;
constructor(private fb: FormBuilder) {
this.signupForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [
Validators.required,
Validators.minLength(8),
specialCharValidator()
]]
});
}
get password() {
return this.signupForm.get('password');
}
onSubmit() {
if (this.signupForm.valid) {
console.log('Form submitted:', this.signupForm.value);
}
}
}
And the corresponding HTML:
<form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
<div>
<label for="email">Email:</label>
<input id="email" type="email" formControlName="email">
<div *ngIf="signupForm.get('email')?.errors?.required && signupForm.get('email')?.touched">
Email is required
</div>
<div *ngIf="signupForm.get('email')?.errors?.email && signupForm.get('email')?.touched">
Please enter a valid email
</div>
</div>
<div>
<label for="password">Password:</label>
<input id="password" type="password" formControlName="password">
<div *ngIf="password?.errors?.required && password?.touched">
Password is required
</div>
<div *ngIf="password?.errors?.minlength && password?.touched">
Password must be at least 8 characters
</div>
<div *ngIf="password?.errors?.specialChar && password?.touched">
Password must include at least one special character
</div>
</div>
<button type="submit" [disabled]="signupForm.invalid">Sign Up</button>
</form>
Using Custom Validators in Template-Driven Forms
For template-driven forms, we need to create a directive:
import { Directive } from '@angular/core';
import { AbstractControl, NG_VALIDATORS, Validator } from '@angular/forms';
import { specialCharValidator } from './validators/special-char.validator';
@Directive({
selector: '[appSpecialChar]',
providers: [{
provide: NG_VALIDATORS,
useExisting: SpecialCharDirective,
multi: true
}]
})
export class SpecialCharDirective implements Validator {
validate(control: AbstractControl): {[key: string]: any} | null {
return specialCharValidator()(control);
}
}
Then use it in your template:
<form #signupForm="ngForm" (ngSubmit)="onSubmit(signupForm)">
<div>
<label for="email">Email:</label>
<input id="email" name="email" type="email" [(ngModel)]="user.email"
required email #email="ngModel">
<div *ngIf="email.errors?.required && email.touched">
Email is required
</div>
<div *ngIf="email.errors?.email && email.touched">
Please enter a valid email
</div>
</div>
<div>
<label for="password">Password:</label>
<input id="password" name="password" type="password" [(ngModel)]="user.password"
required minlength="8" appSpecialChar #password="ngModel">
<div *ngIf="password.errors?.required && password.touched">
Password is required
</div>
<div *ngIf="password.errors?.minlength && password.touched">
Password must be at least 8 characters
</div>
<div *ngIf="password.errors?.specialChar && password.touched">
Password must include at least one special character
</div>
</div>
<button type="submit" [disabled]="signupForm.invalid">Sign Up</button>
</form>
Creating a Validator with Parameters
Sometimes you need validators that can be configured. Here's a custom validator that checks if a control value is within a specified range:
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export function rangeValidator(min: number, max: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (control.value === null || control.value === undefined) {
return null; // Don't validate empty values
}
const value = parseFloat(control.value);
if (isNaN(value)) {
return { notANumber: true };
}
if (value < min) {
return { rangeError: { actualValue: value, requiredMin: min } };
}
if (value > max) {
return { rangeError: { actualValue: value, requiredMax: max } };
}
return null;
};
}
Usage:
this.productForm = this.fb.group({
name: ['', Validators.required],
price: [0, [Validators.required, rangeValidator(0, 10000)]]
});
Creating Cross-Field Validators
Sometimes you need to validate one field against another, like confirming passwords:
import { AbstractControl, FormGroup, ValidationErrors, ValidatorFn } from '@angular/forms';
export function passwordMatchValidator(): ValidatorFn {
return (formGroup: AbstractControl): ValidationErrors | null => {
const password = formGroup.get('password');
const confirmPassword = formGroup.get('confirmPassword');
if (!password || !confirmPassword) {
return null;
}
if (confirmPassword.errors && !confirmPassword.errors.passwordMismatch) {
// Return if another validator has already found an error
return null;
}
// Check if the values match
if (password.value !== confirmPassword.value) {
confirmPassword.setErrors({ passwordMismatch: true });
return { passwordMismatch: true };
} else {
// Remove the error if previously set
const errors = { ...confirmPassword.errors };
if (errors?.passwordMismatch) {
delete errors.passwordMismatch;
}
confirmPassword.setErrors(Object.keys(errors).length ? errors : null);
return null;
}
};
}
To use this validator, apply it to the entire form group:
this.registrationForm = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
confirmPassword: ['', Validators.required]
}, { validators: passwordMatchValidator() });
Creating Async Validators
Async validators are useful for validations that need to check with a server, like verifying if a username is already taken:
import { Injectable } from '@angular/core';
import { AbstractControl, AsyncValidator, ValidationErrors } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { catchError, map, debounceTime, switchMap } from 'rxjs/operators';
import { UserService } from './user.service';
@Injectable({ providedIn: 'root' })
export class UsernameValidator implements AsyncValidator {
constructor(private userService: UserService) {}
validate(
control: AbstractControl
): Observable<ValidationErrors | null> {
return of(control.value).pipe(
debounceTime(500),
switchMap(username =>
this.userService.checkUsernameExists(username).pipe(
map(exists => (exists ? { usernameExists: true } : null)),
catchError(() => of(null))
)
)
);
}
}
Then use it in your form:
constructor(
private fb: FormBuilder,
private usernameValidator: UsernameValidator
) {
this.registerForm = this.fb.group({
username: ['', {
validators: [Validators.required, Validators.minLength(3)],
asyncValidators: [this.usernameValidator.validate.bind(this.usernameValidator)],
updateOn: 'blur'
}],
// other fields...
});
}
Notice that we set updateOn: 'blur'
to trigger the validation when the user leaves the field, not on every keystroke.
Real-World Example: Form with Custom Validation
Here's a comprehensive example of a registration form with multiple custom validators:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { specialCharValidator } from './validators/special-char.validator';
import { passwordMatchValidator } from './validators/password-match.validator';
import { UsernameValidator } from './validators/username.validator';
@Component({
selector: 'app-registration',
templateUrl: './registration.component.html'
})
export class RegistrationComponent implements OnInit {
registrationForm: FormGroup;
submitted = false;
constructor(
private fb: FormBuilder,
private usernameValidator: UsernameValidator
) {}
ngOnInit() {
this.registrationForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
username: ['', {
validators: [Validators.required, Validators.minLength(4)],
asyncValidators: [this.usernameValidator.validate.bind(this.usernameValidator)],
updateOn: 'blur'
}],
email: ['', [Validators.required, Validators.email]],
passwordGroup: this.fb.group({
password: ['', [
Validators.required,
Validators.minLength(8),
specialCharValidator()
]],
confirmPassword: ['', Validators.required]
}, { validators: passwordMatchValidator() }),
age: [null, [Validators.required, rangeValidator(18, 120)]]
});
}
// Convenience getters for easy access to form fields
get f() { return this.registrationForm.controls; }
get passwordGroup() { return this.registrationForm.get('passwordGroup') as FormGroup; }
get password() { return this.passwordGroup.get('password'); }
get confirmPassword() { return this.passwordGroup.get('confirmPassword'); }
onSubmit() {
this.submitted = true;
if (this.registrationForm.invalid) {
return;
}
// Form is valid - proceed with registration
console.log('Registration successful', this.registrationForm.value);
}
}
HTML template:
<form [formGroup]="registrationForm" (ngSubmit)="onSubmit()">
<div>
<label for="name">Full Name</label>
<input id="name" formControlName="name" type="text">
<div *ngIf="submitted && f['name'].errors">
<div *ngIf="f['name'].errors?.required">Name is required</div>
<div *ngIf="f['name'].errors?.minlength">Name must be at least 2 characters</div>
</div>
</div>
<div>
<label for="username">Username</label>
<input id="username" formControlName="username" type="text">
<div *ngIf="f['username'].pending">Checking availability...</div>
<div *ngIf="submitted && f['username'].errors">
<div *ngIf="f['username'].errors?.required">Username is required</div>
<div *ngIf="f['username'].errors?.minlength">Username must be at least 4 characters</div>
<div *ngIf="f['username'].errors?.usernameExists">This username is already taken</div>
</div>
</div>
<div>
<label for="email">Email</label>
<input id="email" formControlName="email" type="email">
<div *ngIf="submitted && f['email'].errors">
<div *ngIf="f['email'].errors?.required">Email is required</div>
<div *ngIf="f['email'].errors?.email">Enter a valid email address</div>
</div>
</div>
<div formGroupName="passwordGroup">
<div>
<label for="password">Password</label>
<input id="password" formControlName="password" type="password">
<div *ngIf="submitted && password?.errors">
<div *ngIf="password?.errors?.required">Password is required</div>
<div *ngIf="password?.errors?.minlength">Password must be at least 8 characters</div>
<div *ngIf="password?.errors?.specialChar">Password must contain a special character</div>
</div>
</div>
<div>
<label for="confirmPassword">Confirm Password</label>
<input id="confirmPassword" formControlName="confirmPassword" type="password">
<div *ngIf="submitted && confirmPassword?.errors">
<div *ngIf="confirmPassword?.errors?.required">Please confirm your password</div>
<div *ngIf="confirmPassword?.errors?.passwordMismatch">Passwords do not match</div>
</div>
</div>
<div *ngIf="submitted && passwordGroup.errors?.passwordMismatch">
Passwords do not match
</div>
</div>
<div>
<label for="age">Age</label>
<input id="age" formControlName="age" type="number">
<div *ngIf="submitted && f['age'].errors">
<div *ngIf="f['age'].errors?.required">Age is required</div>
<div *ngIf="f['age'].errors?.notANumber">Please enter a valid number</div>
<div *ngIf="f['age'].errors?.rangeError">
Age must be between 18 and 120
</div>
</div>
</div>
<button type="submit">Register</button>
</form>
<div *ngIf="submitted && registrationForm.valid">
Registration successful!
</div>
Best Practices for Custom Validators
- Keep validators simple - Each validator should test one thing
- Make validators reusable - Design them so they can be used across projects
- Provide informative error messages - Return detailed error objects that make sense to users
- Handle null/undefined values - Make sure validators don't crash on empty values
- Test your validators - Unit test each validator to ensure accuracy
- Debounce async validators - Use
debounceTime
to prevent excessive server calls
Summary
Custom validators are a powerful feature in Angular forms that allow you to implement application-specific validation logic beyond what the built-in validators provide. You can create:
- Basic validators for single fields
- Validators with parameters for flexibility
- Cross-field validators for comparing values
- Async validators for server-side verification
By combining these techniques, you can create robust form validation that provides a great user experience while ensuring data integrity.
Exercises
-
Create a custom validator that ensures a password contains at least one lowercase letter, one uppercase letter, one number, and one special character.
-
Implement an async validator that checks if an email address is already registered in your system.
-
Create a cross-field validator that ensures a "start date" is always before an "end date".
-
Develop a custom validator that verifies a credit card number using the Luhn algorithm.
-
Build a form with multiple custom validators and implement user-friendly error messages.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)