Angular Custom Validator in Reactive Form
January 21, 2024
On this page, we will learn to create custom validators for reactive form. Angular provides ValidatorFn
and AsyncValidatorFn
interfaces to create synchronous and asynchronous custom validators. These interfaces are used to create validator functions that are passed to FormControl
constructor to enable validations. When the form controls are not validated, validator functions return map of error keys that can be used to display error messages.
1. Using ValidatorFn for Sync Validator
ValidatorFn
is a function to create custom synchronous validator. It receives a control and returns validation errors. Find the call signature.
(control: AbstractControl<any, any>): ValidationErrors | null
ValidatorFn
used in our demo application.
customer-name-validator.ts
import { Directive, Input } from '@angular/core'; import { NG_VALIDATORS, Validator, FormControl } from '@angular/forms'; import { ValidatorFn, ValidationErrors, AbstractControl } from '@angular/forms'; export function customerNameValidator(): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const v = control.value; if (v && (v.charAt(0) !== v.charAt(0).toUpperCase())) { return { "customerName": true }; } else { return null; } }; }
import { ValidationErrors } from '@angular/forms'; import { ValidatorFn, AbstractControl } from '@angular/forms'; export function itemOrderValidator(prefixVal: string): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const v = control.value; if (v !== null && v !== '' && !v.startsWith(prefixVal)) { return { "orderNumPrefix": true }; } else if (v !== null && v !== '' && isNaN(v.split("-")[1])) { return { "orderNumPrefix": false, "orderNAN": true }; } else { return null; } }; }
2. Using AsyncValidatorFn for Async Validator
AsyncValidatorFn
is a function to create asynchronous validator. It receives a control and returns Promise
or Observable
of validation errors. Find the call signature.
(control: AbstractControl<any, any>): Promise<ValidationErrors | null> | Observable<ValidationErrors | null>
AsyncValidatorFn
used in our demo application.
existing-order-number-validator.ts
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms'; import { CustomerService } from '../customer-service'; import { Observable, map } from 'rxjs'; export function existingOrderNumberValidator(customerService: CustomerService): AsyncValidatorFn { return (control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> => { return customerService.getDetailsByOrderNumber(control.value).pipe(map( (orders: any[]) => { return (orders && orders.length > 0) ? { "orderNumExists": true } : null; } )); }; }
3. Using Validators in FormControl
Find the construct signature ofFormControl
FormControl(value: T | FormControlState<T>, opts: FormControlOptions & { nonNullable: true; })
FormControlOptions: Object with validators, asyncValidators and updateOn keys.
Find the code snippet to use validators in
FormControl
.
new FormControl({ value: '', disabled: false }, { validators: [Validators.required, itemOrderValidator(this.orderNumPrefixVal)], asyncValidators: [existingOrderNumberValidator(this.customerService)] });
4. Using Validators in FormGroup
Find the code snippet to use custom validators withFormGroup
.
customerForm = this.formBuilder.group({ orderNum: [{ value: '', disabled: false }, [Validators.required, itemOrderValidator(this.orderNumPrefixVal)], [existingOrderNumberValidator(this.customerService)] ], ------ });
<div *ngIf="orderNum?.hasError('orderNumPrefix')" class="error"> Start with {{orderNumPrefixVal}} </div> <div *ngIf="orderNum?.hasError('orderNAN')" class="error"> Not a number. </div> <div *ngIf="orderNum?.hasError('orderNumExists')" class="error"> Order number already exists. </div>
5. Complete Example
In my demo application, I have created two sync custom validators ascustomerNameValidator()
and itemOrderValidator()
and one async validator as existingOrderNumberValidator()
.
Find the complete code.
customer-validators.ts
import { AbstractControl, AsyncValidatorFn, ValidationErrors, ValidatorFn } from "@angular/forms"; import { CustomerService } from "./customer-service"; import { Observable, map } from "rxjs"; export class CustomerValidators { static customerNameValidator(): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const v = control.value; if (v && (v.charAt(0) !== v.charAt(0).toUpperCase())) { return { "customerName": true }; } else { return null; } }; } static itemOrderValidator(prefixVal: string): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const v = control.value; if (v !== null && v !== '' && !v.startsWith(prefixVal)) { return { "orderNumPrefix": true }; } else if (v !== null && v !== '' && isNaN(v.split("-")[1])) { return { "orderNumPrefix": false, "orderNAN": true }; } else { return null; } }; } static existingOrderNumberValidator(customerService: CustomerService): AsyncValidatorFn { return (control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> => { return customerService.getDetailsByOrderNumber(control.value).pipe(map( (orders: any[]) => { return (orders && orders.length > 0) ? { "orderNumExists": true } : null; } )); }; } }
import { Component } from '@angular/core'; import { FormBuilder, Validators, ReactiveFormsModule, FormControl } from '@angular/forms'; import { CustomerService } from './customer-service'; import { CommonModule } from '@angular/common'; import { CustomerValidators } from './customer-validators'; @Component({ selector: 'app-reactive', standalone: true, imports: [CommonModule, ReactiveFormsModule], templateUrl: './customer.component.html' }) export class CustomerComponent { orderNumPrefixVal = "CO-"; constructor(private formBuilder: FormBuilder, private customerService: CustomerService) { } customerForm = this.formBuilder.group({ customerName: [{ value: '', disabled: false }, { validators: [Validators.required, CustomerValidators.customerNameValidator()] }], orderNum: [{ value: '', disabled: false }, [Validators.required, CustomerValidators.itemOrderValidator(this.orderNumPrefixVal)], [CustomerValidators.existingOrderNumberValidator(this.customerService)] ] }); get customerName() { return this.customerForm.get('customerName') as FormControl; } get orderNum() { return this.customerForm.get('orderNum') as FormControl; } onFormSubmit() { if (this.customerForm.valid) { this.customerService.createOrder(this.customerForm.value); this.customerForm.reset(); } } }
<h3>Customer Form</h3> <form [formGroup]="customerForm" (ngSubmit)="onFormSubmit()"> <table> <tr> <td>Customer Name: </td> <td> <input formControlName="customerName"> <div *ngIf="customerName?.hasError('required')" class="error"> Enter customer name. </div> <div *ngIf="customerName?.hasError('customerName')" class="error"> Enter first letter in uppercase. </div> </td> </tr> <tr> <td>Order Number: </td> <td> <input formControlName="orderNum"> <div *ngIf="orderNum?.hasError('required')" class="error"> Enter order number. </div> <div *ngIf="orderNum?.hasError('orderNumPrefix')" class="error"> Start with {{orderNumPrefixVal}} </div> <div *ngIf="orderNum?.hasError('orderNAN')" class="error"> Not a number. </div> <div *ngIf="orderNum?.hasError('orderNumExists')" class="error"> Order number already exists. </div> </td> </tr> <tr> <td colspan="2"> <button>Submit</button> </td> </tr> </table> </form>
import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class CustomerService { createOrder(order: any) { console.log(JSON.stringify(order)); } getDetailsByOrderNumber(order: string): Observable<any[]> { if (order?.split("-")[1] === "12345") { return of([{ orderNum: "12345" }]); } else { return of([]); } } }