Angular5 + custom form validator

Keywords: Javascript angular Mobile

Angular5 + custom form validator

Custom Validators

Labels (space separated): Angular

First of all, the following problems are described:

  • How to implement the verification of "reenter password" (equal value of two controller s) (equalTo)
  • How to listen in the reverse direction (first enter "enter password again", then enter the setting password)

Solution:

  • The first problem is to get the specified controller s through [AbstractControl].root.get([targetName]) and then compare their values.
  • Second, it can be implemented through [target].setErrors([errors]).
  1. This is my custom form validation:
import {AbstractControl, FormGroup, ValidatorFn} from '@angular/forms';
import {G} from '../services/data-store.service';

export class MyValidators {
  private static isEmptyInputValue(value) {
    // we don't check for string here so it also works with arrays
    return value == null || value.length === 0;
  }
  private static isEmptyObject(obj) {
    if (typeof obj === 'object' && typeof obj.length !== 'number') {
      return Object.keys(obj).length === 0;
    }
    return null;
  }

  /**
   * Equal to the value of the specified controller
   * @param targetName formControlName of the target
   * @returns {(ctrl: FormControl) => {equalTo: {valid: boolean}}}
   */
  static equalTo(targetName: string): ValidatorFn {
    return (control: AbstractControl): {[key: string]: any} | null => {
      const target = control.root.get(targetName);
      if (target === null) {
        return null;
      }
      if (this.isEmptyInputValue(control.value)) {
        return null;
      }
      return target.value === control.value ? null : {'equalto': { valid: false }};
    };
  }

  /**
   * Reverse input listening specifies whether the controller is equal to the current value
   * @param targetName
   */
  static equalFor(targetName: string): ValidatorFn {
    return (control: AbstractControl): {[key: string]: any} | null => {
      const target = control.root.get(targetName);
      if (target === null) {
        return null;
      }
      if (this.isEmptyInputValue(control.value)) {
        return null;
      }
      if (target.value === control.value) {
        const errors = target.errors;
        delete errors['equalto'];

        if (this.isEmptyObject(errors)) {
          target.setErrors(null);
        } else {
          target.setErrors(errors);
        }
        return null;
      }
      target.setErrors({ 'equalto': { valid: false } });
    };
  }

  ...
}

(Note:) among them, G.REGEX and so on are global variables.

  1. Then FormBuilder implements:
import { Component, OnInit } from '@angular/core';
import {EventsService} from '../../../services/events.service';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import {G} from '../../../services/data-store.service';
import {fade} from '../../../animations/fade.animation';
import {MyValidators} from '../../../directives/my-validators.directive';

@Component({
  selector: 'app-sign-up',
  templateUrl: './sign-up.component.html',
  styleUrls: ['./sign-up.component.scss'],
  animations: [fade]
})
export class SignUpComponent implements OnInit {
  signForm: FormGroup; // Form group
  submitting: boolean; // Can I submit
  validations = G.VALIDATIONS;

  constructor(private eventsService: EventsService, private formBuilder: FormBuilder) {
    this.submitting = false;

    // 
    this.init();
  }

  ngOnInit() {
    // Set parent component title
    this.eventsService.publish('setSign', { title: 'register', subTitle: { name: 'Login immediately', uri: '/account/sign-in' } });
  }

  // Immediate registration
  onSubmit() {
    console.log(this.signForm.getRawValue());
  }

  // Form initialization
  private init() {
    this.signForm = this.formBuilder.group({
      username: ['', Validators.compose([Validators.required, Validators.maxLength(this.validations.USR_MAX)])],
      password: ['', Validators.compose([
        Validators.required,
        Validators.minLength(this.validations.PASS_MIN),
        Validators.maxLength(this.validations.PASS_MAX),
        MyValidators.equalFor('passwordConfirm')
      ])],
      passwordConfirm: ['', Validators.compose([
        Validators.required,
        Validators.minLength(this.validations.PASS_MIN),
        Validators.maxLength(this.validations.PASS_MAX),
        MyValidators.equalTo('password')
      ])]
    });
  }
}

(Note:) where fade animation effect.

  1. Then in the html template, display the form validation prompt information:
<form [formGroup]="signForm" (ngSubmit)="onSubmit()" class="sign-form" @fade>

  <!-- Account number -->
  <div class="input-group username">
    <span class="addon prev"><i class="civ civ-i-usr"></i></span>
    <input type="text"
      name="username"
      class="form-control form-control-left default"
      placeholder="Please enter your account number"
      formControlName="username"
      autocomplete="off">
    <ul class="errors" *ngIf="signForm.get('username').invalid && (signForm.get('username').dirty || signForm.get('username').touched)">
      <li *ngIf="signForm.get('username').hasError('required')" class="error">
        Please enter your account number!
      </li>
      <li *ngIf="signForm.get('username').hasError('maxlength')" class="error">
        Account no more than{{ validations.USR_MAX }}position!
      </li>
    </ul>
  </div> <!-- /.Account number -->
  
  <!-- Password -->
  <div class="input-group password">
    <span class="addon prev"><i class="civ civ-i-lock"></i></span>
    <input type="password"
      name="password"
      class="form-control form-control-left default"
      placeholder="Please input a password"
      formControlName="password">
    <ul class="errors" *ngIf="signForm.get('password').invalid && (signForm.get('password').dirty || signForm.get('password').touched)">
      <li *ngIf="signForm.get('password').hasError('required')" class="error">
        Please enter your password!
      </li>
      <li *ngIf="signForm.get('password').hasError('minlength')" class="error">
        Please enter at least{{ validations.PASS_MIN }}Password for digits!
      </li>
      <li *ngIf="signForm.get('password').hasError('maxlength')" class="error">
        Password no more than{{ validations.PASS_MAX }}position!
      </li>
    </ul>
  </div> <!-- /.Password -->
  
  <!-- Repeat password -->
  <div class="input-group password-confirm">
    <span class="addon prev"><i class="civ civ-i-lock"></i></span>
    <input type="password"
           name="passwordConfirm"
           class="form-control form-control-left default"
           placeholder="Please enter the password again"
           formControlName="passwordConfirm">
    <ul class="errors" *ngIf="signForm.get('passwordConfirm').invalid && (signForm.get('passwordConfirm').dirty || signForm.get('passwordConfirm').touched)">
      <li *ngIf="signForm.get('passwordConfirm').hasError('required')" class="error">
        Please enter the password again!
      </li>
      <li *ngIf="signForm.get('passwordConfirm').hasError('minlength')" class="error">
        Please enter at least{{ validations.PASS_MIN }}Password for digits!
      </li>
      <li *ngIf="signForm.get('passwordConfirm').hasError('maxlength')" class="error">
        Password no more than{{ validations.PASS_MAX }}position!
      </li>
      <li *ngIf="!signForm.get('passwordConfirm').hasError('maxlength') && !signForm.get('passwordConfirm').hasError('minlength') && signForm.get('passwordConfirm').hasError('equalto')" class="error">
        Two password entries are inconsistent!
      </li>
    </ul>
  </div> <!-- /.Repeat password -->
  
  <!-- Submit button -->
  <button type="submit"
          class="btn btn-primary btn-block submit"
          [disabled]="submitting || signForm.invalid">Immediate registration</button>
  <!-- /.Submit button -->
  
</form>

Finally, we can see that the desired effect is achieved:






(attachment:) complete custom form verifier:

import {AbstractControl, FormGroup, ValidatorFn} from '@angular/forms';
import {G} from '../services/data-store.service';

export class MyValidators {
  private static isEmptyInputValue(value) {
    // we don't check for string here so it also works with arrays
    return value == null || value.length === 0;
  }
  private static isEmptyObject(obj) {
    if (typeof obj === 'object' && typeof obj.length !== 'number') {
      return Object.keys(obj).length === 0;
    }
    return null;
  }

  /**
   * Equal to the value of the specified controller
   * @param targetName formControlName of the target
   * @returns {(ctrl: FormControl) => {equalTo: {valid: boolean}}}
   */
  static equalTo(targetName: string): ValidatorFn {
    return (control: AbstractControl): {[key: string]: any} | null => {
      const target = control.root.get(targetName);
      if (target === null) {
        return null;
      }
      if (this.isEmptyInputValue(control.value)) {
        return null;
      }
      return target.value === control.value ? null : {'equalto': { valid: false }};
    };
  }

  /**
   * Reverse input listening specifies whether the controller is equal to the current value
   * @param targetName
   */
  static equalFor(targetName: string): ValidatorFn {
    return (control: AbstractControl): {[key: string]: any} | null => {
      const target = control.root.get(targetName);
      if (target === null) {
        return null;
      }
      if (this.isEmptyInputValue(control.value)) {
        return null;
      }
      if (target.value === control.value) {
        const errors = target.errors;
        delete errors['equalto'];

        if (this.isEmptyObject(errors)) {
          target.setErrors(null);
        } else {
          target.setErrors(errors);
        }
        return null;
      }
      target.setErrors({ 'equalto': { valid: false } });
    };
  }

  /**
   * Verify phone number
   * @returns {(ctrl: FormControl) => {mobile: {valid: boolean}}}
   */
  static get mobile() {
    return (control: AbstractControl) => {
      if (this.isEmptyInputValue(control.value)) {
        return null;
      }

      const valid = G.REGEX.MOBILE.test(control.value);

      return valid ? null : {
        'mobile': {
          valid: false
        }
      };
    };
  }

  /**
   * Verification of identity card
   * @returns {(ctrl: FormControl) => {idCard: {valid: boolean}}}
   */
  static get idCard() {
    return (control: AbstractControl) => {
      if (this.isEmptyInputValue(control.value)) {
        return null;
      }

      const valid = G.REGEX.ID_CARD.test(control.value);

      return valid ? null : {
        'idcard': {
          valid: false
        }
      };
    };
  }

  /**
   * Verifying Chinese characters
   * @returns {(ctrl: FormControl) => {cn: {valid: boolean}}}
   */
  static get cn() {
    return (control: AbstractControl) => {
      if (this.isEmptyInputValue(control.value)) {
        return null;
      }

      const valid = G.REGEX.CN.test(control.value);

      return valid ? null : {
        'cn': {
          valid: false
        }
      };
    };
  }

  /**
   * Number of numbers specified
   * @param {number} length
   * @returns {(ctrl: FormControl) => (null | {number: {valid: boolean}})}
   */
  static number(length: number = 6) {
    return (control: AbstractControl) => {
      if (this.isEmptyInputValue(control.value)) {
        return null;
      }

      const valid = new RegExp(`^\\d{${length}}$`).test(control.value);

      return valid ? null : {
        'number': {
          valid: false
        }
      };
    };
  }

  /**
   * Strong password (must contain alphanumeric)
   * @returns {(ctrl: FormControl) => (null | {number: {valid: boolean}})}
   */
  static get strictPass() {
    return (control: AbstractControl) => {
      if (this.isEmptyInputValue(control.value)) {
        return null;
      }

      const valid = G.REGEX.STRICT_PASS.test(control.value);

      return valid ? null : {
        'strictpass': {
          valid: false
        }
      };
    };
  }
}

Posted by mcccy005 on Sat, 28 Dec 2019 06:51:27 -0800