DEV Community

Michael Musatov
Michael Musatov

Posted on • Updated on

Angular Forms Validation: Part I - Single control validation

Angular offers powerful tools for managing forms. In this series of posts, I will discuss various Reactive Forms Validation examples, ranging from the simplest to more advanced ones.

In the first part of the series, we will focus on single form control validation. You can apply zero or any number of synchronous and asynchronous validators to a single control.

Let's start from the simplest possible example

A FormControl without validation, along with the Angular component markup to display this FormControl:

export class SingleControlComponent {
  readonly emailControl = new FormControl();
}
Enter fullscreen mode Exit fullscreen mode
<label for="email">Email</label>
<input name="email" type="text" [formControl]="emailControl">
Enter fullscreen mode Exit fullscreen mode

Running this example will yield the following results in the browser:
Sample control

Adding synchronous validation to the FormControl

We are going to use validators provided by the Angular framework. Validators.required - requires to have a non-empty value. Validators.email - checking email against the pattern which is based on the definition of a valid email address in the WHATWG HTML specification with some enhancements to incorporate more RFC rules.

export class SingleControlComponent {
  readonly emailControl = new FormControl(null, [Validators.required, Validators.email]);
}
Enter fullscreen mode Exit fullscreen mode

As the next step, we need to handle validation results in the control markup. The FormControl API is helpful here. The 'hasError' method checks for the presence of an error code ('required', 'email', etc.) in the control's errors collection.

<label for="email">Email</label>
<input name="email" type="text" [formControl]="emailControl">
<div class="errors">
  <span *ngIf="emailControl.hasError('required')">Email is Required</span> 
  <span *ngIf="emailControl.hasError('email')">Email is mailformed</span>
</div>
Enter fullscreen mode Exit fullscreen mode

After making these changes, our control should work as shown in the following recording:
Alt Text

Implement asynchronous validator.

The next step will be implementing some asynchronous validation. Let's create an async validator that simulates checking for email presence on the server.

function emailMustNotBeUsed(control: AbstractControl): Observable<ValidationErrors | null> {
  return control.value === 'used@email.com' 
      ? of({'email-is-used': 'Email was used'}).pipe(delay(2000))
      : of(null).pipe(delay(2000));   
}
Enter fullscreen mode Exit fullscreen mode

We're delaying the execution result to simulate a call to a remote resource. Now, we will add the async validator to the FormControl.

export class SingleControlComponent {
  readonly emailControl = new FormControl(null, 
    // Sync validators
    [Validators.required, Validators.email],
    // Async validator
    emailMustNotBeUsed);
}
Enter fullscreen mode Exit fullscreen mode

Handling validator results in the component markup by adding the following element:

...
<span *ngIf="emailControl.hasError('email-is-used')">{{emailControl.getError('email-is-used')}}</span> 
...
Enter fullscreen mode Exit fullscreen mode

Note: Error message from the validator is shown at the UI by using getError method. It can be helpfull if async validator return specific error message (received from the server for example).

The results of all our changes will be as shown in the recording below:
The Results

Validators execution order

The order of execution is guaranteed. First, all synchronous validators are executed in the order of declaration. Once synchronous validators return results and there are no errors, asynchronous validators are initiated. Asynchronous validators are initiated in the order of declaration, but the order of execution results is not guaranteed. Let's take a look at the code.

export class SingleControlComponent {
  readonly emailControl = new FormControl(null, 
    // Sync validators
    [Validators.required, Validators.email],
    // Async validators
    [emailMustNotBeUsed, emailIsForbidden]);
}
Enter fullscreen mode Exit fullscreen mode

The implementation of asynchronous validators used for this form control is presented below:

function emailMustNotBeUsed(control: AbstractControl): Observable<ValidationErrors | null> {
  console.log('First async validator stars executing');
  const resutl$ = control.value === 'used@email.com' 
      ? of({'email-is-used': 'Email was used'}).pipe(delay(2000))
      : of(null).pipe(delay(2000));
  return resutl$.pipe(finalize(() => console.log('First async validator completed')));    
}

function emailIsForbidden(control: AbstractControl): Observable<ValidationErrors | null> {
  console.log('Second async validator stars executing');
  const resutl$ = control.value === 'forbidden@email.com' 
      ? of({'email-is-forbidden': 'Email was forbidden'}).pipe(delay(1500))
      : of(null).pipe(delay(1500));

  return resutl$.pipe(finalize(() => console.log('Second async validator completed')));    
}
Enter fullscreen mode Exit fullscreen mode

The console output of running this sample will be exactly as follows:

First async validator stars executing
Second async validator stars executing
Second async validator completed
First async validator completed

The first two lines will always be printed in the same order, regardless of the internal async validator implementation. The order of the other two lines depends on the async validation delay.

Conclusion

Thank you for reading. I hope this information was helpful in some way. All code samples can be found on Github.

There are two other articles available on the topic:
Angular forms validation. Part II. FormGroup validation.
Angular forms validation. Part III. Async Validators gotchas.

Top comments (0)