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]).
- 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.
- 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.
- 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 } }; }; } }