Skip to content

How to create and use Angular async validators

calendar_monthPosted on:December 15, 2023

Angular doesn’t just provide best-in-class tools to create and manage forms, it gives you the building blocks to extend them as your applications need. A very common scenario when working with forms is to need to validate the inputted data against an asynchronous source, like with an API request or a store. Let’s see how we can do that.

What’s a validator?

Validators… well… validate.

Forms, as implemented natively in HTML/JS, have attributes we can add to our controls. These enforce restrictions we impose to create valid and invalid states. Here are some of them.

NOTE: Never rely on client-side validation for security purposes. It’s very easy for a malicious user to alter data.

<form>
  <label>
    Name
    <input type="text" required />
  </label>

  <label>
    Email
    <input type="email" required maxlength="100" />
  </label>

  <label>
    Age
    <input type="number" max="125" />
  </label>
</form>

There are three controls in this form: name, email, and age; each with their own unique validators:

NOTE: Angular doesn’t do anything with validity by default. It’s up to the developer to decide what to do in these cases and handle it.

How Angular enhances them

These validators work just like that in Angular. They are augmented with directives to better integrate with how the framework manages forms. Reactive forms have their own implementation but mimic the same behavior. For the purpose of this article, we’re gonna skip over the differences between both types of forms and go into the implementation details of validator functions.

import { ValidatorFn } from "@angular/forms";

// type ValidatorFn = (control: AbstractControl) => ValidationErrors | null;
export const fooValidator: ValidatorFn = control => {
  return control.value === "foo" ? null : { fooError: true };
};

// Component imports and code omitted for brevity
@Component({ template: `<input [formControl]="control" />` })
export class MyComponent {
  control = new FormControl(null, { validators: [fooValidator] });
}

A validator in Angular is a function that takes an AbstractControl (any type of control) as an argument and returns an object with an error key or null when it passes validation. Any number of validators can be passed into a control, but note you must pass the function without calling it. It may seem obvious but it’s worth pointing out.

An async validator works in the same way, but it returns an Observable or Promise of ValidationErrors or null instead.

A not-so-great way to validate asynchronously

We’ve outlined how validators work and briefly mentioned how async validators differ. Let’s propose a solution – albeit a bad one – that aligns with how asynchronous logic is commonly handled in Angular code.

@Component({
  template: `<input [formControl]="control" (change)="controlChanged()" />`,
})
export class MyComponent {
  control = new FormControl(null);

  constructor(private http: HttpClient) {}

  controlChanged() {
    this.http
      .post("/check-value", { value: this.control.value })
      .subscribe(result => this.control.setErrors(result));
  }
}

What’s going on here? We’re calling an endpoint with the control’s value to see if it’s valid, then setting errors manually on the control. It might be clear to you that this is wrong, but I thought we’d get it out of the way. Subscribing to an HTTP client response to set things synchronously is something you see all the time. But as usual, there’s a better way to handle asynchronous logic.

Enter async validators

Async validators are implemented almost exactly like the other validators, except they return an Observable or Promise and they’re assigned to a different property.

import { AsyncValidatorFn } from "@angular/forms";

// type AsyncValidatorFn = (control: AbstractControl)
//   => Observable<ValidationErrors | null> | Promise<ValidationErrors | null>;
export const asyncFooValidator: AsyncValidatorFn = control => {
  return of(control.value === "foo" ? null : { fooError: true });
};

@Component({ template: `<input [formControl]="control" />` })
export class MyComponent {
  control = new FormControl(null, { asyncValidators: [asyncFooValidator] });
}

Another big distinction is that they have a "PENDING" state. It indicates that validation is waiting for something to complete, and the control and form aren’t valid until it does. This is very important to know because, at the same time, it’s not considered invalid either. If you’re acting on the status to perform logic on a form with async validators, use the valid or status properties.

<form [formGroup]="formGroup">
  <!-- This won't work as intended if the FormGroup or its controls 
       have an async validator that's pending!  -->
  <button [disabled]="formGroup.invalid"></button>

  <!-- Do one of these instead -->
  <button [disabled]="!formGroup.valid"></button>
  <button [disabled]="formGroup.invalid || formGroup.pending"></button>
  <button [disabled]="formGroup.status !== 'VALID'"></button>
</form>

Making more flexible Validators using factories

Going back to the first AsyncValidatorFn example, you might notice it has a big limitation: it can’t take any more arguments. How are we supposed to provide services, dependencies, or other sources to it? By returning them from a factory function.

A factory function is a pattern to configure and return an internal entity by using closures. I’ll explain how this applies to functions and, more specifically, validators.

import { ValidatorFn } from "@angular/forms";

export const maxLength: (length: number) => ValidatorFn = length => {
  return control => {
    return control.value.length >= length ? { maxLengthError: true } : null;
  };
};

@Component({ template: `<input [formControl]="control" />` })
export class MyComponent {
  control = new FormControl(null, { validators: [maxLength(20)] });
}

We mentioned before that validator functions are passed without calling them, so why are we doing that here? We aren’t calling the validator – we’re calling a factory. Here, maxLength is a function that returns another function: the validator we need. The maxLength function takes a number and returns the configured function, which is what we’re actually passing in.

For async validators, we typically need to access injected dependencies, like HTTP services or stores. In the next section, we’ll talk about all the patterns we can use to achieve that. Some better than others, but all exposed for you to take your pick depending on your use case.

All the ways to create and use async validators

Injectable dependencies must be retrieved from an injection context. There are many ways to do it, but this, specifically, won’t work:

import { FormGroup, ValidatorFn } from "@angular/forms";

export const apiCheck: () => AsyncValidatorFn = () => {
  const service = inject(MyService);

  return control => {
    return service
      .apiCheck(control.value)
      .pipe(map(valid => (valid ? null : { apiCheckError: true })));
  };
};

@Component({ template: `<input [formControl]="control" />` })
export class MyComponent implements OnInit {
  control!: FormControl;

  ngOnInit() {
    // Careful: this doesn't work!
    control = new FormControl(null, { validators: [apiCheck()] });
  }
}

ngOnInit doesn’t have access to inject dependencies. In other words, any function that runs after your class is constructed will fail to inject using this pattern. Let’s see what the options are.

Initialize the form in an injection context

The same code as above will work if the factory function is called in the constructor or during class field initialization.

@Component({ template: `<input [formControl]="control" />` })
export class MyComponent {
  // In this case, the constructor isn't needed
  control = new FormControl(null, { validators: [apiCheck()] });

  // or:
  control: FormControl;
  constructor() {
    this.control = new FormControl(null, { validators: [apiCheck()] });
  }
}

Call the factory in an injection context

If your form needs to be initialized OnInit or at any other point, you can call the factory and assign it later.

@Component({ template: `<input [formControl]="control" />` })
export class MyComponent implements OnInit {
  // The factory is executed in an injection context
  // and gets its own dependencies first.
  apiCheckValidator = apiCheck();
  control!: FormControl;

  ngOnInit() {
    // The control is created later and the prebuilt validator is assigned.
    this.control = new FormControl(null, { validators: [apiCheckValidator] });
  }
}

Pass the dependency to the factory

Instead of letting it retrieve its own dependencies, you can pass them to the factory.

import { FormGroup, ValidatorFn } from "@angular/forms";

export const apiCheck: (service: MyService) => ValidatorFn = service => {
  return control => {
    return service
      .apiCheck(control.value)
      .pipe(map(valid => (valid ? null : { apiCheckError: true })));
  };
};

@Component({ template: `<input [formControl]="control" />` })
export class MyComponent implements OnInit {
  // The dependency is injected by the component
  // and passed to the factory function.
  apiCheckValidator = apiCheck(inject(MyService));
  control!: FormControl;

  // Constructor injection works the same.
  constructor(service: MyService) {
    this.apiCheckValidator = apiCheck(service);
  }

  ngOnInit() {
    // The control is created later and the prebuilt validator is assigned.
    this.control = new FormControl(null, { validators: [apiCheckValidator] });
  }
}

Declare and bind a method in the same class

I don’t use this, ahem, method personally, but it’s a perfectly valid pattern and worth mentioning. If your service is already a dependency of the class and you want to keep the implementation close, you can create a method and pass that as a validator.

@Component({ template: `<input [formControl]="control" />` })
export class MyComponent implements OnInit {
  control!: FormControl;

  constructor(private service: MyService) {}

  ngOnInit() {
    // We aren't calling the function because it's not a factory.
    // Also notice it must be bound to `this` because of the instance dependency.
    this.control = new FormControl(null, { validators: [this.apiCheck.bind(this)] });
  }

  private apiCheck(control) {
    return this.service
      .apiCheck(control.value)
      .pipe(map(valid => (valid ? null : { apiCheckError: true })));
  }
}

You can also combine this with the factory pattern.

Class-based AsyncValidators

I think this is actually the most common – call it old-school – way to implement async validators. In some ways, they might still be easier to reason about since they manage their own dependencies. This next example is straight from angular.dev (with some omissions):

@Injectable({ providedIn: "root" })
export class UniqueRoleValidator implements AsyncValidator {
  constructor(private actorsService: ActorsService) {}

  validate(control) {
    return this.actorsService
      .isRoleTaken(control.value)
      .pipe(map(isTaken => (isTaken ? { uniqueRole: true } : null)));
  }
}

As more and more APIs move toward functional implementations, I find myself not using this pattern a lot. It’s kinda clunky, too. Consumers inject it as a service and also need to bind the method to its implementor:

@Component({ template: `<input [formControl]="control" />` })
export class MyComponent implements OnInit {
  constructor(private roleValidator: UniqueRoleValidator) {}

  control = new FormControl("", {
    asyncValidators: [this.roleValidator.validate.bind(this.roleValidator)],
  });
}

Directives

This very similar class-based example has an important difference: it’s a directive. It provides itself as NG_ASYNC_VALIDATORS and, of course, has a selector, so it’s much more ergonomic.

@Directive({
  selector: "[appUniqueRole]",
  providers: [
    {
      provide: NG_ASYNC_VALIDATORS,
      useExisting: forwardRef(() => UniqueRoleValidatorDirective),
      multi: true,
    },
  ],
})
export class UniqueRoleValidatorDirective implements AsyncValidator {
  constructor(private validator: UniqueRoleValidator) {}

  validate(control) {
    return this.validator.validate(control);
  }
}

This is meant to be used for template-driven forms, but I see no problem combining them with reactive forms. 😉

<input [formControl]="control" appUniqueRole /> <input [(ngModel)]="model" appUniqueRole />

Other patterns not covered here

There are other similar ways to achieve the same things described here that weren’t mentioned. For example, injection tokens, classes with static methods, etc. We won’t go over them because the concepts are transferrable and largely the same, but use whatever works for you. We’re also skipping alternative ways to run code in injection contexts since it’s beyond our scope for this, but it’s certainly possible. There might be even more ways to create these that are useful and I don’t know about, so if you want to share them, reach out!

Known issues with async validators and potential solutions

There are several open GitHub issues regarding statusChanges in controls with AsyncValidators. Multiple scenarios cause them to be stuck in the "PENDING" state, requiring different workarounds to force them out. I usually fix them by setting the errors manually on the same control inside the validator, but your outcome may vary.

export const apiCheck: (service: MyService) => AsyncValidatorFn = service => control =>
  service.apiCheck(control.value).pipe(
    map(valid => {
      const errors = valid ? null : { apiCheckError: true };
      // This is a non-exhaustive example. When setting errors
      // manually, you want to merge with existing errors.
      control.setErrors(errors);
      return errors;
    })
  );

Issue #41519 has lots more detail, including other solutions and an explanation about why this can’t be fixed without breaking changes. Other related issues are linked from this one as well.

Conclusion

Async validators are yet another very powerful abstraction Angular provides to improve developer experience with forms. Hopefully after reading this, you’ve learned about how browsers validate natively, what Angular brings to the table for them, and how to connect with asynchronous sources to perform these checks seamlessly behind the scenes.

Special thanks to Nelson Gutierrez and Andrés Villanueva for reviewing!

Resources