Custom Validators in Angular Forms

Validation is a critical part of any application to ensure data integrity and provide meaningful feedback to users. Angular provides several built-in validators like required, minLength, maxLength, email, and pattern. However, many real-world scenarios require custom validation logic that goes beyond the built-in validators.

In this article, we will explore custom validators in Angular, covering their creation, usage in reactive and template-driven forms, advanced techniques, and best practices.

Table of Contents

  1. Introduction to Custom Validators
  2. Why Use Custom Validators
  3. Built-in Validators vs Custom Validators
  4. Creating a Simple Custom Validator
  5. Using Custom Validators in Reactive Forms
  6. Using Custom Validators in Template-Driven Forms
  7. Parameterized Custom Validators
  8. Asynchronous Custom Validators
  9. Nested Form Validation with Custom Validators
  10. FormGroup and Cross-Field Validation
  11. Error Display for Custom Validators
  12. Combining Multiple Validators
  13. Real-World Example: Username Validation
  14. Real-World Example: Password Strength Validator
  15. Common Mistakes and Pitfalls
  16. Best Practices for Custom Validators
  17. Conclusion

1. Introduction to Custom Validators

A custom validator in Angular is a function that checks the value of a form control and returns:

  • null if the control is valid
  • An error object if the control is invalid

Custom validators allow developers to implement complex validation logic that built-in validators cannot handle.


2. Why Use Custom Validators

While Angular’s built-in validators handle common scenarios, there are many cases that require custom rules:

  • Restricting usernames to alphanumeric characters only
  • Checking that passwords contain uppercase, lowercase, numbers, and symbols
  • Validating that two fields (like password and confirm password) match
  • Ensuring a date is within a specific range

Custom validators give developers the flexibility to implement business-specific rules for their forms.


3. Built-in Validators vs Custom Validators

Built-in ValidatorsCustom Validators
requiredChecks if a control is empty
minLength / maxLengthChecks string length
patternMatches regex
emailChecks email format

Custom validators complement built-in validators and are often used together.


4. Creating a Simple Custom Validator

A custom validator is a function that takes a FormControl as input and returns an error object or null:

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

export function noSpecialChars(control: FormControl): ValidationErrors | null {
  const regex = /^[a-zA-Z0-9]+$/;
  return regex.test(control.value) ? null : { invalidChars: true };
}
  • regex.test(control.value) checks if the value contains only alphanumeric characters.
  • { invalidChars: true } is returned if validation fails.
  • null is returned if validation passes.

5. Using Custom Validators in Reactive Forms

Custom validators can be added to FormControl or FormGroup:

import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { noSpecialChars } from './validators';

@Component({
  selector: 'app-user-form',
  template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
  <input formControlName="username" placeholder="Username" />
  <div *ngIf="userForm.get('username')?.errors?.invalidChars">
    Username contains invalid characters.
  </div>
  <button type="submit">Submit</button>
</form>
` }) export class UserFormComponent { userForm = new FormGroup({
username: new FormControl('', [Validators.required, noSpecialChars])
}); onSubmit() {
if (this.userForm.valid) {
  console.log('Form Submitted', this.userForm.value);
}
} }
  • Combine custom validators with built-in validators like Validators.required.
  • Error messages can be displayed based on the custom validation result.

6. Using Custom Validators in Template-Driven Forms

Template-driven forms can also use custom validators via NG_VALIDATORS:

import { Directive } from '@angular/core';
import { NG_VALIDATORS, Validator, AbstractControl, ValidationErrors } from '@angular/forms';

@Directive({
  selector: '[appNoSpecialChars]',
  providers: [{ provide: NG_VALIDATORS, useExisting: NoSpecialCharsDirective, multi: true }]
})
export class NoSpecialCharsDirective implements Validator {
  validate(control: AbstractControl): ValidationErrors | null {
const regex = /^[a-zA-Z0-9]+$/;
return regex.test(control.value) ? null : { invalidChars: true };
} }

Usage in template:

<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
  <input name="username" ngModel appNoSpecialChars required />
  <div *ngIf="userForm.controls.username?.errors?.invalidChars">
Username contains invalid characters.
</div> <button type="submit">Submit</button> </form>
  • Directives allow template-driven forms to use the same validation logic as reactive forms.

7. Parameterized Custom Validators

Validators can accept parameters for dynamic behavior:

export function minLengthCustom(min: number) {
  return (control: FormControl): ValidationErrors | null => {
return control.value.length &gt;= min ? null : { minLengthCustom: { requiredLength: min } };
}; } // Usage username: new FormControl('', [minLengthCustom(5)])
  • Allows reusing the validator for different fields with different rules.
  • Error object can include parameter details for dynamic error messages.

8. Asynchronous Custom Validators

Some validations require server-side checks:

import { AbstractControl } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { delay, map } from 'rxjs/operators';

export function asyncUsernameValidator(control: AbstractControl): Observable<ValidationErrors | null> {
  const forbiddenUsernames = ['admin', 'superuser'];
  return of(forbiddenUsernames.includes(control.value)).pipe(
delay(1000),
map(isForbidden =&gt; (isForbidden ? { forbiddenName: true } : null))
); }

Usage:

username: new FormControl('', Validators.required, asyncUsernameValidator)
  • Asynchronous validators return an observable or promise.
  • Useful for server-side validation like checking existing usernames or emails.

9. Nested Form Validation with Custom Validators

Custom validators can validate FormGroups:

import { FormGroup } from '@angular/forms';

export function passwordsMatch(group: FormGroup): ValidationErrors | null {
  const password = group.get('password')?.value;
  const confirm = group.get('confirmPassword')?.value;
  return password === confirm ? null : { passwordsMismatch: true };
}

// Usage
new FormGroup({
  password: new FormControl('', Validators.required),
  confirmPassword: new FormControl('', Validators.required)
}, { validators: passwordsMatch });
  • Ensures two fields match.
  • Works at the group level rather than individual controls.

10. FormGroup and Cross-Field Validation

Cross-field validation is essential for:

  • Password confirmation
  • Start date and end date validation
  • Dependent fields

Example: Validating date range:

export function dateRangeValidator(group: FormGroup): ValidationErrors | null {
  const start = group.get('startDate')?.value;
  const end = group.get('endDate')?.value;
  return start <= end ? null : { invalidDateRange: true };
}

11. Error Display for Custom Validators

Show error messages dynamically:

<div *ngIf="userForm.get('username')?.errors?.invalidChars">
  Username contains invalid characters.
</div>

<div *ngIf="userForm.errors?.passwordsMismatch">
  Passwords do not match.
</div>
  • Use errors property of FormControl or FormGroup.
  • Combine with Angular structural directives like *ngIf.

12. Combining Multiple Validators

You can combine multiple built-in and custom validators:

new FormControl('', [
  Validators.required,
  Validators.minLength(6),
  noSpecialChars
])
  • Validators are evaluated in order.
  • If one fails, others are still executed to accumulate errors.

13. Real-World Example: Username Validation

this.userForm = new FormGroup({
  username: new FormControl('', [Validators.required, noSpecialChars])
});
  • Prevents users from entering invalid characters.
  • Ensures usernames are alphanumeric.
  • Provides immediate feedback on invalid input.

14. Real-World Example: Password Strength Validator

export function strongPassword(control: FormControl): ValidationErrors | null {
  const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
  return regex.test(control.value) ? null : { weakPassword: true };
}

this.userForm = new FormGroup({
  password: new FormControl('', [Validators.required, strongPassword])
});
  • Enforces uppercase, lowercase, number, and minimum length.
  • Strengthens security for user accounts.

15. Common Mistakes and Pitfalls

  • Returning false instead of null for valid input.
  • Forgetting to include validators in the form control array.
  • Not handling asynchronous validators properly.
  • Overcomplicating simple validation that could be handled with built-ins.
  • Not displaying error messages correctly in the template.

16. Best Practices for Custom Validators

  1. Always return null for valid input.
  2. Include clear error object keys for template use.
  3. Use parameterized validators for reusability.
  4. Combine built-in and custom validators logically.
  5. Use asynchronous validators for server-side checks.
  6. Keep validators pure and side-effect-free.
  7. Test custom validators thoroughly.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *