Thursday, November 19, 2020

Custom Validator in Angular Template-Driven Form

Angular framework provides many built-in validators that can be used with forms but sometimes you may need a validation in your application that can’t be catered by a built-in validators. For such scenario Angular also gives the option to create a custom validator. In this tutorial we’ll see how to create a custom validator to be used with Angular template-driven form.

Custom validator for Template-Driven form

If you want to write a custom validator for a template-driven form that has to be written as an Angular directive which should implement the Validator interface. This Validator interface is implemented by classes that perform synchronous validation.

interface Validator {
  validate(control: AbstractControl): ValidationErrors | null
  registerOnValidatorChange(fn: () => void)?: void
}

Here validate() is the method that performs synchronous validation against the provided control.

registerOnValidatorChange()- Registers a callback function to call when the validator inputs change.

In the FormControl with in the template when you specify the implemented Directive as your custom validator, Angular will automatically invoke the validate() method whenever the value of the bound FormControl is changed.

Angular Custom validator Template-Driven form example

In this custom validator example we’ll create an Angular template-driven form to capture membership details which has a field ‘membership date’. We want to validate that the membership date is not less than the current date. To check that we’ll write a custom validator.

Initial form state

custom validator in template-driven form

Data model (member.model.ts)

Member class defines the data model reflected in the form, we’ll bind values from the form to an object of this Member class.

export class Member {
  name: string;
  mail: string;
  membershipDate: Date;
  membershipType: string;
  constructor(name: string, mail: string, membershipDate: Date, membershipType: string) {
    this.name = name;
    this.mail = mail;
    this.membershipDate  = membershipDate;
    this.membershipType = membershipType;
  }
}

Directive as custom validator (datevalidator.directive.ts)

import { Directive } from '@angular/core';
import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator } from '@angular/forms';
import { CustomValidatorService } from './services/customvalidator.service';

@Directive({
  selector: '[inputdatevalidator]',
  providers: [{provide: NG_VALIDATORS, useExisting: DateValidatorDirective, multi: true}]
})
export class DateValidatorDirective implements Validator{
  constructor(private customvalidatorService: CustomValidatorService){}
  validate(control: AbstractControl): {[key: string]: any} | null {
    let inDate:string = control.value;
    if(!this.customvalidatorService.dateValidator(inDate)){
      return {invalidDate: true};
    }
    return null;
  }
}

Some important points to note here-

  1. Since it is a Directive that is specified by using the @Directive decorator.
  2. With in the providers array first element is provide: NG_VALIDATORS which registers the directive with the NG_VALIDATORS provider. That’s how Angular knows that this directive has to be used as a validator. NG_VALIDATORS is a predefined provider with an extensible collection of validators.
  3. Second element useExisting: DateValidatorDirective instructs that an existing instance of the directive has to be used rather than creating a new instance. if you use useClass: DateValidatorDirective, then you’d be registering a new class instance.
  4. By using Third element multi: true you can register many initializers using the same token. In this context you can register a custom form validator using the built-in NG_VALIDATORS token.
  5. This directive is used as a custom validator that means it has to be added as an attribute in a form control in the template. That attribute is added using the selector specified here.
  6. Though you can write the logic for validation with in the validate method here but better to create a separate Angular Service class where you can add all of the required custom validators and then call them. Here it is done like that by creating a Service class and the service class in injected in the Directive in the constructor.
  7. If validation fails validate() method returns ValidationErrors otherwise null. ValidationErrors is a map of validation errors having key, value pair.
  8. In our validate method {invalidDate: true} key,value pair is returned if validation fails.

Service class (customvalidator.service.ts)

import { Injectable } from '@angular/core';

@Injectable({  
  providedIn: 'root'  
})  
export class CustomValidatorService{
  dateValidator(inDate: string): boolean{
    let curDate = new Date();
    // standardize date
    curDate = new Date(Date.UTC(curDate.getUTCFullYear(), curDate.getUTCMonth(), curDate.getUTCDate()));
    
    let inputDate = new Date(inDate);
    inputDate = new Date(Date.UTC(inputDate.getUTCFullYear(), inputDate.getUTCMonth(), inputDate.getUTCDate()));

    if(curDate > inputDate){
      return false;
    }
    return true;
  }
}

From the Directive when you call the dateValidator() method of the Service class you pass the value of the FormControl as a String. Since you need to compare the current date to the input date so that string value is converted to Date and then compared with the current date. You return false or true depending on whether current date is greater than input date or not.

Component class (app.compnent.ts)

Since it is a template-driven form so not much is there in the Component class.

import { Component, ViewChild } from '@angular/core';
import { AbstractControl, FormControl, NgForm } from '@angular/forms';
import { Member } from './member.model';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent {
  @ViewChild('membershipForm') memberForm : NgForm;
  membershiptypes = ['Silver', 'Gold', 'Platinum'];
  currentDate = new Date();
  member = new Member('', '', new Date(), '');
  submitted = false;

  onSubmit(){
    this.submitted = true;
    this.member.name = this.memberForm.value.name;
    this.member.mail = this.memberForm.value.email;
    this.member.membershipDate = this.memberForm.value.mdate;
    this.member.membershipType = this.memberForm.value.type; 
  }
}

Template (app.component.html)

<div class="container">
  <div class="row">
    <div class="col-xs-12 col-sm-10 col-md-8">
      <h1>Membership Form</h1>
      <form (ngSubmit)="onSubmit()" #membershipForm="ngForm">
        <div class="form-group">
          <label for="name">Name</label>
          <input type="text" class="form-control" 
            id="name"
            ngModel name="name" #name="ngModel" required>     
            <div class="alert alert-danger" *ngIf="name.invalid && name.touched">Name is required</div>                  
        </div>
          
        <div class="form-group">
          <label for="email">email</label>
          <input type="email" class="form-control" 
              id="email" required email
              ngModel name="email" #email="ngModel"> 
              <div class="alert alert-danger" *ngIf="email.invalid && email.touched">
                Please enter a valid email
              </div>                       
        </div>
        <div class="form-group">
          <label for="mdate">Membership Date</label>
          <input type="date" class="form-control" id="mdate" inputdatevalidator
            [ngModel]="currentDate | date:'yyyy-MM-dd'" name="mdate" #mdate="ngModel">      
            <div class="alert alert-danger" *ngIf="mdate.hasError('invalidDate') && mdate.touched">
              Membership date should be equal to or greater than current date
            </div>                     
        </div>
        <div class="form-group">
          <label for="type">Membership Type</label>
          <select class="form-control" id="type"                    
              ngModel name="type" required #type="ngModel">
            <option *ngFor="let mtype of membershiptypes" [value]="mtype">{{mtype}}</option>
          </select>
          <div class="alert alert-danger" *ngIf="type.invalid && type.touched">
              Please select membership type
          </div> 
        </div>
        <button type="submit" [disabled]="membershipForm.invalid" class="btn btn-success">Submit</button>
      </form> 
    </div>
  </div>
  <hr>
  <div *ngIf="submitted">
    <div class="row">
      <div class="col-xs-12 col-sm-10 col-md-8">
        <p>Name: {{member.name}}</p>
        <p>email: {{member.mail}}</p>
        <p>Membership Date: {{member.membershipDate | date:'dd/MM/yyyy'}}</p>
        <p>Membership Type: {{member.membershipType}}</p>
      </div>
    </div>
  </div>  
</div>

In the control for MembershipDate, ‘inputdatevalidator’ is added to trigger the custom validation. You check if there is any validation error by passing the key ('invalidDate') which is returned from the custom validator in hasError() method.

<div class="form-group">
  <label for="mdate">Membership Date</label>
  <input type="date" class="form-control" id="mdate" inputdatevalidator
    [ngModel]="currentDate | date:'yyyy-MM-dd'" name="mdate" #mdate="ngModel">      
    <div class="alert alert-danger" *ngIf="mdate.hasError('invalidDate') && mdate.touched">
      Membership date should be equal to or greater than current date
    </div>                     
</div>

CSS class (src/assets/forms.css)

You will also need to add a CSS class to show the bars on the left side of the controls.

.ng-invalid.ng-untouched:not(form)  {
  border-left: 5px solid #42A948; /* green */
}

.ng-invalid.ng-touched:not(form)  {
  border-left: 5px solid #a94442; /* red */
}

In the index.html file, update the <head> tag to include the new style sheet.

<link rel="stylesheet" href="assets/forms.css">

Adding Directive to app module

You need to import and declare the created Directive and Service in the app module.

@NgModule({
  declarations: [
    AppComponent,
    DateValidatorDirective
  ],
  imports: [
    BrowserModule,
    FormsModule,
  ],
  providers: [CustomValidatorService],
  bootstrap: [AppComponent]
})
export class AppModule { }
Custom Validator Angular Template-Driven Form

That's all for this topic Custom Validator in Angular Template-Driven Form. If you have any doubt or any suggestions to make please drop a comment. Thanks!

>>>Return to Angular Tutorial Page


Related Topics

  1. Angular Template-Driven Form Validation Example
  2. Angular Reactive Form Validation Example
  3. FormGroup in Angular With Examples
  4. FormArray in Angular With Example
  5. Angular Form setValue() and patchValue()

You may also like-

  1. Angular - Call One Service From Another
  2. Angular Property Binding With Examples
  3. Angular ngStyle Directive With Examples
  4. Location Strategies in Angular Routing
  5. Character Streams in Java IO
  6. Interface Default Methods in Java
  7. How to Compile Java Program at Runtime
  8. Converting Text File to Parquet File Using Hadoop MapReduce

No comments:

Post a Comment