Angular Icon Get 73% off the Angular Master bundle

See the bundle then add to cart and your discount is applied.

0 days
00 hours
00 mins
00 secs

Write Angular like a pro. Angular Icon

Follow the ultimate Angular roadmap.

Custom Validators in Angular Reactive Forms

Reactive Forms are one of the best features of the Angular framework. But despite their built-in validators that handle a lot of use-cases, it’s likely you’ll want to create your own custom validator for reactive forms at some point - and here’s how!

We’re going to look at two different approaches in this article, first using Angular’s Validators.pattern method, and secondly abstracting patterns away into a custom validator to help us share the code across the application.

For this example, we’ll focus on a simple password validator.

Here’s our App component with some simple reactive form config:

export class App {
  form: FormGroup;

  constructor(private formBuilder: FormBuilder) {
    this.form = this.formBuilder.group({
      password: ['', [Validators.required]],
    });
  }
}

Note that we’re using Validators.required already, which is pretty common for everyone to use.

But let’s say for security reasons we want to ensure users are providing a strong and safe password, we might want to compose a custom validator to enforce it upon signing up to our app.

Using Validators.pattern is one approach, and if you’re just writing a single component is perfectly acceptable.

Here’s a Regular Expression that will match at least one uppercase character, a number, and a minimum of 8 characters:

/^(?=.*[A-Z])(?=.*\d).{8,}$/

Here’s how the Regular Expression works:

Let’s add it to Validators.pattern:

export class App {
  form: FormGroup;

  constructor(private formBuilder: FormBuilder) {
    this.form = this.formBuilder.group({
      password: [
        '',
        [Validators.required, Validators.pattern(/^(?=.*[A-Z])(?=.*\d).{8,}$/)],
      ],
    });
  }
}

Then, in the template we can write the ngIf logic to show the errors when appropriate:

<form [formGroup]="form">
  <input 
    formControlName="password" 
    type="password">
  <div 
    *ngIf="form.get('password').invalid && form.get('password').touched">
    <div 
      *ngIf="form.get('password').errors.required">
      Password is required.
    </div>
    <div 
      *ngIf="form.get('password').errors.pattern">
      Password must be at least 8 characters long and contain at least one uppercase letter and digit.
    </div>
  </div>
</form>

🧠 Please note that by simply enforcing specific characters doesn’t instantly make a password “safe”, your backend security is important on this one.

Notice here how we reference errors.pattern inside the template? For me, this limits the particular patterns we can use as if we want to use multiple patterns we’ll see the same error - but also it’s not very descriptive. What pattern didn’t match?

So, while this works - I believe using a custom validator in Angular could be a better option for us. Not to mention, we can reuse the validator across multiple forms without repeating our Regular Expression over and over.

Angular Directives In-Depth eBook Cover

Free eBook

Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.

  • Green Tick Icon Observables and Async Pipe
  • Green Tick Icon Identity Checking and Performance
  • Green Tick Icon Web Components <ng-template> syntax
  • Green Tick Icon <ng-container> and Observable Composition
  • Green Tick Icon Advanced Rendering Patterns
  • Green Tick Icon Setters and Getters for Styles and Class Bindings

First, create a new file called password.validator.ts (this is just a convention I like to use), and drop in a function:

export const passwordValidator = () => {}

A custom validator returns a function, which gets called by Angular internally. This is a critical step as we get access to any FormControl objects, via the AbstractControl class, and from there we would test the value:

export const passwordValidator = () => {
  return (control: AbstractControl) => {
    // test RegEx against `control.value`
  };
}

All we need to do is either return null or an object (which must contain a property with a Boolean value, used in our template as a reference later).

Sprinkling on the correct types and our end result would look like this:

import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

export const passwordValidator = (): ValidatorFn => {
  return (control: AbstractControl): ValidationErrors | null => {
    const passwordRegex = /^(?=.*[A-Z])(?=.*\d).{8,}$/;

    if (passwordRegex.test(control.value)) {
      return null; // passes the test, return null to clear any errors
     }
     // fails the test, therefore the password is of invalid structure
     return { invalidPassword: true };
  };
};

The passwordRegex.test() returns true or false based on whether the control.value matches the requirement. Our control.value here is the FormControl value.

We use AbstractControl here as the parameter type as FormControl inherits from it - they have the same properties and methods to a degree.

We could compress things down using a ternary operator as well given that it’s a simple validator:

import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

export const passwordValidator = (): ValidatorFn => {
  return (control: AbstractControl): ValidationErrors | null => {
    const passwordRegex = /^(?=.*[A-Z])(?=.*\d).{8,}$/;
    return passwordRegex.test(control.value) ? null : { invalidPassword: true };
  };
};

So now instead of using Validators.pattern inside our FormGroup, we can import our function and call it:

export class App {
  form: FormGroup;

  constructor(private formBuilder: FormBuilder) {
    this.form = this.formBuilder.group({
      password: ['', [Validators.required, passwordValidator()]],
    });
  }
}

Followed by a simple template adjustment to show our new error message when the custom validator triggers:

<form [formGroup]="form">
  <input 
    formControlName="password" 
    type="password">
  <div 
    *ngIf="form.get('password').invalid && form.get('password').touched">
    <div 
      *ngIf="form.get('password').errors.required">
      Password is required.
    </div>
    <div 
      *ngIf="form.get('password').errors.invalidPassword">
      Password must be at least 8 characters long and contain at least one uppercase letter and digit.
    </div>
  </div>
</form>

Note how the change was from errors.pattern to errors.invalidPassword, and that’s it. Much more readable and we get the added benefit of being able to use our validator elsewhere in our application without repeating ourselves.

Give it a try:

It’s also fully testable and encapsulated, so any changes can be made in a single place.

Here’s an example unit test for our custom validator too:

describe('passwordValidator', () => {
  let validatorFn;

  beforeEach(() => {
    validatorFn = passwordValidator();
  });

  it('should return null when the input value is a valid password', () => {
    const validPassword = 'Abcdefg1';
    const control = new FormControl(validPassword);
    const result = validatorFn(control);
    expect(result).toBeNull();
  });

  it('should return an error object when the input value is an invalid password', () => {
    const invalidPassword = 'invalidpassword';
    const control = new FormControl(invalidPassword);
    const result = validatorFn(control);
    expect(result).toEqual({ invalidPassword: true });
  });
});

🚀 Learn more techniques, best practices and real-world expert knowledge, I’d highly recommend checking out my Angular courses - they will guide you through your journey to mastering Angular to the fullest!

Happy validating!

Learn Angular the right way.

The most complete guide to learning Angular ever built.
Trusted by 82,951 students.

Todd Motto

with Todd Motto

Google Developer Expert icon Google Developer Expert

Related blogs 🚀

Free eBooks:

Angular Directives In-Depth eBook Cover

JavaScript Array Methods eBook Cover

NestJS Build a RESTful CRUD API eBook Cover