Skip to main content

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:

  1. Sync Validators - Execute immediately and return results
  2. 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:

typescript
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:

  1. Takes a form control as input
  2. Tests if it contains a special character using a regular expression
  3. Returns null if valid (contains special character)
  4. Returns an error object { specialChar: true } if invalid

Using Custom Validators in Reactive Forms

To use this validator in a reactive form:

typescript
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:

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:

typescript
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:

html
<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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

html
<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

  1. Keep validators simple - Each validator should test one thing
  2. Make validators reusable - Design them so they can be used across projects
  3. Provide informative error messages - Return detailed error objects that make sense to users
  4. Handle null/undefined values - Make sure validators don't crash on empty values
  5. Test your validators - Unit test each validator to ensure accuracy
  6. 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

  1. Create a custom validator that ensures a password contains at least one lowercase letter, one uppercase letter, one number, and one special character.

  2. Implement an async validator that checks if an email address is already registered in your system.

  3. Create a cross-field validator that ensures a "start date" is always before an "end date".

  4. Develop a custom validator that verifies a credit card number using the Luhn algorithm.

  5. 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! :)