How to Create a Custom Validator with Parameters in Angular Reactive Forms

Thiago Araújo - Mar 5 - - Dev Community

Hey there! When you’re crafting reactive forms in Angular—be it for a user profile, a survey, or something out-of-this-world like a spaceship control panel—you’ll often need to enforce specific rules on your input fields. Maybe a username must contain "user," an email needs an "@" symbol, or another field requires "xyz." The core question is the same: "Does this value contain a specific substring?" But hardcoding a custom validator for each field, tweaking only the substring, feels repetitive and messy. As fans of the DRY principle (Don’t Repeat Yourself), we can do better! Imagine a single validator recipe that adapts to any flavor you throw at it. That’s where JavaScript closures shine!

The Problem: Repetitive Validation Logic

Imagine you’re crafting a reactive form in Angular—say, a quirky profile where one field needs to include "banana" (maybe it’s a secret passphrase) and another must have "angular" (to flex your framework fandom). Without a smart approach, you might write separate validators like this:

function containsBananaValidator(control: AbstractControl): ValidationErrors | null {
  return control.value.includes('banana') ? null : { missing: 'banana' };
}

function containsAngularValidator(control: AbstractControl): ValidationErrors | null {
  return control.value.includes('angular') ? null : { missing: 'angular' };
}
Enter fullscreen mode Exit fullscreen mode

Then, hook them up to your form:

const myForm = new FormGroup({
  passphrase: new FormControl('', containsBananaValidator),
  fandom: new FormControl('', containsAngularValidator)
});
Enter fullscreen mode Exit fullscreen mode

Spot the issue? The logic—checking if a value contains a substring—is identical, except for the substring itself. If your form has 10 fields, you’d end up with 10 nearly identical functions. That’s not just tedious; it’s a maintenance nightmare. Let’s fix that with closures and factory functions!

What’s a Closure, Anyway?

Before we jump into the solution, let’s demystify closures. Think of a closure as a function with a memory—a little backpack carrying variables from where it was created. Even after its parent function finishes, the closure keeps access to those variables. In JavaScript, this happens when you define a function inside another and return it.

Here’s a fun example to get the gist:

function makeGreeter(greeting: string) {
  return function(name: string): string {
    return `${greeting}, ${name}!`;
  };
}

const sayHello = makeGreeter('Hello');
console.log(sayHello('Alice')); // "Hello, Alice!"
const sayHi = makeGreeter('Hi');
console.log(sayHi('Bob')); // "Hi, Bob!"
Enter fullscreen mode Exit fullscreen mode

makeGreeter builds a function that remembers the greeting. When we call sayHello, it pulls "Hello" from its backpack. Same logic, different data—perfect for our validator needs! If this feels fuzzy, don’t worry—the validator example will make it click.

Closures and Factory Functions

A factory function is a function that creates and returns other functions or objects. It acts like a "factory" by producing new, customized instances based on the parameters you pass to it.

// Function creating new objects
// without use of 'new' keyword
function createRobot(name) {
  return {
    name: name,
    talk: function () {
      console.log("My name is " + name + ", the robot.");
    },
  };
}

//Create a robot with name Chitti
const robo1 = createRobot("Chitti");
// source code: https://www.geeksforgeeks.org/what-are-factory-functions-in-javascript/
Enter fullscreen mode Exit fullscreen mode

Closures and factory functions are not the same, but they are related concepts in JavaScript that often work together. This is the tool that we will use here!

The solution: Crafting a Reusable Validator

In Angular, a validator is a function that takes an AbstractControl and returns null (valid) or an error object (invalid). We’ll create a factory function that takes a substring, packs it into a closure, and returns a custom validator tailored to that substring. Here’s the magic

function containsSubstringValidator(substring: string) {
  return (control: AbstractControl): ValidationErrors | null => {
    if (control.value && typeof control.value === 'string' && control.value.includes(substring)) {
      return null; // Valid: substring found
    } else {
      return { doesNotContain: substring }; // Invalid: substring missing
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

Now, apply it to your form like this:

const myForm = new FormGroup({
  passphrase: new FormControl('', containsSubstringValidator('banana')),
  fandom: new FormControl('', containsSubstringValidator('angular'))
});
Enter fullscreen mode Exit fullscreen mode

What’s happening here?

  • containsSubstringValidator('banana') generates a validator that checks for "banana".
  • containsSubstringValidator('angular') does the same for "angular".
  • One function, two (or more!) use cases. No repetition, just elegance.

Why this rocks?

Reusability: Write the logic once, tweak it with parameters—done!

Simplicity: No need for bulky classes or complex setups.

Angular-Friendly: Angular’s built-in validators, like Validators.minLength(5), use closures too. You’re in good company!

Angular uses this too!

Speaking of Angular, let’s peek under the hood. This is the current implementation of Validators.minLength :

export function minLengthValidator(minLength: number): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const length = control.value?.length ?? lengthOrSize(control.value);
    if (length === null || length === 0) {
      // don't validate empty values to allow optional controls
      // don't validate values without `length` or `size` property
      return null; 
    }

    return length < minLength
      ? {'minlength': {'requiredLength': minLength, 'actualLength': length}}
      : null;
  };
}
Enter fullscreen mode Exit fullscreen mode

See the implementation in there:

https://github.com/angular/angular/blob/2e03a8685be440a531ce94f060691af62838d8a9/packages/forms/src/validators.ts#L529

Taking It Further

Want to get fancy? Make the validator more general. Maybe pass a function instead of a substring:

function customValidator(check: (value: string) => boolean) {
  return (control: AbstractControl): ValidationErrors | null => {
    if (
      control.value &&
      typeof control.value === "string" &&
      check(control.value)
    ) {
      return null;
    } else {
      return { customError: true };
    }
  };
}

const myForm = new FormGroup({
  username: new FormControl(
    "",
    customValidator((val) => val.includes("user"))
  ),
  email: new FormControl(
    "",
    customValidator((val) => val.includes("@"))
  ),
});
Enter fullscreen mode Exit fullscreen mode

Seeing It in Action

Want to see this live? Check out a demo implementation on this website. Watch those inputs light up invalid until they meet the rules!

Demo implementation of the validation in action

The source code you can get here.

Wrapping Up

There you have it! Closures transform your validator game in Angular reactive forms, letting you write one flexible function that adapts to any field. It’s reusable, readable, and feels like wizardry. Next time you’re building a form with parameterized rules, pull out this trick—you’ll save time, reduce clutter, and impress your peers.

Got questions? Try it out and drop a comment—I’d love to hear how it goes! For a visual take, check out this awesome video: https://youtu.be/2PfMVL0OIGg.

.