Juri Strumpflohner

RSS

Custom validators in template driven Angular forms

Author profile pic
Juri Strumpflohner
Published

Angular version 2+ has two different kind of Forms API, the reactive and template driven approach. In this article we will focus on the template driven approach and learn how to use it as well as how to build a custom validator with it.

Contents are based on Angular version >= 2

Todd Motto recently published a similar article on “Reactive FormGroup validation with AbstractControl in Angular 2”, which you might definitely want to check out for the reactive kind of approach to this.

TL;DR - Egghead.io Video Lesson

Don’t wanna read through the entire article? Then lean back and watch my free Egghead lesson 😉

A simple Angular Form

Ok, before starting, let’s take a look at our simple Angular, template driven form.

<form #form="ngForm" (ngSubmit)="onSubmit(form.value)">
    <div>
        Firstname: 
        <input type="text" ngModel name="firstname" />
    </div>
    <div>
        <button>Submit</button>
        <button (click)="onReset($event, form)">reset</button>
    </div>
</form>

So far so good, binding works. For more details on how to setup your initial form binding, take a look at my article on that topic:

Angular Forms - a first look

Learn about the reactive as well as template driven approach to forms in Angular

Adding built-in validators

We can make use of the built-in HTML5 validators in Angular just as we were able already in AngularJS (Angular 1.x). Behind the scenes, Angular recognizes these validators and integrates with them.

Next we can take a closer look at the input control and add a simple required attribute for making it a required input field.

<form #form="ngForm" (ngSubmit)="onSubmit(form.value)">
   <div>
       Firstname: 
       <input type="text" ngModel name="firstname" required />
   </div>
</form>

Note, starting with Angular v4 there is no need to even add the novalidate attribute on the form which normally disables the built-in HTML 5 validation. If you still want the native validation, add the ngNativeValidate attribute. Thanks @jbandi for the catch!

This makes our input a required field and when it’s not compiled, the according form.valid property will be false. You may be wondering where the form comes from? It’s the template variable I’ve defined and associated with ngForm. This is the way it’s done in the Angular template driven approach.

<form #form="ngForm" (ngSubmit)="form.valid && onSubmit(form.value)">
    ...
</form>

Note that I’m adding form.valid && onSubmit(...) to our (ngSubmit) event. This way our form won’t submit unless it is valid. Obviously we also need to display some kind of message. To do so, we first have to get a reference to our NgModel object that sits behind the <input>. This object is instantiated for us by Angular when we add the ngModel directive to our control. NgModel holds information about the input control such as validation errors.

<div>
    Firstname: 
    <input type="text" ngModel name="firstname" required #firstname="ngModel" />
</div>

Now that we have this reference, we can use it to check for the different kind of validation errors. There are different approaches to visualizing validation errors. Check out the Forms Validation cookbook on the Angular IO site for more details. For now we will simply hard code them in our template.

<div>
    Firstname: 
    <input type="text" ngModel name="firstname" required #firstname="ngModel" />
    <div style="color:red" 
        *ngIf="firstname.errors && (firstname.dirty || firstname.touched || form.submitted)">
        
        <p *ngIf="firstname.errors.required">
            The name is required
        </p>
    </div>
</div>

So what we did here is to add another <div> which contains the validation messages. The *ngIf you see, is there to only visualize our validation errors when our firstname field (pointing to the ngModel instance) actually contains errors (firstname.errors). Moreover, we also only want to only show it when either our fields is dirty, touched or when the form has been submitted.
Within that div, we can then do some more fine-grained checks for the kind of error by accessing the firstname.errors property, in our case the firstname.errors.required.

Building a custom validator

Ok, now that you’ve seen how to add the build-in validators, let’s build the super useful “JuriNameValidator”, a validator that only allows “Juri” to be entered in a textbox. Sounds reasonable, doesn’t it?

So first we need to define and implement our so-called validation factory. Don’t let be scared away, it’s a simple function that builds and returns our validation function. We get an AbstractControl as parameter to our validation function that can be used to access the underlying field value, by using the according value property. If our validation check is valid, we return null, otherwise we return an object with a property juriName containing itself a property valid set to false.

import { AbstractControl, ValidatorFn } from '@angular/forms';

// validation function
function validateJuriNameFactory() : ValidatorFn {
  return (c: AbstractControl) => {
    
    let isValid = c.value === 'Juri';

    if(isValid) {
        return null;
    } else {
        return {
            juriName: {
                valid: false
            }
        };
  }
}

Once we have our validation factory funtion, we need to create a directive which mainly serves to attach our validation function onto an existing HTML input control. An Angular directive is mostly like a component with the main difference that it doesn’t have a template.

@Directive({
  selector: '[juriName][ngModel]'
})
export class JuriNameValidator implements Validator { ... }

We register it on HTML controls having the attribute juriName as well as ngModel. Next, we connect our validation factory function with our directive. Note how in the constructor we call our factory function (which resides in this very same file) and associate it to our local validator variable. Note that here we could pass parameters to our valdiateJuriNameFactory which get passed as @Input to our directive.

function validateJuriNameFactory() : ValidatorFn { ... }

@Directive({...})
export class JuriNameValidator implements Validator {
  validator: ValidatorFn;
  
  constructor() {
    this.validator = validateJuriNameFactory();
  }
  
  validate(c: FormControl) {
    return this.validator(c);
  }
  
}

Great. But who calls the validate(...) function of our directive?? That’s a last missing step we have to do:

import { NG_VALIDATORS, Validator } from '@angular/forms';
...
@Directive({
  selector: '[juriName][ngModel]',
  providers: [
    { provide: NG_VALIDATORS, useExisting: JuriNameValidator, multi: true }
  ]
})
export class JuriNameValidator implements Validator { ... }

Finally we’re now ready to add our validator to our form which is as simple as adding juriName to our input control as well as defining a proper error message.

<div>
    Firstname: 
    <input type="text" 
        ngModel 
        name="firstname" 
        #firstname="ngModel" 
        required 
        juriName />
        
    <div style="color:red" *ngIf="firstname.errors && (firstname.dirty || firstname.touched || form.submitted)">
        <p *ngIf="firstname.errors.required">
            The name is required
        </p>
        <p *ngIf="firstname.errors.juriName">
            Only allowed name is "Juri" ;)
        </p>
    </div>
</div>

Final Code

Here’s the final code in a easy to use Plunk:

Many thanks to Sam Vloeberghs for reviewing this article