The sun was barely climbing over Medellín’s Aburrá Valley when I hopped on a video call from a breezy veranda in Santo Domingo. Lucía, our newest hire in Bogotá, had built a slick sign-up flow—except users could enter the same password they used last year, bypassing security policy. “Angular’s built-in rules don’t cover our history check,” she sighed as rain hammered my balcony roof. A quick screen share, one custom validator, and a dash of async logic later, we blocked recycled passwords without freezing the UI. Frame times stayed smooth on her budget phone, security passed audit, and I still made it to desayuno with time to spare. That morning fix is the backbone for today’s deep dive into rolling your own form validators.


Why Custom Validation Deserves a Spot in Your Toolkit

Browser-native constraints handle basics like required and pattern. Angular’s synchronous validators catch most use cases—yet real products often demand checks such as:

Shipping those conditions client-side means earlier feedback, smaller server logs, and accessible error messaging for screen readers. Done correctly, custom validators slot into Angular’s reactive-forms engine as first-class citizens—no hacks, no performance penalties.


Foundations: Validators in Plain English

A validator is a pure function. It receives an AbstractControl, inspects its current value (or sibling controls), and returns either null when all is well or an error object describing the failure. Angular merges these error maps so template directives such as formControl.errors?.passwordHistory reveal precise messages.

Async validators share the same contract but return an Observable or Promise that resolves to the error map. Perfect for hitting APIs without blocking change detection.


Building a Synchronous Rule: Banning Reused Passwords

First, scaffold a new standalone file:

bashCopyEditng g class validators/password-history --type=validator --skip-tests
tsCopyEdit// validators/password-history.validator.ts
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

export function passwordHistoryValidator(
  lastHashes: string[]
): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const raw = String(control.value || '');
    const hash = sha256(raw);                     // placeholder for real hash
    return lastHashes.includes(hash)
      ? { passwordHistory: true }
      : null;
  };
}

Hook it into a FormControl:

tsCopyEditform = this.fb.group({
  password: [
    '',
    [Validators.required, Validators.minLength(8), passwordHistoryValidator(this.prev)]
  ],
});

Angular runs our rule whenever the control value changes, in the same pass as built-ins.


Adding Async Validation: Username Availability

tsCopyEditexport function usernameTakenValidator(api: UserService): AsyncValidatorFn {
  return (control: AbstractControl) =>
    timer(400).pipe(                 // debounce keystrokes
      switchMap(() => api.exists(control.value)),
      map(exists => (exists ? { usernameTaken: true } : null)),
      first()
    );
}

Attach it:

tsCopyEditusername: [
  '',
  [Validators.required, Validators.pattern(/^[a-z\d]{4,}$/i)],
  [usernameTakenValidator(this.api)]
],

Angular disables the submit button while the observable is unresolved—no extra code.


Remote-Work Insight Box

When our team spread across five time zones, we noticed async validators hammering staging. We wrapped every API call in a small NgRx effect that cached results for ten minutes. CDN edge locations near Panama and Brazil served hits instantly, bandwidth dropped, and our Peruvian QA colleague finally stopped seeing “rate limit” toast notifications at midnight.


Rendering Friendly Error Messages

htmlCopyEdit<input type="password" formControlName="password" />
<small *ngIf="form.get('password')?.errors?.passwordHistory">
  Please pick a password you haven’t used before.
</small>

Screen readers announce the message because it appears after the input in DOM order; add aria-live="polite" for extra safety.


Performance & Accessibility Checkpoints


Common Pitfalls & Straightforward Remedies

SymptomLikely CauseQuick Fix
Infinite loop of validator runsFunction recreated in templateDefine validator once in constructor, not inline
Async rule never resolvesObservable missing completeFinish with first() or take(1)
Errors disappear on blurControl marked touched too earlyTrigger validation on submit or valueChanges only
Server & client rules divergeTwo code pathsShare schema via Zod or run same validator in NestJS

CLI Command Call-Out

CLI CommandPurpose
ng g class validators/age-range --type=validatorSkeleton for new rule
ng testRun spec suite; custom validators need unit tests
ng profiler timeChangesBenchmark validator impact

Sketching the Architecture (SVG Idea)

A flow diagram where user input enters FormControl ➜ synchronous validators run ➜ async validator debounced ➜ API response merges errors ➜ template updates ➜ ARIA alert announces change. Side branch shows NgRx cache path to illustrate performance boost.


Bringing It All Together

Below is a compact signup component that ties the concepts:

tsCopyEdit@Component({
  selector: 'signup-form',
  templateUrl: './signup.html',
  standalone: true,
  imports: [ReactiveFormsModule],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SignupFormComponent {
  lastHashes = ['ab12...', 'cd34...'];        // fetched on init

  form = this.fb.group({
    username: [
      '',
      [Validators.required, Validators.minLength(4)],
      [usernameTakenValidator(this.api)]
    ],
    password: [
      '',
      [Validators.required, Validators.minLength(8), passwordHistoryValidator(this.lastHashes)]
    ],
    confirm: [''],
  }, { validators: match('password', 'confirm') });

  constructor(private fb: FormBuilder, private api: UserService) {}

  submit() {
    if (this.form.valid) this.api.register(this.form.value).subscribe();
  }
}

Halo of OnPush, custom sync rule, async availability check, and a cross-field matcher. Users get instant feedback; bandwidth stays lean.


Wrap-Up

Crafting your own validators turns Angular’s forms from generic to bespoke without surrendering performance or accessibility. Keep functions pure, debounce async logic, and cache wherever remote work leads you—from Colombian rooftops to Mexican coworks. Your users will feel the polish, and compliance auditors will smile.

Have a tricky validation scenario? Drop it below; I’ll reply between flights and late-night arepa sessions.

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x