Custom Angular Validators with Dependencies
Angular has great support for validating data in forms, both template-driven and and reactive ones, built imperatively with FormGroup
. It is also easy to create own custom validators. However, I did not find it obvious, how to use injected dependencies in non-directive validators for reactive forms.
Let's start with a simple form on an Ionic 2 page as a testbed for such a validator:
<ion-content padding>
<form [formGroup]="form" (ngSubmit)="submit()">
<ion-item>
<ion-input type="text" formControlName="id" placeholder="Enter ID">
</ion-input>
</ion-item>
<button ion-button block type="submit" [disabled]="!form.valid">
Submit
</button>
</form>
</ion-content>
The corresponding page class constructs the FormGroup
and defines the submit
method:
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'page-test',
templateUrl: 'test.html'
})
export class TestPage {
form: FormGroup;
constructor(formBuilder: FormBuilder) {
this.form = formBuilder.group({
id: [ '', Validators.required ]
});
}
submit() {
if (this.form.valid) {
// submit form
}
}
}
The form already uses one of Angular's bundled validators: Validators.required
. It causes the submit button to only be enabled when the input is not empty. A custom validator that can be used the same way is simply a function with ValidatorFn
signature:
validateId(control: AbstractControl): {[key: string]: any} {
if (['1', '2', '3'].find(id => control.value === id) !== undefined) {
return null;
} else {
return { validateId: true };
}
}
This validator can be assigned to a control just like any of the built-in validators:
constructor(formBuilder: FormBuilder) {
this.form = formBuilder.group({
id: ['', [ Validators.required, this.validateId ] ]
});
}
The submit button will now only be enabled when one of the listed ids is entered. Of course, we don't want to have the list of valid ids hard-coded in the form. It makes much more sense to get it from a provider:
validateId(control: AbstractControl): {[key: string]: any} {
if (this.data.getIds().find(id => control.value === id) !== undefined) {
return null;
} else {
return { validateId: true };
}
}
In the above code, data
is a provider that gets injected into the constructor:
constructor(formBuilder: FormBuilder, private data: Data) {
this.form = formBuilder.group({
id: ['', [ Validators.required, this.validateId ] ]
});
}
However, this won't work. Our validator will throw an exception when invoked:
TypeError: Cannot read property 'data' of undefined
at Page1.validateId (http://localhost:8100/build/main.js:84620:17)
Obviously, the value of this
is undefined
and not the instance of TestPage
, as required for the code to work correctly. To preserve the correct context, a lambda function must be used when assigning the template to the control:
constructor(formBuilder: FormBuilder, private data: Data) {
this.form = formBuilder.group({
id: ['', [ Validators.required, (control) => this.validateId(control) ] ]
});
}
Alternatively, bind
method can be used instead:
constructor(formBuilder: FormBuilder, private data: Data) {
this.form = formBuilder.group({
id: ['', [ Validators.required, this.validateId.bind(this) ] ]
});
}