Skip to the solution
MediumFrontend9 min read

How do you create a custom validator in reactive forms?

944 views

The problem

Question presented to candidate: "Our sign-up form must reject usernames containing banned words, and must check with the server that the name is not already taken. How do you write custom validators for that in reactive forms?"

What a strong answer should cover:

  • A validator is just a function, ValidatorFn = (control: AbstractControl) => ValidationErrors | null: null means valid, an object means invalid and its keys name the errors.
  • Configurable rules are written as a factory that takes options and returns the ValidatorFn (forbiddenWords(["spam"])), the same shape as Validators.minLength(3).
  • Put useful detail in the error value ({ forbiddenWord: { word: "spam" } }) so the template can build a message with hasError / getError.
  • Async validators (AsyncValidatorFn) return a Promise or Observable; the control is PENDING meanwhile, and they only run once all sync validators pass.
  • Attach with validators / asyncValidators in the options object, or addValidators later; for template-driven forms wrap the function in a directive provided via NG_VALIDATORS.
  • Know the four statuses: VALID, INVALID, PENDING, DISABLED.

Clarifying questions expected:

  • "Should the check run on every keystroke, or only on blur, given that one of them calls the server?"
  • "Is the banned-word list static, or does it come from configuration?"

Code / implementation expected: Yes β€” a validator factory and an async validator on one control, with the resulting error objects shown.

  • forms
  • validators

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read Β· 10 sections

Read the code

AngularA validator factory plus an async availability check, with status and error objects shown
import { Component, signal } from "@angular/core";
import {
  AbstractControl,
  AsyncValidatorFn,
  FormControl,
  ReactiveFormsModule,
  ValidationErrors,
  ValidatorFn,
  Validators,
} from "@angular/forms";

// 1. A validator FACTORY: takes config, returns the ValidatorFn Angular calls.
export function forbiddenWords(words: string[]): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const value = String(control.value ?? "").toLowerCase();
    const hit = words.find((w) => value.includes(w));
    // null means valid; an object means invalid, and its key names the error.
    return hit ? { forbiddenWord: { word: hit, actual: control.value } } : null;
  };
}

// 2. An ASYNC validator: returns a Promise (or Observable) of errors or null.
const taken = ["admin", "root"];
export const usernameAvailable: AsyncValidatorFn = (control) =>
  new Promise((resolve) =>
    setTimeout(() => resolve(taken.includes(control.value) ? { usernameTaken: true } : null), 50)
  );

@Component({
  selector: "app-root",
  imports: [ReactiveFormsModule],
  template: `
    <input id="username" [formControl]="username" />
    @if (username.hasError("forbiddenWord")) {
      <p class="msg">"{{ username.getError("forbiddenWord").word }}" is not allowed</p>
    }
    @if (username.hasError("usernameTaken")) {
      <p class="msg">That name is taken</p>
    }
    <p id="status">{{ status() }}</p>
  `,
})
export class AppComponent {
  username = new FormControl("", {
    nonNullable: true,
    validators: [Validators.required, Validators.minLength(3), forbiddenWords(["badword", "spam"])],
    asyncValidators: [usernameAvailable],
  });
  status = signal(this.snapshot());

  constructor() {
    // statusChanges fires on every validation run, including PENDING -> VALID/INVALID.
    this.username.statusChanges.subscribe(() => this.status.set(this.snapshot()));
  }

  snapshot() {
    return this.username.status + " " + JSON.stringify(this.username.errors);
  }
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 24 of 121 in Angular

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Angular questions
Up nextMedium

How does catchError work in RxJS, and how do you return a fallback value?

Continue