Custom Angular Validators with Dependencies

April 21st 2017 Angular Ionic 2/3 TypeScript

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) ] ]
  });
}

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License