Angular Custom Validator in Reactive Form

By Arvind Rai, 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 
Find the custom validators with 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;
    }
  };
}
item-order-number-validator.ts
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> 
Find the custom validator with 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 of FormControl
FormControl(value: T | FormControlState<T>, opts: FormControlOptions & { nonNullable: true; }) 
FormControlState : Object with value and disabled key.
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)]
}); 
The value for validators key is array of built-in sync validators and custom sync validators. The value for asyncValidators is array of async validators.

4. Using Validators in FormGroup

Find the code snippet to use custom validators with FormGroup.
customerForm = this.formBuilder.group({
  orderNum: [{
    value: '',
    disabled: false
  }, 
     [Validators.required, itemOrderValidator(this.orderNumPrefixVal)],
     [existingOrderNumberValidator(this.customerService)]
  ],
  ------
}); 
Find the code to access error messages.
<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 as customerNameValidator() 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;
                }
            ));
        };
    }
} 
customer.component.ts
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();
    }
  }
} 
customer.component.html
<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> 
customer-service.ts
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([]);
    }
  }
} 
Find the print-screen of the output.
Angular Custom Validator in Reactive Form

6. References

7. Download Source Code

POSTED BY
ARVIND RAI
ARVIND RAI







©2024 concretepage.com | Privacy Policy | Contact Us