Asynchronous Validation in Angular's Reactive Forms Control

I've been playing around with Angular for some time now. When it comes to building forms, I tend to go with reactive forms. In short, while using reactive forms; you create a form model consists of different form controls directly in your component class. Once you are done, you bind them (form controls) to your native form control elements defined in the component's view. But that is all about reactive forms and the official documentation talks at length about it here,

https://angular.io/guide/reactive-forms

I'm assuming that you already have an go to idea on reactive forms. In this post I'm going to talk about adding a custom asynchronous validator to a form control. So the example I'm using for the sake of this demonstration is pretty simple and kind of a rip-off of the Tour of Heroes tutorial sample available in the official site. Instead of managing heroes, here we are managing weather forecasts of different hours of specific dates. The application doesn't let you add new forecast, but it let's you modify existing forecast's data (i.e. date, temperature, summary etc.). Here is the final look of the application; once a date time is selected, you can start modifying it through the form:

The following is the structure of the form model that is specified in the component's code:

forecast-detail.component.ts
forecastForm: FormGroup;

createForm() {
    this.forecastForm = this.fb.group({
        dateFormatted: new FormControl('', [Validators.required]),
        temperatureC: new FormControl('', [Validators.required]),
        temperatureF: '',
        summary: ''
    });
}

If you noticed carefully, we already have a validation constraint on the dateFormatted i.e. the field can't be null of empty (Validators.required). The other validation constraint we are looking for on that form control is, we should have an unique date time for every forecast. In edit view, the date time can be changed but it can't be changed to a date time that is already available in the list. The Validators.required is a built-in synchronous validator provided by Angular. You specified the synchronous validatiors in an array and pass it as the second parameter to the FormControl. Similarly, you specified your asynchronous validators in another array and pass it as the third parameter to the control. The reason behind specifying two different arrays instead of one is that Angular doesn't fire the asynchronous validators until every synchronous validation is satisfied. Since an asynchronous validation is resourceful (i.e. you would call some kind of server to do the validation), you shouldn't always initiate the validation process on every changes made to the form control.

If you don't know how to make a custom synchronous validators, go though this post:

https://angular.io/guide/form-validation#custom-validators

The idea is very similar to the type of function we define for a custom synchronous validator. In case of asynchronous validators, we define a function which returns a function that is a type of AsyncValidatorFn. Following is the structure of the AsyncValidatorFn interface:

interface AsyncValidatorFn { 
  (c: AbstractControl): Promise<ValidationErrors|null>|Observable<ValidationErrors|null>
}

The AsyncValidatorFn then returns a promise or observable. We process the validation result and decide whether it satisfies a specific validation constraint or not by resolving the promise or the observable object. Following is the validator function that checks the uniqueness of the date time value available in the control.value:

existingDateValidator(): AsyncValidatorFn {
    return (control: AbstractControl): Promise<{ [key: string]: any } | null> | Observable<{ [key: string]: any } | null> => {
        let forecastService = new ForecastService();

        if (isEmptyInputValue(control.value)) {
            return Observable.of(null);
        }
        else {
            return forecastService.getForecastByDate(control.value).map(forecast => {
                return forecast ? { 'existingDate': { value: control.value } } : null;
            });
        }
    };
}

Here, AbstractControl is the base class from which every FormControl inherits from. Also, I'm going for observable here but you can return a promise if you want.

Notice the piece of code written in the else statement. Once the subscription is done to the observable that is returned from the getForecastByDate(dateFormatted: string) function, we map the result and check whether we have an existing forecast or not. If yes, we return a validation key (existingDate) with the value set to the form control's value { value: control.value }. And if not, we return a null object. The getForecastByDate() function is available in the forecast.service.ts file:

forecast.service.ts
import { Injectable } from '@angular/core';

import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import 'rxjs/add/operator/delay';

import { WeatherForecast, weatherForecasts } from './forecast-models';

@Injectable()
export class ForecastService {

    delayMs = 500;

    getForecastByDate(dateFormatted: string): Observable<WeatherForecast | undefined> {
        const existingForcast = weatherForecasts.find(w => w.dateFormatted === dateFormatted);
        return of(existingForcast).delay(this.delayMs); // simulate latency with delay
    }
}

Now, that's cool! The last piece of the puzzle is that we now have to attach the validator with the form control as followings:

ngOnChanges(): void {
    this.forecastForm = this.fb.group({
        dateFormatted: new FormControl(this.forecast.dateFormatted, [Validators.required], [this.forecastValidators.existingDateValidator()]),
        temperatureC: new FormControl(this.forecast.temperatureC, [Validators.required]),
        temperatureF: this.forecast.temperatureF,
        summary: this.forecast.summary
    });
}

Remember that we have the validation result returned with a key named existingDate. So, we can check whether the value of the key is null or not in a conditional *ngIf directive and show relevant message if it isn't. We check for the existingDate key in the errors object of the form control i.e. dateFormatted.errors.existingDate. Following is the component's markup as plain html with some basic native form control elements:

forecast-detail.component.html
<form [formGroup]="forecastForm" (ngSubmit)="onSubmit()" novalidate>
    <div class="form-group">
        <label for="dateFormatted">Date (Formatted)</label>
        <input type="datetime-local" id="dateFormatted" class="form-control"
               formControlName="dateFormatted" required>
        <div *ngIf="dateFormatted.invalid && (dateFormatted.dirty || dateFormatted.touched)">
            <span *ngIf="dateFormatted.errors.required">
                <i class="text-danger">Date is required.</i>
            </span>
            <span *ngIf="dateFormatted.errors.minlength">
                <i class="text-danger">Date must be at least 10 characters long.</i>
            </span>
            <span *ngIf="dateFormatted.errors.existingDate"> 
                <i class="text-danger">Date already exists.</i>
            </span>
        </div>
    </div>
    <div class="form-group">
        <label for="temperatureC">Temperature in Celcius</label>
        <input id="temperatureC" class="form-control" formControlName="temperatureC">
        <div *ngIf="temperatureC.invalid && (temperatureC.dirty || temperatureC.touched)">
            <span *ngIf="temperatureC.errors.required">
                <i class="text-danger">Temperature in celcius is required.</i>
            </span>
        </div>
    </div>
    <div class="form-group">
        <label for="temperatureF">Temperature in Farenheit</label>
        <input id="temperatureF" class="form-control" formControlName="temperatureF">
    </div>
    <div class="form-group">
        <label for="summary">Summary</label>
        <select id="summary" class="form-control" formControlName="summary">
            <option *ngFor="let summary of summaries" [value]="summary">{{summary}}</option>
        </select>
    </div>
    <button type="submit"
            [disabled]="forecastForm.pristine || forecastForm.invalid" class="btn btn-success">
        Save
    </button>
</form>

And that's all about it. One thing you can do to further optimize your validation function. At this moment the validation logic will fire every time the component is changed. But you can minimize the payload by checking whether the current control value is same as the initial value set to it i.e. the value of this.forcast.dateFormatted.

forcast.detail.component.ts
dateFormatted: new FormControl(this.forecast.dateFormatted, [Validators.required, Validators.minLength(10)], [this.forecastValidators.existingDateValidator(this.forecast.dateFormatted)])
forcast.service.ts
if (isEmptyInputValue(control.value)) {
    return Observable.of(null);
} else if (control.value === initialDate) {
    return Observable.of(null);
}

Another thing you can do is, you can make a validation service and put all your validator functions in it like the following:

forecast.validators.ts
import { Injectable } from '@angular/core';
import { AbstractControl, AsyncValidatorFn, ValidatorFn, Validators } from '@angular/forms';
import { Observable } from 'rxjs'
import { ForecastService } from './forecast.service'
import 'rxjs/add/operator/map';

function isEmptyInputValue(value: any): boolean {
    // we don't check for string here so it also works with arrays
    return value == null || value.length === 0;
}

@Injectable()
export class ForcastValidators {
    constructor(private forecastService: ForecastService) {

    }

    existingDateValidator(initialDate: string = ""): AsyncValidatorFn {
        return (control: AbstractControl): Promise<{ [key: string]: any } | null> | Observable<{ [key: string]: any } | null> => {
            if (isEmptyInputValue(control.value)) {
                return Observable.of(null);
            } else if (control.value === initialDate) {
                return Observable.of(null);
            }
            else {
                return this.forecastService.getForecastByDate(control.value).map(forecast => {
                    return forecast ? { 'existingDate': { value: control.value } } : null;
                });
            }
        };
    }
}

Notice that, instead of newing up a instance of the ForecastService directly in the validator function; now we are using the constructor of ForcastValidators to do that for us.

Once you are done, don't forget to add the ForcastValidators service in the providers list of the main module of your application:

app.module.shared.ts
@NgModule({
    providers: [ForecastService, ForcastValidators] 
})

Run the application now and go to edit mode of a forecast. Try changing the date time value to value that is already available in the forecast list. Tada! you will get an error message, saying that your validation has failed miserably:

That is how easy to make a custom asynchronous validators. I hope you like the post. Share it and have a nice day ☺.

The sample project is available in this repository:

https://github.com/fiyazbinhasan/ReactiveAsyncValidation