Carlos Cifuentes
AngularJS → Angular reference

$validators / $asyncValidators in modern Angular

Validation directives that wrote to $validators and $asyncValidators become plain functions attached to the form control.

Before · AngularJS
app.directive('companyEmail', function ($q, UserService) {
  return {
    require: 'ngModel',
    link: function (scope, el, attrs, ngModel) {
      ngModel.$validators.company = function (v) { return !v || v.endsWith('@acme.com'); };
      ngModel.$asyncValidators.unique = function (value) {
        return UserService.exists(value).then(function (taken) {
          if (taken) { return $q.reject(); }
        });
      };
    }
  };
});
After · Angular
export const companyEmail: ValidatorFn = control =>
  !control.value || control.value.endsWith('@acme.com') ? null : { company: true };

export function uniqueEmail(): AsyncValidatorFn {
  const users = inject(UserService);
  return control => users.exists(control.value).pipe(
    map(taken => (taken ? { unique: true } : null)),
  );
}

// component field, so inject() has an injection context
email = new FormControl('', {
  nonNullable: true,
  validators: [Validators.required, companyEmail],
  asyncValidators: [uniqueEmail()],
});

What changes

  • A $validators function returned true or false; a ValidatorFn returns null when valid or an error object like { company: true }.
  • An $asyncValidators promise that rejected becomes an AsyncValidatorFn that emits null or an error object.
  • No directive is needed with reactive forms. Async validators run only after the sync ones pass, as before.

Migration tip. An async validator runs on every value change. Set updateOn: 'blur' on the control or add a delay inside the validator, the job ng-model-options debounce did in AngularJS.

Related

Planning a migration or a micro-frontend platform?

In a 20-minute call you describe your situation and I tell you how I'd approach it. If it isn't worth doing yet, I'll say so.

Message me on LinkedIn