Angular 6 Custom Validation Directive with Tooltip

Angular 6 Custom Validation Directive with Tooltip

In this post, we are going to see how to create a custom validator directive in Angular 6. Let's just create a new custom directive 'validation-label.directive.ts' to handle validation of reactive form manually using focus, focusOut, click events.

Folder Structure

|- app/
    |- api.service.ts
    |- app.component.html
    |- app.component.css
    |- app.component.ts
    |- app.module.ts
    |- validation-label.directive.ts
    |- validation-msg.service.ts
    |- register/
         |- register.component.css
         |- register.component.ts
         |- register.component.html
|- index.html
|- styles.css
|- tsconfig.json

app.component.html

<!--The content below is only a placeholder and can be replaced.-->
<div class="globalError" *ngIf="isGlobalError">
    <p>To register, please fill up <a href?="javascript:void(0);" (click)="goToField()">{{errorsObj.length}}</a> fields.</p>
</div>
<app-register (formErrorsCount)='formErrorsEvent($event)'></app-register>


app.component.ts

import { Component, ElementRef } from '@angular/core';


@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'app';
  errorsObj: any;
  isGlobalError = false;
  globalErrorTimer: any;


  constructor(private el: ElementRef) {
  }


  formErrorsEvent(evt) {
    this.errorsObj = evt;
    console.log('formErrorsArr => ', this.errorsObj);
    if (this.errorsObj.length > 0) {
      this.isGlobalError = true;
      window.scrollTo(0, 0);
      this.globalErrorTimer = setTimeout( () => {
        console.log(this.el.nativeElement.querySelector('.globalError'));
        this.el.nativeElement.querySelector('.globalError').focus();
      }, 0);
    }
  }


  goToField() {
    console.log(this.el.nativeElement);
    this.el.nativeElement.querySelector('#' + this.errorsObj[0].fieldName).focus();
  }


}

formErrorsEvent function is used to fetch the errors list object from register page and count the fields which will be display in app.component.html file. Click on the erroscount and goToField() will be fired to move focus to the first field of errors list object that is firstname.

app.service.ts

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';


@Injectable({
  providedIn: 'root'
})
export class ApiService {


  constructor(
    private http: HttpClient
  ) { }


/**
   * Get Form content
  */
 getFormContent() {
  // tslint:disable-next-line:no-shadowed-variable
 const promise = new Promise((resolve, reject) => {
   const apiURL = 'https://localhost:4200/assets/formcontent.json';
   return this.http.get<{frcontent: any}>(apiURL).toPromise().then(
     res => {
       resolve(res);
     },
     msg => {
       reject(msg);
     }
   );
 });
 return promise;
}


   /**
   * Get Field Info message
  */
 getFieldInfoMessage() {
  // tslint:disable-next-line:no-shadowed-variable
 const promise = new Promise((resolve, reject) => {
   const apiURL = 'https://localhost:4200/assets/fieldInfoContent.json';
   return this.http.get<{vlderrors: any}>(apiURL).toPromise().then(
     res => {
       resolve(res);
     },
     msg => {
       reject(msg);
     }
   );
 });
 return promise;
}


}

register.component.html

<div class="loader-bg" *ngIf="isLoading">
  <div class="loader"></div>
</div>


<div class="container" *ngIf="!isLoading">
  <div class="col-xl-8 offset-xl-2 col-md-12 registerBox">
    <h3 class="text-center">{{title}}</h3>
    <form [formGroup]="registerForm" (ngSubmit)="verifyForm()">
      <div class="row form-group clearfix">
            <div class="col-md-4 col-12">
              <label for="firstName">First Name</label>
              <input type="text" appValidationLabel
              id="{{formContent?.controls?.firstName}}"
              [formControlName]="formContent?.controls?.firstName"
              [formErrorsArr]="formErrorsArr"
              [fieldInfo]="fieldInfoMsgArr?.firstName"
              (formErrorsCount)='formErrorsEvent($event)'
              class="form-control" autofocus />
            </div>
            <div class="col-md-4 col-12">
              <label for="middleName">Middle Name</label>
                <input type="text" appValidationLabel
                id="{{formContent?.controls?.middleName}}"
                [formControlName]="formContent?.controls?.middleName"
                [formErrorsArr]="formErrorsArr"
                [fieldInfo]="fieldInfoMsgArr?.middleName"
                (formErrorsCount)='formErrorsEvent($event)'
                class="form-control" />
            </div>
            <div class="col-md-4 col-12">
              <label for="lastName">Last Name</label>
                <input type="text" appValidationLabel
                id="{{formContent?.controls?.lastName}}"
                [formControlName]="formContent?.controls?.lastName"
                [formErrorsArr]="formErrorsArr"
                [fieldInfo]="fieldInfoMsgArr?.lastName"
                (formErrorsCount)='formErrorsEvent($event)'
                class="form-control" />
            </div>
      </div>      
      <div class="row form-group clearfix">
          <div class="col-md-4 col-12">
            <label for="emailId">Email ID</label>
              <input type="text" appValidationLabel
              id="{{formContent?.controls?.emailId}}"
              [formControlName]="formContent?.controls?.emailId"
              [formErrorsArr]="formErrorsArr"
              [fieldInfo]="fieldInfoMsgArr?.emailId"
              (formErrorsCount)='formErrorsEvent($event)'
              class="form-control" />
          </div>    
          <div class="col-md-4 col-12">
            <label for="mobile">Mobile No</label>
            <input type="text" appValidationLabel
            id="{{formContent?.controls?.mobile}}"
            [formControlName]="formContent?.controls?.mobile"
            [formErrorsArr]="formErrorsArr"
            [fieldInfo]="fieldInfoMsgArr?.mobile"
            (formErrorsCount)='formErrorsEvent($event)'
            class="form-control" />
          </div> 
          <div class="col-md-4 col-12">
            <label for="umail">Password</label>
            <input type="password" appValidationLabel
            id="{{formContent?.controls?.password}}"
            [formControlName]="formContent?.controls?.password"
            [formErrorsArr]="formErrorsArr"
            [fieldInfo]="fieldInfoMsgArr?.password"
            (formErrorsCount)='formErrorsEvent($event)'
            class="form-control" />
          </div>  
      </div>
      <div class="row form-group clearfix">
        <div class="col-12 text-center">
            <button class="btn btn-primary" type="submit" appFormSubmit>{{formContent?.btnName}}</button>
        </div>
      </div>  
    </form>
  </div>
</div>

I am using this appValidationLabel directive for all input fields. I have used multiple input fields formControlName, formErrorsArr, fieldInfo to fetch value from register.component.html to directive. This kind of attributes ([formErrorsArr], [formControlName]) you can any value from HTML file to relative directive and @Input decorator will be used to get the value.

register.component.ts

import { Component, OnInit, ElementRef, EventEmitter, Output, Renderer2 } from '@angular/core';
import { ApiService } from '../api.service';
import { ValidationMessageService } from '../validation-msg.service';
import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms';


@Component({
  selector: 'app-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.css']
})
export class RegisterComponent implements OnInit {
  isLoading: Boolean = true;
  registerForm: FormGroup;
  formContent = {};
  formErrorsArr = [];
  title: String;
  fieldInfoMsgArr = [];
  errors: any;
  isFieldExit = false;
  @Output() formErrorsCount: EventEmitter<any> = new EventEmitter();


  constructor(
    private formBuilder: FormBuilder,
    private apiService: ApiService,
    private validErrorMsgService: ValidationMessageService,
    private el: ElementRef,
    private ren: Renderer2
  ) {
  }


  ngOnInit() {
    this.title = 'Register';
    this.formContentFunc();
    this.validationErrorMsg();
  }


  formContentFunc() {
    this.apiService.getFormContent().then(
      (res) => {
        console.log(res);
        if (res !== undefined || res !== null) {
          this.formContent = res;
          this.isLoading = false;
          this.createForm();
        }
      }, (error) => {
        console.log(error);
        this.isLoading = false;
      });
  }


  createForm() {
    this.registerForm = this.formBuilder.group({
      firstName: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(30)]],
      middleName: ['', [Validators.minLength(2), Validators.maxLength(30)]],
      lastName: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(30)]],
      emailId: ['', [Validators.required, Validators.email]],
      mobile: ['', [Validators.required, Validators.pattern('^[0-9]*$'), Validators.minLength(10), , Validators.maxLength(10)]],
      password: ['', [Validators.required, Validators.minLength(6)]]
    });
  }


  /*
  *** Get API response as validation error json and load response in validationErrorObj of validErrorMsgService
  */
 validationErrorMsg() {
  this.apiService.getFieldInfoMessage().then(
    (res) => {
      if (this.validErrorMsgService.validationErrorObj.length === 0) {
        this.fieldInfoMsgArr = res['fieldInfo'];
        console.log('Field Info Array => ', this.fieldInfoMsgArr);
        this.isLoading = false;
      }
    }, (error) => {
      console.log(error);
      this.isLoading = false;
    });
  }


  formErrorsEvent(evt) {
    this.errors = evt;
  }


  verifyForm() {
    const cloneErrors = this.errors;
    Object.keys(this.registerForm.controls).forEach(key => {
      if (this.registerForm.get(key).invalid) {
        this.el.nativeElement.querySelector('#' + key).classList.add('errorfield');
        this.el.nativeElement.querySelector('#' + key).parentElement.querySelector('label').classList.add('errorlabel');
        const obj = {fieldName: key};
        if (cloneErrors.length > 0) {
          for (let i = 0; i < cloneErrors.length; i++) {
            if (cloneErrors[i].fieldName === key) {
              this.isFieldExit = true;
            }
          }
        }
        if (!this.isFieldExit) {
          this.errors.push(obj);
        }
      }
      this.isFieldExit = false;
    });
    this.formErrorsCount.emit(this.errors);
  }


}

this.registerForm.controls will give you all required fields list object. Using forEach(key), i am adding a errorfield class to invalid input property and a errorlabel class to label of that field. I am sending this errors object to app component using formErrorsCount emit function to count the error fields in app component.

getFieldInfoMessage function is used to get error information from a JSON file (fieldInfoContent.json) which is kept in assets folder.

validation-label.directive.ts

import { Directive, Input, Output, HostListener, ElementRef, Renderer2, EventEmitter, OnInit,
  OnChanges, OnDestroy, SimpleChange } from '@angular/core';
import { NgControl, ValidationErrors } from '@angular/forms';
import { Subscription } from 'rxjs';
import { ValidationMessageService } from './validation-msg.service';


@Directive({
  selector: '[appValidationLabel]'
})
export class ValidationLabelDirective implements OnInit, OnChanges {
  @Input() formControlName;
  @Input() formErrorsArr: any[];
  @Input() fieldInfo: string;
  @Output() formErrorsCount: EventEmitter<any> = new EventEmitter();
  fieldAlreadyExit: Boolean;
  allFields = [];



  constructor(
    private control: NgControl,
    private elr: ElementRef,
    private ren: Renderer2,
    private validationMsgService: ValidationMessageService
  ) { }


  errorSpanId = '';
  statusChangeSubscription: Subscription;


  ngOnInit(): void {
    this.fieldAlreadyExit = false;
  }


  ngOnChanges() {
    // if (changes.formErrorsArr) {
    //   console.log(changes.formErrorsArr);
    // }
  }


  @HostListener('focus', ['$event.target'])
  onFocus(target) {


  }


  @HostListener('click', ['$event.target'])
  onClickFocus(target) {
    this.creteErrorInfoTooltip();
  }


  @HostListener('focusout', ['$event.target'])
  onFocusOut(target) {
    if (this.control.touched && this.control.invalid) {
      if (this.formErrorsArr.length !== 0) {
        for (let i = 0; i < this.formErrorsArr.length; i++) {
          if (this.formErrorsArr[i].fieldName === this.control.name) {
            this.fieldAlreadyExit = true;
          }
        }
      }
      if (!this.fieldAlreadyExit) {
        const errorObj = {fieldName: this.control.name};
        this.formErrorsArr.push(errorObj);
      }
      this.addErrorClass();
    } else {
      for (let i = 0; i < this.formErrorsArr.length; i++) {
        if (this.formErrorsArr[i].fieldName === this.control.name) {
          this.formErrorsArr.splice(i, 1);
          this.removeErrorClass();
        }
      }
    }
    console.log('this.formErrorsArr => ', this.formErrorsArr);
    this.formErrorsCount.emit(this.formErrorsArr);
    this.removeErrorInfoTooltip();
  }


  creteErrorInfoTooltip() {
    const parentElem = this.elr.nativeElement.parentElement;
    const errorDiv = this.ren.createElement('div');
    const text = this.ren.createText(this.fieldInfo);
    this.ren.addClass(errorDiv, 'fieldInfo');
    this.ren.appendChild(errorDiv, text);
    this.ren.appendChild(parentElem, errorDiv);
  }


  removeErrorInfoTooltip() {
    const parentElem = this.elr.nativeElement.parentElement;
    const errorDiv = this.elr.nativeElement.parentElement.querySelector('.fieldInfo');
    console.log(errorDiv);
    if (errorDiv !== null) {
      this.ren.removeChild(parentElem, errorDiv);
    }
  }


  addErrorClass() {
      const fieldElement = this.elr.nativeElement;
      this.ren.addClass(fieldElement, 'errorfield');
      const labelElement = this.elr.nativeElement.parentElement.querySelector('label');
      this.ren.addClass(labelElement, 'errorlabel');
  }


  removeErrorClass() {
      const fieldElement = this.elr.nativeElement;
      this.ren.removeClass(fieldElement, 'errorfield');
      const labelElement = this.elr.nativeElement.parentElement.querySelector('label');
      this.ren.removeClass(labelElement, 'errorlabel');
  }


}

@HostListener is used listen to the DOM events like focus, focusout, click in a directive. onfocus event, this.control is used to check the current input is valid or not. Suppose, focus is currently now in firsname field, now you have press tab and focus is moved to middle name, Then focusOut function will be fired and this.control, which is now previous focus field that is firsname. So this.control (firstname) will be checked, is valid or not. If not valid then addErrorClass function will be called and errorfield class will be added in input field(firstname) when errorlabel class will be added in label respective input field(firstname). Now you have fill up the firsname field and focous out with pressing TAB key. Then removeErrorClass function will be called to remove errorfield and errorlabel class from respective input field and label.

validation-msg.service.ts

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


@Injectable({
  providedIn: 'root'
})


export class ValidationMessageService {


  validationErrorObj = [];


  public getValidationMsg(validationId: string): string {
    return this.validationErrorObj[validationId];
  }


}



Source Code : https://github.com/piyalidas10/validation-tooltip-directive

Live URL : https://piyalidas10.github.io/validation-tooltip-directive/


Rahman (Raymond) Parwez

Senior Software Engineer

3 年

Great post, One quick question, if I don't want the errors to be fetched from an API or filedInfoContent.JSON how do I modify the code in the register component? I am kind of new to Angular, could you please paste the code here? I would really appreciate it. Thanks

回复

要查看或添加评论,请登录

Piyali Das的更多文章

  • Accessible Bootstrap Dropdown Navigation

    Accessible Bootstrap Dropdown Navigation

    The Focus Focus refers to what element in your application (such as a field, checkbox, button, or link) currently…

  • Front-end development using Gulp

    Front-end development using Gulp

    Gulp is a JavaScript toolkit which helps you in implementing multiple front end tasks during web development, it can be…

  • Angular Dynamic Context Menu

    Angular Dynamic Context Menu

    "contextmenu" is an event like click, mouseup, mousemove, mousedown. This "contextmenu" event is typically triggered by…

    2 条评论
  • Right-click Context Menu In JavaScript

    Right-click Context Menu In JavaScript

    "contextmenu" is an event like click, mouseup, mousemove, mousedown. This "contextmenu" event is typically triggered by…

  • Dynamic Height in Angular Application

    Dynamic Height in Angular Application

    In this tutorial, i have explained how you an dynamically set height of a div of component depending on other div of…

  • Code Optimization Advantage with Example

    Code Optimization Advantage with Example

    Dynamic Font Change non-optimize code HTML code…

  • Advantages of Angular Library in Architectural Design

    Advantages of Angular Library in Architectural Design

    Application Strategic Design Decompose the big application into smaller libraries Smaller libraries are easily…

  • Angular Multi Application Project Creation

    Angular Multi Application Project Creation

    If your installed Angular global version is different and you want to create different version Angular application…

    1 条评论
  • Tree shaking vs. Non tree shaking providers in Angular

    Tree shaking vs. Non tree shaking providers in Angular

    In our example we are importing and referencing our Service in the AppModule causing an explicit dependency that cannot…

  • Angular Port Change

    Angular Port Change

    3 steps to change port in Angular Change in Angular.json Change in script of package.

    2 条评论

社区洞察

其他会员也浏览了