Angular2 Custom Input Validation with Template-driven Forms

I began playing with Angular2.  I built a form with an input.  I want the input to be numeric and for its value to be within a certain range: min <= x <= max.

I already knew how to do this with Angular1, but Angular2 has changed everything.  There are two ways of building Angular2 forms: template-driven and model-driven.  I chose template-driven since my app will not have a lot of forms and I wanted maximum flexibility to customize my form.

At the time of this writing, Angular2 provides only three basic validators, required, minLength and maxLength.  I chose to write two custom validators, one called min-value and one called max-value, each of which takes a single parameter.  Here is a demo implementation that illustrates the mechanics.

The first gotcha is that you remember to provide disableDeprecatedForms() and provideForms() in your call to bootstrap().
bootstrap(App, [
    disableDeprecatedForms(),
    provideForms()
  ])
Next, when you import Typescript classes for forms, you have to remember to import them from 'angular/forms', not 'angular/common'.  For example:
import NgFrom from '@angular/forms'
not
import NgForm form '@angular/common'
Doing it the wrong way will mean that you are still getting the old deprecated form behavior and your validator will not get added to the collection for that input.

The magic sauce comes here, in app.ts in my demo:
@Directive({
  selector: '[ngModel][duration], [formControl][duration]',
  providers: [
    {
      provide: NG_VALIDATORS,
      useClass: DurationDirective,
      multi: true,
    }
  ]
})
export class DurationDirective implements Validator{
    validate(c:FormControl) {
        console.error('called validate()')
        return parseInt(c.value) < 10 ? null : {
          duration: {message: 'Please enter a number < 10'}
        };
    }
}
To understand what is really going on, you may need to go over the Angular2 documentation about Dependency Injection (DI).

Here, the providers property is assigned an array of providers (in this case an array of one item) defined by an anonymous object.  This will be used by the Angular2 provider factory to generate a new provider.  NG_VALIDATORS already returns the validators for the directives required, minLength, and maxLength.  Setting multi: true tells Angular you are adding DurationDirective to the NG_VALIDATORS token instead of redefining it.

Angular2 will invoke each validator assigned to an input by calling the validate() method of the Validator interface on the provider it created.

If we also provided our directive with some sort of parameter, such as:
duration="10"
this will work because useClass tells the provider factory to generate a new validator object for each instance of our directive it encounters.

In this example, our directive has no such parameter, so we could replace useClass with useExisting.  This is telling Angular2 to use the same instance of our validator for each duration directive.  If we had a lot of these in our page, this would make it slightly smaller and faster.  I saved it for last, since I thought it took me longer to understand it, and therefore, I wanted to save it for last to avoid confusion.

Simple?  Hardly!  Custom input evaluation actually looks a bit easier with model-driven forms.  Nevertheless, if you are going to use template-driven forms with Angular2 and you need some input validation that you want to integrate into the Angular2 DOM evaluation cycle, this is how you do it.



Update: In my project that uses custom form validation, unlike the Plunkr demo, I had to assign the  useExisting property and not the useClass property or the validator would not get invoked.  I have not yet found the reason for this.

Comments

Popular Posts