[How-to] Implement custom form validators with Angular

Form validation is always a hot topic whenever I teach Angular. People usually ask me whether they should use Template driven forms or Reactive forms, and I used to tell them that reactive forms are a better option when you need custom validators.

That was partially wrong though: It is actually fairly easy to write custom validators that work seamlessly with both template-driven and reactive forms, which is what I’m going to show you in this post.

I’m going to create a custom credit card number validator. The validator is going to make sure that a credit card number has 16 digits and that it comes from an accepted credit card company.

In order to perform such validation, we have to implement a validator function that has the following signature:

interface ValidatorFn {    
(c: AbstractControl): ValidationErrors | null

That’s the method signature used for both template-driven and reactive forms. The input parameter is a form control, and the output is either an error object or null if the value is valid.

So here is what that method would look like:

static validateCcNumber(control: FormControl): ValidationErrors {
if (! (control.value.startsWith('37')
|| control.value.startsWith('4')
|| control.value.startsWith('5'))
) {
// Return error if card is not Amex, Visa or Mastercard
return {creditCard : 'Your credit card number is not from a supported credit card provider'};
    } else if (control.value.length !== 16) {
// Return error if length is not 16 digits
return {creditCard : 'A credit card number must be 16-digit long'};}
   // If no error, return null  
return null;

Now that our validator function is defined, we have to make it applicable to any kind of form.

For template-driven forms, we have to make it a directive that implements a specific interface: Validator.

import { Directive, forwardRef } from '@angular/core';
import { NG_VALIDATORS, AbstractControl, ValidationErrors, Validator, FormControl } from '@angular/forms';
selector: '[validCreditCard]',
// We add our directive to the list of existing validators
providers: [
{ provide: NG_VALIDATORS, useExisting: CreditCardValidator, multi: true }
export class CreditCardValidator implements Validator {
// This method is the one required by the Validator interface
validate(c: FormControl): ValidationErrors | null {
// Here we call our static validator function
return CreditCardValidator.validateCcNumber(c);
static validateCcNumber(control: FormControl): ValidationErrors {
// Here goes the validation code mentioned earlier

The above code defines our directive that implements the Validator interface. It also registers that directive as a NG_VALIDATOR, which adds our validator to the collection of existing ones (like required or pattern for instance).

Now I can define a form and make my validator apply to a text input:

<form #form="ngForm" (ngSubmit)="displayData(form.value)">
  <input type="text" name="cardnumber" ngModel validCreditCard />

And that’s it! The above form would be invalid as long as the user does not enter a credit card number that passes our validation criteria.

Now let’s add some error handling to render the right error message from our validator function:

<form #form="ngForm" (ngSubmit)="displayData(form.value)">
  <input type="text" name="cardnumber" ngModel #cc="ngModel" validCreditCard />
  <div *ngIf="cc.dirty && cc.errors?.creditCard" class="error">
  <button type="submit" *ngIf="form.valid">Send info</button>

And there you go, with some added CSS styling, that’s what the above code would render when an invalid number is entered:

Why using a static validator function?

That’s a trick of mine so that I can use that function in a reactive form. As a static method, I can easily pass a reference to it as follows:

this.registerForm = this.formBuilder.group({
firstname: ['', Validators.required],
// ...
creditcardnumber: ['', CreditCardValidator.validateCcNumber]

You can find the full code for that example in that StackBlitz.

My name is Alain Chautard. I am a Google Developer Expert in Angular, as well as founding consultant and trainer at Angular Training where I help web development teams learn and become fluent with Angular.
If you enjoyed this article, please clap for it or share it. Your help is always appreciated.