One of the biggest strengths of Angular is its’ forms library for handling forms logic. Even though Angular has some built-in form validators as required, it sometimes is necessary to create your own form validators.
Template driven and reactive forms
In Angular, there are two form modules: template driven and reactive.
The template-driven allows you to specify the form logic in the template and reactive allows you to write your form as typescript code in the template. This means that creating custom validators for template driven forms is gonna be slightly different from reactive forms. The difference is basically that you are gonna wrap the reactive custom validator in a directive to make it work with template driven forms. If you are using template driven forms I recommend coding your custom validators in a way that they are also compatible with reactive forms, should you want to use that.
Creating a custom validator for reactive forms
Creating a custom validator for reactive forms is actually more simple than for a template driven form. You only need to implement ValidatorFn, which takes a form control and returns an error object.
A date validator can be created as:
export function invalidDateValidatorFn(): ValidatorFn { return (control: AbstractControl): { [key: string]: any } => { const date = new Date(control.value); const invalidDate = !control.value || date.getMonth === undefined; return invalidDate ? { 'invalidDate': { value: control.value } } : null; }; }
Here we are validating if the input can be converted to a date and if not, we return an error object with “invalidDate” set + the invalid value, this when can be used to display an error message to the user.
This validator is hooked up to a reactive form like this:
this.form = this.formBuilder.group({ title: this.formBuilder.control('', Validators.required), description: this.formBuilder.control('', Validators.required), dueDate: this.formBuilder.control('', Validators.required, invalidDateValidatorFn), });
Creating a custom validator for template driven forms
As said before, when creating a custom validator for a template driven form, you should have created the validator fn first, which is used seperately if it was in a reactive form:
export function invalidDateValidatorFn(): ValidatorFn { return (control: AbstractControl): { [key: string]: any } => { const date = new Date(control.value); const invalidDate = !control.value || date.getMonth === undefined; return invalidDate ? { 'invalidDate': { value: control.value } } : null; }; } @Directive({ selector: '[appInvalidDate]', providers: [{ provide: NG_VALIDATORS, useExisting: InvalidDateValidatorDirective, multi: true }] }) export class InvalidDateValidatorDirective implements Validator { // tslint:disable-next-line:no-input-rename @Input('appInvalidDate') public invalidDate: string; public validate(control: AbstractControl): { [key: string]: any } { return this.invalidDate ? invalidDateValidatorFn()(control) : null; } }
For using a validator in a template-driven form we hook it in with a directive. Notice that we bind to an attribute with [] in the selector. The way we hook it into Angular template driven forms by adding the directive to Angular’s NG_VALIDATORS using the multi option. NG_VALIDATORS is a provider Angular is using on every form change to loop through the validators in the form and update the form’s validity.
A validator directive implements Validator from @angular/forms which contain a validate callback which is called by Angular forms module when it iterates on all directives hooked into NG_VALIDATORS.
Input to a validator can be done with an Input validator that matches the selectors name.
A bizarre trick for creating a flexible custom validator
Alright, enough of the affiliate marketing…
I have found it could become tedious for doing all the above process for really simple validation logic, so for that reason, I came up with a custom validator directive that evaluates boolean expressions. For complex validation logic I would like to encapsulate validation logic, like in the above example, but if it is really simple boolean expressions I preferer to use the flexible custom validator, as it saves you from doing the above steps for every validator.
The flexible custom validator looks like this:
export class CustomValidator { constructor(public expression: () => boolean, public validatorName: string) {} } export function customValidatorFnFactory( customValidator: CustomValidator ): ValidatorFn { return function(control: AbstractControl) { const errorObj = {}; errorObj[customValidator.validatorName] = true; return customValidator.expression() ? null : errorObj; }; } @Directive({ selector: '[appCustomValidator]', providers: [ { provide: NG_VALIDATORS, useExisting: CustomValidatorDirective, multi: true } ] }) export class CustomValidatorDirective implements Validator { private _customValidator: CustomValidator; public get appCustomValidator(): CustomValidator { return this._customValidator; } @Input() public set appCustomValidator(customValidator: CustomValidator) { this._customValidator = customValidator; if (this._onChange) { this._onChange(); } } private _onChange: () => void; constructor() {} public validate(control: AbstractControl): { [key: string]: any } { return customValidatorFnFactory(this.appCustomValidator)(control); }https://christianlydemann.com/wp-admin/post.php?post=174&action=edit# public registerOnValidatorChange?(fn: () => void): void { this._onChange = fn; } }
As we saw before we are creating a validationFn that is used in a directive. The directive takes as input a CustomValidator object which contains a boolean express, that is gonna be evaluated, a validator name, used in the error object.
When running the validators, you can show an error message in your template like this:
<div class="form-group"> <label for="todo-description">Description</label> <input type="text" #todoDescriptionInput="ngModel" [appCustomValidator]="getLengthCustomValidator(todoDescriptionInput.value)" required name="todo-description" [(ngModel)]="currentTODO.description" class="form-control" id="todo-description" placeholder="Enter description"> </div> <div *ngIf="todoDescriptionInput.touched && todoDescriptionInput.errors" class="alert alert-danger" role="alert"> Error </div>
Here we are applying the custom validator in a template by passing a custom validator object containing an expression for validating the length of the input as well as the validator name used for showing validation messages:
public getLengthCustomValidator = (value: string) => new CustomValidator( () => value.length < MAX_DESCRIPTION_LENGTH, 'minLengthValidator' )
I’m using bootstrap here for the styling.
Upon a validation error you can show something like this to a user:
Read the code for validators and more in my Angular best practices repository on Github.
Do you want to become an Angular architect? Check out Angular Architect Accelerator.