Configurable Custom Form Validators
New content! replaced by JavaScript
Learning Objectives
-
Know how to create advanced configurable custom validators for both model-driven and template-driven forms.
Configurable Model-Driven Validators
In the previous lecture we created a custom email validator which checked that emails ended in a certain domain, a domain which we hardcoded in the validator.
What if we wanted to make the domain name configurable, so the validator can work for other domains?
We solve this by turning our validator function into a factory function. The factory function returns a validator function configured as we want, like so:
Copyfunction emailDomainValidatorFactory(requiredDomain) { (1)
return function (control: FormControl) { (2)
let email = control.value;
if (email && email.indexOf("@") != -1) {
let [_, domain] = email.split("@");
if (domain !== requiredDomain) { (3)
return {
emailDomain: {
valid: false,
parsedDomain: domain
}
}
}
}
return null;
}
}
1 | We pass into our factory function the domain we want to limit emails to. |
2 | We return from our factory function the actual validator function. |
3 | We use the passed in requiredDomain to check emails against instead of a hardcoded string. |
We can then use this in our FormControl like so:
Copythis.email = new FormControl('', [
Validators.required,
Validators.pattern("[^ @]*@[^ @]*"),
emailDomainValidatorFactory('codecraft.tv')
]);
To match the
Copyclass CodeCraftValidators {
static emailDomain(requiredDomain) {
return function (control: FormControl) {
let email = control.value;
if (email && email.indexOf("@") != -1) {
let [_, domain] = email.split("@");
if (domain !== requiredDomain) {
return {
emailDomain: {
valid: false,
parsedDomain: domain
}
}
}
}
return null;
}
}
}
Then we can use the validator in our form control like so:
Copythis.email = new FormControl('', [
Validators.required,
Validators.pattern("[^ @]*@[^ @]*"),
CodeCraftValidators.emailDomain('codecraft.tv')
]);
Configurable Template-Driven Validators
To configure our directive validator we need to:
-
Provide the required domain name to the DI framework.
-
Inject the required domain name into our directive constructor.
-
Use the factory function to create a configured validator function.
-
Provide this configured validator function to the directives providers.
Numbers 1 & 2 are quite simple. For numbers 3 & 4 we need to take advantage of another fact. A validator as well as being a function can also be a class with a member function called validate
. So we can actually turn our directives class into a validator class.
Firstly letβs provide the configurable domain name in our NgModule
like so:
Copy@NgModule({
.
.
.
providers: [
{provide: 'RequiredDomain', useValue: 'codecraft.tv'}
]
})
Then letβs update our directive so that it uses both the provided RequiredDomain
and the CodeCraftValidators.emailDomain
factory function.
Copyclass EmailDomainValidator {
private valFn = ValidatorFn;
constructor(@Inject('RequiredDomain') requiredDomain: string) { (1)
this.valFn = CodeCraftValidators.emailDomain(requiredDomain) (2)
}
validate(control: FormControl) { (3)
return this.valFn(control);
}
}
1 | We inject into the constructor the RequiredDomain token that we configured on the NgModule . |
2 | We use the CodeCraftValidators.emailDomain factory function to create a configured validator function and store that locally. |
3 | Now whenever validate is called we simply call our configured validator function. |
We also need to change the directives provider so it now points to using the class instead of the validator function we used previously:
Copy@Directive({
selector: '[emailDomain][ngModel]',
providers: [
{
provide: NG_VALIDATORS,
useClass: EmailDomainValidator, (1)
multi: true
}
]
})
1 | This provider now provides an instance of the EmailDomainValidator class instead. |
The validation error messages were also hardcoded to show codecraft.tv
we need to make them show the required domain instead.
We can simply return the required domain in the error object and then use that in our error message, like so:
Copyclass CodeCraftValidators {
static emailDomain(requiredDomain) {
return function (control: FormControl) {
console.log("here");
let email = control.value;
if (email && email.indexOf("@") != -1) {
let [_, domain] = email.split("@");
if (domain !== requiredDomain) {
return {
emailDomain: {
parsedDomain: domain,
requiredDomain: requiredDomain (1)
}
}
}
}
return null;
}
}
}
1 | Pass the requiredDomain back in the error object. |
Then we just just the requiredDomain
variable in our validation message:
Copy<p *ngIf="email.errors.emailDomain">Email must be on the {{ email.errors.emailDomain.requiredDomain }} domain</p>
Now if we change the domain name in the global provider like so:
Copy@NgModule({
providers: [
{provide: 'RequiredDomain', useValue: 'example.com'}
]
})
The email domain validator now check against this new domain instead:

Bindable Template-Driven Validators
We can also configure out template validator directive via property binding in the template instead, like so:
Copy<input type="email"
class="form-control"
name="email"
[(ngModel)]="model.email"
required
pattern="[^ @]*@[^ @]*"
[emailDomain]='codecraft.tv' (1)
#email="ngModel">
1 | We can configure the validator via template property binding. |
Then we update our EmailDomainValidator
class so it can take the required domain as an input:
Copyclass EmailDomainValidator {
@Input('emailDomain') emailDomain: string; (1)
private valFn = Validators.nullValidator;
ngOnChanges(): void { (2)
if (this.emailDomain) {
this.valFn = CodeCraftValidators.emailDomain(this.emailDomain)
} else {
this.valFn = Validators.nullValidator;
}
}
validate(control: FormControl) {
return this.valFn(control);
}
}
1 | First we create an input property on our directive named the same as our selector. |
2 | We know the emailDomain input property has been set when the ngOnChanges lifecycle function is called, this is where we now initialise our directive with the configured validator function. |
Our directive needs one more change in order to function, we are providing this as a class, we need to provide as an alias so we get exactly the same instance
provided to the validators.
Copy@Directive({
selector: '[emailDomain][ngModel]',
providers: [
{
provide: NG_VALIDATORS,
useExisting: EmailDomainValidator,
multi: true
}
]
})
Now we have a template-driven form validator which can be configured via input property binding.
Summary
For model-driven forms we use a factory function which returns a validator function configured as we want.
For template-driven forms we create a validator class, often
Model-Driven Listing
Copyimport {
NgModule,
Component,
Pipe,
OnInit
} from '@angular/core';
import {
ReactiveFormsModule,
FormsModule,
FormGroup,
FormControl,
Validators,
FormBuilder
} from '@angular/forms';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
// Basic hardcoded validator function
//
function emailDomainValidator(control: FormControl) {
let email = control.value;
if (email && email.indexOf("@") != -1) {
let [_, domain] = email.split("@");
if (domain !== "codecraft.tv") {
return {
emailDomain: {
parsedDomain: domain
}
}
}
}
return null;
}
// Configurable validator function
//
class CodeCraftValidators {
static emailDomain(requiredDomain) {
return function (control: FormControl) {
let email = control.value;
if (email && email.indexOf("@") != -1) {
let [_, domain] = email.split("@");
if (domain !== requiredDomain) {
return {
emailDomain: {
parsedDomain: domain,
requiredDomain: requiredDomain
}
}
}
}
return null;
}
}
}
@Component({
selector: 'model-form',
template: `<form novalidate
[formGroup]="myform">
<fieldset formGroupName="name">
<div class="form-group"
[ngClass]="{
'has-danger': firstName.invalid && (firstName.dirty || firstName.touched),
'has-success': firstName.valid && (firstName.dirty || firstName.touched)
}">
<label>First Name</label>
<input type="text"
class="form-control"
formControlName="firstName"
required>
<div class="form-control-feedback"
*ngIf="firstName.errors && (firstName.dirty || firstName.touched)">
<p *ngIf="firstName.errors.required">Last Name is required</p>
</div>
</div>
<div class="form-group"
[ngClass]="{
'has-danger': lastName.invalid && (lastName.dirty || lastName.touched),
'has-success': lastName.valid && (lastName.dirty || lastName.touched)
}">
<label>Last Name</label>
<input type="text"
class="form-control"
formControlName="lastName"
required>
<div class="form-control-feedback"
*ngIf="lastName.errors && (lastName.dirty || lastName.touched)">
<p *ngIf="lastName.errors.required">Last Name is required</p>
</div>
</div>
</fieldset>
<div class="form-group"
[ngClass]="{
'has-danger': email.invalid && (email.dirty || email.touched),
'has-success': email.valid && (email.dirty || email.touched)
}">
<label>Email</label>
<input type="email"
class="form-control"
formControlName="email"
required>
<div class="form-control-feedback"
*ngIf="email.errors && (email.dirty || email.touched)">
<p *ngIf="email.errors.required">Email is required</p>
<p *ngIf="email.errors.pattern">The email address must contain at least the @ character</p>
<!--<p *ngIf="email.errors.emailDomain">Email must be on the codecraft.tv domain</p>-->
<p *ngIf="email.errors.emailDomain">Email must be on the {{ email.errors.emailDomain.requiredDomain }} domain</p>
</div>
</div>
<div class="form-group"
[ngClass]="{
'has-danger': password.invalid && (password.dirty || password.touched),
'has-success': password.valid && (password.dirty || password.touched)
}">
<label>Password</label>
<input type="password"
class="form-control"
formControlName="password"
required>
<div class="form-control-feedback"
*ngIf="password.errors && (password.dirty || password.touched)">
<p *ngIf="password.errors.required">Password is required</p>
<p *ngIf="password.errors.minlength">Password must be 8 characters long, we need another {{password.errors.minlength.requiredLength - password.errors.minlength.actualLength}} characters </p>
</div>
</div>
<div class="form-group"
[ngClass]="{
'has-danger': language.invalid && (language.dirty || language.touched),
'has-success': language.valid && (language.dirty || language.touched)
}">
<label>Language</label>
<select class="form-control"
formControlName="language">
<option value="">Please select a language</option>
<option *ngFor="let lang of langs"
[value]="lang">{{lang}}
</option>
</select>
</div>
<pre>{{myform.value | json}}</pre>
</form>`
})
class ModelFormComponent implements OnInit {
langs: string[] = [
'English',
'French',
'German',
];
myform: FormGroup;
firstName: FormControl;
lastName: FormControl;
email: FormControl;
password: FormControl;
language: FormControl;
ngOnInit() {
this.createFormControls();
this.createForm();
}
createFormControls() {
this.firstName = new FormControl('', Validators.required);
this.lastName = new FormControl('', Validators.required);
this.email = new FormControl('', [
Validators.required,
Validators.pattern("[^ @]*@[^ @]*"),
CodeCraftValidators.emailDomain('codecraft.tv')
]);
this.password = new FormControl('', [
Validators.required,
Validators.minLength(8)
]);
this.language = new FormControl('');
}
createForm() {
this.myform = new FormGroup({
name: new FormGroup({
firstName: this.firstName,
lastName: this.lastName,
}),
email: this.email,
password: this.password,
language: this.language
});
}
}
@Component({
selector: 'app',
template: `<model-form></model-form>`
})
class AppComponent {
}
@NgModule({
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule],
declarations: [
AppComponent,
ModelFormComponent
],
bootstrap: [
AppComponent
]
})
class AppModule {
}
platformBrowserDynamic().bootstrapModule(AppModule);
Template-Driven Listing
Copyimport {
NgModule,
Component,
OnInit,
ViewChild,
Directive,
Inject,
Input,
} from '@angular/core';
import {
NG_VALIDATORS,
FormsModule,
FormGroup,
FormControl,
ValidatorFn,
Validators
} from '@angular/forms';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
class Signup {
constructor(public firstName: string = '',
public lastName: string = '',
public email: string = '',
public password: string = '',
public language: string = '') {
}
}
// Basic hardcoded validator function
//
function emailDomainValidator(control: FormControl) {
let email = control.value;
if (email && email.indexOf("@") != -1) {
let [_, domain] = email.split("@");
if (domain !== "codecraft.tv") {
return {
emailDomain: {
parsedDomain: domain
}
}
}
}
return null;
}
// Configurable validator function
//
class CodeCraftValidators {
static emailDomain(requiredDomain) {
return function (control: FormControl) {
let email = control.value;
if (email && email.indexOf("@") != -1) {
let [_, domain] = email.split("@");
if (domain !== requiredDomain) {
return {
emailDomain: {
parsedDomain: domain,
requiredDomain: requiredDomain
}
}
}
}
return null;
}
}
}
// Basic hardcoded directive
//
// @Directive({
// selector: '[emailDomain][ngModel]',
// providers: [
// {
// provide: NG_VALIDATORS,
// useValue: emailDomainValidator,
// multi: true
// }
// ]
// })
// class EmailDomainValidator {
// }
// Directive configured via DI
//
// @Directive({
// selector: '[emailDomain][ngModel]',
// providers: [
// {
// provide: NG_VALIDATORS,
// useClass: EmailDomainValidator,
// multi: true
// }
// ]
// })
// class EmailDomainValidator {
//
// private valFn = ValidatorFn;
//
// constructor(@Inject('RequiredDomain') requiredDomain: string) {
// this.valFn = CodeCraftValidators.emailDomain(requiredDomain)
// }
//
// validate(control: FormControl) {
// return this.valFn(control);
// }
// }
// Directive configured via input property binding
//
@Directive({
selector: '[emailDomain][ngModel]',
providers: [
{
provide: NG_VALIDATORS,
useExisting: EmailDomainValidator,
multi: true
}
]
})
class EmailDomainValidator {
@Input('emailDomain') emailDomain: string;
private valFn = Validators.nullValidator;
ngOnChanges(): void {
if (this.emailDomain) {
this.valFn = CodeCraftValidators.emailDomain(this.emailDomain)
} else {
this.valFn = Validators.nullValidator;
}
}
validate(control: FormControl) {
return this.valFn(control);
}
}
@Component({
selector: 'template-form',
template: `<!--suppress ALL -->
<form novalidate
(ngSubmit)="onSubmit()"
#f="ngForm">
<fieldset ngModelGroup="name">
<div class="form-group"
[ngClass]="{
'has-danger': firstName.invalid && (firstName.dirty || firstName.touched),
'has-success': firstName.valid && (firstName.dirty || firstName.touched)
}">
<label>First Name</label>
<input type="text"
class="form-control"
name="firstName"
[(ngModel)]="model.firstName"
required
#firstName="ngModel">
<div class="form-control-feedback"
*ngIf="firstName.errors && (firstName.dirty || firstName.touched)">
<p *ngIf="firstName.errors.required">First name is required</p>
</div>
</div>
<div class="form-group"
[ngClass]="{
'has-danger': lastName.invalid && (lastName.dirty || lastName.touched),
'has-success': lastName.valid && (lastName.dirty || lastName.touched)
}">
<label>Last Name</label>
<input type="text"
class="form-control"
name="lastName"
[(ngModel)]="model.lastName"
required
#lastName="ngModel">
<div class="form-control-feedback"
*ngIf="lastName.errors && (lastName.dirty || lastName.touched)">
<p *ngIf="lastName.errors.required">Last name is required</p>
</div>
</div>
</fieldset>
<div class="form-group"
[ngClass]="{
'has-danger': email.invalid && (email.dirty || email.touched),
'has-success': email.valid && (email.dirty || email.touched)
}">
<label>Email</label>
<input type="email"
class="form-control"
name="email"
[(ngModel)]="model.email"
required
pattern="[^ @]*@[^ @]*"
[emailDomain]="'codecraft.tv'"
#email="ngModel">
<div class="form-control-feedback"
*ngIf="email.errors && (email.dirty || email.touched)">
<p *ngIf="email.errors.required">Email is required</p>
<p *ngIf="email.errors.pattern">Email must contain at least the @ character</p>
<!--<p *ngIf="email.errors.emailDomain">Email must be on the codecraft.tv domain</p>-->
<p *ngIf="email.errors.emailDomain">Email must be on the {{ email.errors.emailDomain.requiredDomain }} domain</p>
</div>
</div>
<div class="form-group"
[ngClass]="{
'has-danger': password.invalid && (password.dirty || password.touched),
'has-success': password.valid && (password.dirty || password.touched)
}">
<label>Password</label>
<input type="password"
class="form-control"
name="password"
[(ngModel)]="model.password"
required
minlength="8"
#password="ngModel">
<div class="form-control-feedback"
*ngIf="password.errors && (password.dirty || password.touched)">
<p *ngIf="password.errors.required">Password is required</p>
<p *ngIf="password.errors.minlength">Password must be at least 8 characters long</p>
</div>
</div>
<div class="form-group">
<label>Language</label>
<select class="form-control"
name="language"
[(ngModel)]="model.language">
<option value="">Please select a language</option>
<option *ngFor="let lang of langs"
[value]="lang">{{lang}}
</option>
</select>
</div>
<button type="submit"
class="btn btn-primary"
[disabled]="f.invalid">Submit
</button>
<pre>{{f.value | json}}</pre>
</form>
`
})
class TemplateFormComponent {
model: Signup = new Signup();
@ViewChild('f') form: any;
langs: string[] = [
'English',
'French',
'German',
];
onSubmit() {
if (this.form.valid) {
console.log("Form Submitted!");
this.form.reset();
}
}
}
@Component({
selector: 'app',
template: `<template-form></template-form>`
})
class AppComponent {
}
@NgModule({
imports: [
BrowserModule,
FormsModule
],
declarations: [
AppComponent,
TemplateFormComponent,
EmailDomainValidator
],
bootstrap: [
AppComponent
],
providers: [
{provide: 'RequiredDomain', useValue: 'example.com'}
]
})
class AppModule {
}
platformBrowserDynamic().bootstrapModule(AppModule);
Caught a mistake or want to contribute to the book? Edit this page on GitHub!

Advanced JavaScript
This unique course teaches you advanced JavaScript knowledge through a series of interview questions. Bring your JavaScript to the 2021's today.
Copy[π²,π³,π΄].push(π²)If you find my courses useful, please consider planting a tree on my behalf to combat climate change. Just $4.50 will pay for 25 trees to be planted in my name. Plant a tree!