Custom Validators in Angular
Forms are part of almost every web application out there. Angular strives for making working with forms a breeze. While there are a couple of built-in validators provided by the framework, we often need to add some custom validation capabilities to our application’s form, in order to fulfill our needs.
We can easily extend the browser vocabulary with additional custom validators and in this article we are going to explore how to do that.
Built-in Validators
Angular comes with a subset of built-in validators out of the box. We can apply them either declaratively as directives on elements in our DOM, in case we’re building a template-driven form, or imperatively using the FormControl
and FormGroup
or FormBuilder
APIs, in case we’re building a reactive forms. If you don’t know what it’s all about with template-driven and reactive forms, don’t worry, we have an articles about both topics here and here.
The supported built-in validators, at the time of writing this article, are:
- required - Requires a form control to have a non-empty value
- minlength - Requires a form control to have a value of a minimum length
- maxlength - Requires a form control to have a value of a maximum length
- pattern - Requires a form control’s value to match a given regex
As mentioned earlier, validators can be applied by simply using their corresponding directives. To make these directives available, we need to import Angular’s FormsModule
to our application module first:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [BrowserModule, FormsModule], // we add FormsModule here
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule {}
Once this is done, we can use all directives provided by this module in our application. The following form shows how the built-in validators are applied to dedicated form controls:
<form novalidate>
<input type="text" name="name" ngModel required>
<input type="text" name="street" ngModel minlength="3">
<input type="text" name="city" ngModel maxlength="10">
<input type="text" name="zip" ngModel pattern="[A-Za-z]{5}">
</form>
Or, if we had a reactive form, we’d need to import the ReactiveFormsModule
first:
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
imports: [BrowserModule, ReactiveFormsModule],
...
})
export class AppModule {}
And can then build our form either using FormControl
and FormGroup
APIs:
@Component()
class Cmp {
form: FormGroup;
ngOnInit() {
this.form = new FormGroup({
name: new FormControl('', Validators.required)),
street: new FormControl('', Validators.minLength(3)),
city: new FormControl('', Validators.maxLength(10)),
zip: new FormControl('', Validators.pattern('[A-Za-z]{5}'))
});
}
}
Or use the less verbose FormBuilder
API that does the same work for us:
@Component()
class Cmp {
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.form = this.fb.group({
name: ['', Validators.required],
street: ['', Validators.minLength(3)],
city: ['', Validators.maxLength(10)],
zip: ['', Validators.pattern('[A-Za-z]{5}')]
});
}
}
We would still need to associate a form model with a form in the DOM using the [formGroup]
directive likes this:
<form novalidate [formGroup]="form">
...
</form>
Observing these two to three different methods of creating a form, we might wonder how it is done that we can use the validator methods imperatively in our component code, and apply them as directives to input controls declaratively in our HTML code.
It turns out there’s really not such a big magic involved, so let’s build our own custom email validator.
Building a custom validator
In it’s simplest form, a validator is really just a function that takes a Control
and returns either null
when it’s valid, or and error object if it’s not. A TypeScript interface for such a validator looks something like this:
interface Validator<T extends FormControl> {
(c:T): {[error: string]:any};
}
Let’s implement a validator function validateEmail
which implements that interface. All we need to do is to define a function that takes a FormControl
, checks if it’s value matches the regex of an email address, and if not, returns an error object, or null
in case the value is valid.
Here’s what such an implementation could look like:
import { FormControl } from '@angular/forms';
function validateEmail(c: FormControl) {
let EMAIL_REGEXP = ...
return EMAIL_REGEXP.test(c.value) ? null : {
validateEmail: {
valid: false
}
};
}
Pretty straight forward right? We import FormControl
from @angular/forms
to have the type information the function’s signature and simply test a regular expression with the FormControl
’s value. That’s it. That’s a validator.
But how do we apply them to other form controls? Well, we’ve seen how Validators.required
and the other validators are added to the new FormControl()
calls. FormControl()
takes an initial value, a synchronous validator and an asynchronous validator. Which means, we do exactly the same with our custom validators.
ngOnInit() {
this.form = new FormGroup({
...
email: new FormControl('', validateEmail)
});
}
Don’t forget to import validateEmail
accordinlgy, if necessary. Okay cool, now we know how to add our custom validator to a form control.
However, what if we want to combine multiple validators on a single control? Let’s say our email field is required
and needs to match the shape of an email address. FormControl
s takes a single synchronous and a single asynchronous validator, or, a collection of synchronous and asynchronous validators.
Here’s what it looks like if we’d combine the required
validator with our custom one:
ngOnInit() {
this.form = new FormGroup({
...
email: new FormControl('', [
Validators.required,
validateEmail
])
});
}
Building custom validator directives
Now that we’re able to add our custom validator to our form controls imperatively when building model-driven forms, we might also enable our validator to be used in template driven forms. In other words: We need a directive. The validator should be usable like this:
<form novalidate>
...
<input type="email" name="email" ngModel validateEmail>
</form>
validateEmail
is applied as an attribute to the <input>
DOM element, which already gives us an idea what we need to do. We need to build a directive with a matching selector so it will be executed on all input controls where the directive is applied. Let’s start off with that.
import { Directive } from '@angular/core';
@Directive({
selector: '[validateEmail][ngModel]'
})
export class EmailValidator {}
We import the @Directive
decorator form @angular/core
and use it on a new EmailValidator
class. If you’re familiar with the @Component
decorator that this is probably not new to you. In fact, @Directive
is a superset of @Component
which is why most of the configuration properties are available.
Okay, technically we could already make this directive execute in our app, all we need to do is to add it to our module’s declarations
:
import { EmailValidator } from './email.validator';
@NgModule({
...
declarations: [AppComponent, EmailValidator],
})
export class AppModule {}
Even though this works, there’s nothing our directive does at the moment. What we want to do is to make sure that our custom validator is executed when Angular compiles this directive. How do we get there?
Angular has an internal mechanism to execute validators on a form control. It maintains a multi provider for a dependency token called NG_VALIDATORS
. If you’ve read our article on multi providers in Angular, you know that Angular injects multiple values for a single token that is used for a multi provider. If you haven’t, we highly recommend checking it out as the rest of this article is based on it.
It turns out that all built-in validators are already added to the NG_VALIDATORS
token. So whenever Angular instantiates a form control and performs validation, it basically injects the dependency for the NG_VALIDATORS
token, which is a list of all validators, and executes them one by one on that form control.
Since multi providers can be extended by adding more multi providers to a token, we can consider NG_VALIDATORS
as a hook to add our own validators.
Let’s add our validator to the NG_VALIDATORS
via our new directive:
import { Directive } from '@angular/core';
import { NG_VALIDATORS } from '@angular/forms';
@Directive({
selector: '[validateEmail][ngModel]',
providers: [
{ provide: NG_VALIDATORS, useValue: validateEmail, multi: true }
]
})
class EmailValidator {}
Again, if you’ve read our article on multi providers, this should look very familiar to you. We basically add a new value to the NG_VALIDATORS
token by taking advantage of multi providers. Angular will pick our validator up by injecting what it gets for NG_VALIDATORS
, and performs validation on a form control. Awesome, we can now use our validator for reactiveand for template-driven forms!
Custom Validators with dependencies
Sometimes, a custom validator has dependencies so we need a way to inject them. Let’s say our email validator needs an EmailBlackList
service, to check if the given control value is not only a valid email address but also not on our email black list (in an ideal world, we’d build a separate validator for checking against an email black list, but we use that as a motivation for now to have a dependency).
The not-so-nice way
One way to handle this is to create a factory function that returns our validateEmail
function, which then uses an instance of EmailBlackList
service. Here’s what such a factory function could look like:
import { FormControl } from '@angular/forms';
function validateEmailFactory(emailBlackList: EmailBlackList) {
return (c: FormControl) => {
let EMAIL_REGEXP = ...
let isValid = /* check validation with emailBlackList */
return isValid ? null : {
validateEmail: {
valid: false
}
};
};
}
This would allow us to register our custom validator via dependency injection like this:
@Directive({
...
providers: [
{
provide: NG_VALIDATORS,
useFactory: (emailBlackList) => {
return validateEmailFactory(emailBlackList);
},
deps: [EmailBlackList]
multi: true
}
]
})
class EmailValidator {}
We can’t use useValue
as provider recipe anymore, because we don’t want to return the factory function, but rather what the factory function returns. And since our factory function has a dependency itself, we need to have access to dependency tokens, which is why we use useFactory
and deps
. If this is entirely new to you, you might want to read our article on Dependency Injection in Angular before we move on.
Even though this would work, it’s quite a lot of work and also very verbose. We can do better here.
The better way
Wouldn’t it be nice if we could use constructor injection as we’re used to it in Angular? Yes, and guess what, Angular has us covered. It turns out that a validator can also be a class as long as it implements a validate(c: FormControl)
method. Why is that nice? Well, we can inject our dependency using constructor injection and don’t have to setup a provider factory as we did before.
Here’s what our EmailValidator
class would look like when we apply this pattern to it:
@Directive({
...
})
class EmailValidator {
validator: Function;
constructor(emailBlackList: EmailBlackList) {
this.validator = validateEmailFactory(emailBlackList);
}
validate(c: FormControl) {
return this.validator(c);
}
}
However, we now need to adjust the provider for NG_VALIDATORS
, because we want to use an instance of EmailValidator
to be used for validation, not the factory function. This seems easy to fix, because we know that we create instances of classes for dependency injection using the useClass
recipe.
However, we already added EmailValidator
to the directives
property of our component, which is a provider with the useClass
recipe. We want to make sure that we get the exact same instance of EmailValidator
on our form control, even though, we define a new provider for it. Luckily we have the useExisting
recipe for that. useExisting
defines an alias token for but returns the same instance as the original token:
@Directive({
...
providers: [
{ provide: NG_VALIDATORS, useExisting: EmailValidator, multi: true }
]
})
class EmailValidator {
...
}
Yikes! This won’t work . We’re referencing a token (EmailValidator
) which is undefined at the point we’re using it because the class definition itself happens later in the code. That’s where forwardRef()
comes into play.
import { forwardRef } from '@angular/core';
@Directive({
...
providers: [
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => EmailValidator), multi: true }
]
})
class EmailValidator {
...
}
If you don’t know what forwardRef()
does, you might want to read our article on Forward References in Angular.
Here’s the full code for our custom email validator:
import { Directive, forwardRef } from '@angular/core';
import { NG_VALIDATORS, FormControl } from '@angular/forms';
function validateEmailFactory(emailBlackList: EmailBlackList) {
return (c: FormControl) => {
let EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
return EMAIL_REGEXP.test(c.value) ? null : {
validateEmail: {
valid: false
}
};
};
}
@Directive({
selector: '[validateEmail][ngModel],[validateEmail][formControl]',
providers: [
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => EmailValidator), multi: true }
]
})
export class EmailValidator {
validator: Function;
constructor(emailBlackList: EmailBlackList) {
this.validator = validateEmailFactory(emailBlackList);
}
validate(c: FormControl) {
return this.validator(c);
}
}
You might notice that we’ve extended the selector, so that our validator not only works with ngModel
but also with formControl
directives. If you’re interested in more articles on forms in Angular, we’ve written a couple about template-driven forms and reactive forms.