Angular Forms Validation: Part III - Async Validators gotchas
Tags: angular, validation, reactiveforms, forms
Published: 2020-03-26 06:25:11

I am about to continue sharing my expertise on Angular's Forms Validation. In this forthcoming article, we will examine common pitfalls and challenges that arise when utilizing async validators.

### I. Absence of Error Message in UI after Async Validation Completion
This is a common issue that many developers encounter when working with async validators. Interestingly, it has been an open [issue](https://github.com/angular/angular/issues/12378) on the Angular GitHub repository for over three and a half years. This problem is not limited to just the OnPush change detection strategy but also affects the Default strategy. Let's explore a scenario where you are likely to encounter this problem.

To demonstrate the issue, let's declare a simple component with the Default change detection strategy:
```typescript
@Component({...})
export class SingleControlComponent {
  readonly usernameControl = new FormControl(null, 
    { asyncValidators: userDoesNotExist, updateOn: 'blur' });
}

function userDoesNotExist(control: FormControl): Observable<ValidationErrors | null> {
  console.log('Starting async validation...')
  const result$ = control.value !== 'test'
    // 'delay' is used to simulate server call
    ? of({'user-does-not-exist': true}).pipe(delay(1000))
    : of(null).pipe(delay(1000));

  return result$.pipe(tap(result => console.log(result)));
}
```
The component consists of a single `FormControl` with an Async Validator function that emulates checking the availability of a username. The template markup for the component is as follows:
```html
<label for="username">Username</label>
<input name="username" type="text" [formControl]="usernameControl">
<div class="errors">
  <span *ngIf="usernameControl.hasError('user-does-not-exist')">Username in not used</span> 
</div>
```
After executing this code, the following behavior can be observed:
![UI updated after focus change](https://dev-to-uploads.s3.amazonaws.com/i/usmemkgddgo5x7cwx46n.gif)
What is going on here? Let's split it into stages

1. The user enters a valid value 'test', in the field and then moves the focus away from it. (_NOTE: The `updateOn: 'blur'` configuration is used to prevent multiple validation calls._) Upon doing so, the console displays messages indicating the start and completion of the validation process without any errors. So far, everything is working as expected.
2. The user updates the value to an invalid one, such as 'test1'. Once again, messages about the start and completion of validation appear in the console. Since the validation fails, the console displays `{user-does-not-exist: true}`. However, at the UI level, **no errors are displayed**.
3. The user interacts with the field by focusing on it and then moving the focus away. This triggers a change detection, resulting in the UI being updated. (_NOTE: In the case of the OnPush change detection strategy, change detection will not be triggered by this interaction, and the UI will remain in an outdated state. In such cases, manual triggering of change detection is necessary_.)

Indeed, to address this issue, we need to explicitly inform Angular to run change detection when we have the validation result. Adding the following code snippet to our component will achieve precisely that:
```typescript
...
constructor(cd: ChangeDetectorRef) {
  this.usernameControl.statusChanges.subscribe(() => cd.markForCheck());
}
...
```
Now, with the addition of the code snippet, the behavior aligns with our expectations, and the issue has been resolved:
![UI updated automatically](https://dev-to-uploads.s3.amazonaws.com/i/olmlii06rd28f68nwfjw.gif)

### II. Async Validators start simultaneously on parent and child

There are situations where we need to asynchronously validate not just the value of a single `FormControl`, but the entire `FormGroup`. Angular provides this functionality, but unfortunately, not everything goes as expected. Below, we will demonstrate two common problems that you are likely to encounter.

#### II.A Status of the parent not updated as expected

In some cases, when performing async validation, we may want to show a progress indication or lock form controls in the UI to inform the user. Angular's `AbstractFormControl` (and his descendants `FormControl` and `FormGroup`) provides a useful observable property for such scenarios: `statusChanges`. The value of this property becomes `PENDING` when an async validation is in progress. Let's take a look at a demo for this scenario.

Here we have a simple component with a `FormGroup` and an async validator applied to the group.
```typescript
...
Component({...})
export class ParentChildStatusComponent {
  constructor() { 
    this.form = new FormGroup({
      'username': new FormControl(null, [Validators.required]),
      'password': new FormControl(null, [])
    }, { updateOn: 'blur', asyncValidators: this.passwordAlreadyUsed.bind(this) });
  }

  private passwordAlreadyUsed(control: FormGroup): Observable<ValidationErrors | null> { ... }
}

```
_NOTE: Some code related to displaying the validation progress has been omitted for simplicity._

The markup for this component is as follows:
```html
<div class="form" [formGroup]="form">
  <label for="username">Username</label>
  <input name="username" type="text" formControlName="username">
  <div class="errors">
    <span *ngIf="form.get('username').hasError('required')">Username is Required</span> 
  </div>
  <label for="password">Password</label>
  <input name="password" type="text" formControlName="password">
  <div class="progress validator">USERNAME ASYNC VALIDATOR STATUS: {{usernameValidatorStatus$ | async}}</div>
  <div class="progress validator">PASSWORD ASYNC VALIDATOR STATUS: {{passwordValidatorStatus$ | async}}</div>
  <div class="progress">FORM STATUS IS {{(form.statusChanges | async) || form.status}}</div>
</div>
```
Let's take a look at the output during the execution of this demo:
![Form editing and validation](https://dev-to-uploads.s3.amazonaws.com/i/9wtx8y9d7zv0pbbu5zn8.gif)
1. The form has a single synchronous validator (`Validators.required`) applied to the 'username' FormControl. Initially, the form is in the 'INVALID' state and no asynchronous validators are running or executed.
2. The user enters a value in the username field and then moves the focus away from it (the form has the `updateOn: 'blur'` option set). After that, the synchronous validator is executed, and the result is valid. Then, the asynchronous validator of the FormGroup starts executing, and the FormGroup's status becomes `PENDING` . The validator is in the  `STARTED` state.
3. Once the validation is completed, the `FormGroup`'s status becomes `VALID`. Everything is progressing smoothly and exactly as expected.

Now, let's add an additional asynchronous validator to the 'username' `FormControl` and observe how it affects the form's behavior.
```typescript
...
Component({...})
export class ParentChildStatusComponent {
  constructor() { 
    this.form = new FormGroup({
      'username': new FormControl(null, [Validators.required], [this.userDoesNotExist.bind(this)]),
      'password': new FormControl(null, [])
    }, { updateOn: 'blur', asyncValidators: this.passwordAlreadyUsed.bind(this) });
  }

  private userDoesNotExist(control: FormControl): Observable<ValidationErrors | null> { ... }

  private passwordAlreadyUsed(control: FormGroup): Observable<ValidationErrors | null> { ... }
}
```
Let's examine the user interface of the form after incorporating this small enhancement in form validation.
![Form editing and validation](https://dev-to-uploads.s3.amazonaws.com/i/ui02evbio0kz60aniiaw.gif)
Although the user interface appears similar, we have encountered a problem in the form validation.

1. Initially, the form is in an `INVALID` state with no asynchronous validators running or executed.
2. When the user enters a value in the username field and moves the focus away, the synchronous validator completes. After that, both the asynchronous validator of the 'username' `FormControl` **and** the asynchronous validator of the `FormGroup` start executing. As a result, the `FormGroup` status becomes `PENDING` and both validators are indicated as `STARTED` in the console. So far so good.
3. However, after the validation for the 'username' `FormControl` is completed and the `FormGroup` status changes to `VALID`, we encounter an issue. **The asynchronous validator of the `FormGroup` is still running**, which leads to an incorrect status for the form. Consequently, we cannot rely on the form status for locking the user interface or displaying progress indications. This is a disappointing discovery.

Therefore, the current status of the form is incorrect, and we cannot depend on it for locking the user interface or displaying progress indications. This limitation is indeed disappointing, as it hinders our ability to provide accurate feedback to the user.

#### II.B A synchronous validator failure **does not prevent** async validator from triggering

Let's take a look at another example of async validator problems. This one is going to be the last one, but definitely not the least. Suppose we want to make the password field required in our password setting form. We can achieve this by applying the `Validators.required` validator to the 'password' `FormContorl`.
```typescript
...
Component({...})
export class ParentChildStatusComponent {
  constructor() { 
    this.form = new FormGroup({
      'username': new FormControl(null, [Validators.required], [this.userDoesNotExist.bind(this)]),
      'password': new FormControl(null, [Validators.required])
    }, { updateOn: 'blur', asyncValidators: this.passwordAlreadyUsed.bind(this) });
  }

  private userDoesNotExist(control: FormControl): Observable<ValidationErrors | null> { ... }

  private passwordAlreadyUsed(control: FormGroup): Observable<ValidationErrors | null> { ... }
}
```
And now we expect the `passwordAlreadyUsed` async validator to be executed only after all sync validators have passed. This behavior is explicitly mentioned in Angular's [documentation](https://angular.io/guide/form-validation#the-basics):
> It is important to note that the asynchronous validation happens after the synchronous validation, and is performed only if the synchronous validation is successful. This check allows forms to avoid potentially expensive async validation processes such as an HTTP request if more basic validation methods fail.

However, the form validation behaves differently in this case.
![Unexpected async validation](https://dev-to-uploads.s3.amazonaws.com/i/8s7lk4g5aa6lw11a9puv.gif)
The form goes through the following stages:
1. The form is initially in an `INVALID` state , and no async validators are running or executed.
2. The user edits the 'username' field and moves the focus away from it. The synchronous validation successfully completes, and then the asynchronous validator for this control starts executing.
3. However, something unexpected happens. The **asynchronous validator `passwordAlreadyUsed` starts running!** , even though the 'password' `FormControl` is **invalid**.

After the user fills out the form and all the validations are completed, the resulting form state is correct. However, we notice that there are unnecessary calls to the server due to the unexpected execution of the `passwordAlreadyUsed` async validator.

Angular's documentation likely meant the order of execution for sync and async validators for the same `FormControl` or `FormGroup.` However, in this case, we have a hierarchy of validators that seems to unexpectedly deviate.    

### Conclusion
Thank you for reading. I hope this article has provided you with valuable insights and saved you time in understanding how Angular's forms behave. It's important to note that while the actual behavior may differ from what you expect, understanding these nuances will help you work more effectively with Angular's forms. You can find all the code samples used in this article at the following location [Github](https://github.com/musatov/angular-samples/tree/master/form-validators). If you have any questions or need further clarification, feel free to reach out. Happy coding!

Links to the previous articles:
[Angular Forms Validation: Part I - Single control validation.](https://dev.to/musatov/angular-forms-validation-part-i-single-control-validation-49op)
[Angular Forms Validation: Part II - FormGroup validation.](https://dev.to/musatov/angular-forms-validation-part-ii-formgroup-validation-3g68)