Σάββατο 27 Ιουνίου 2020

Angular change password form with validators, error handling and password confirmation










Create a form for changing a password



Verify same password in both inputs

Apply error displays, according to case

Toggle password visibility











1. Component - HTML


<div class="password-reset"> <form role="form" (ngSubmit)="submit()" [formGroup]="changePasswordForm" autocomplete="off" > <div fxLayout="column" fxLayoutAlign="space-between" fxLayoutGap="5.7vw"> <div> <mat-form-field hideRequiredMarker class="full-width"> <mat-label>Password</mat-label> <input matInput name="password" id="password" formControlName="password" [type]="hidePassword ? 'password' : 'text'" > <mat-error *ngIf="doShowHintsForCorrectPassword()"> {{ doDisplayHintsForCorrectPassword() }} </mat-error> <button type="button" mat-icon-button matSuffix (click)="hidePassword = !hidePassword" > <mat-icon>{{ hidePassword ? 'visibility_off' : 'visibility' }}</mat-icon> </button> </mat-form-field> </div> <div> <mat-form-field hideRequiredMarker class="full-width"> <mat-label>Confirm Password</mat-label> <input matInput name="confirmPassword" id="confirmPassword" formControlName="confirmPassword" [type]="hideConfirmPassword ? 'password' : 'text'" > <mat-error *ngIf="changePasswordForm.get('confirmPassword')?.errors?.sameValue"> Confirm password error </mat-error> <button type="button" mat-icon-button matSuffix (click)="hideConfirmPassword = !hideConfirmPassword" > <mat-icon>{{ hideConfirmPassword ? 'visibility_off' : 'visibility' }}</mat-icon> </button> </mat-form-field> </div> <br> </div> <div fxLayoutAlign="space-between" fxLayoutGap="2.7vw"> <div fxFlex="50%"> <button type="submit" mat-raised-button class="full-width" [disabled]="!changePasswordForm.valid" > Save </button> </div> <div fxFlex="50%"> <button type="button" mat-raised-button class="full-width"> Cancel </button> </div> </div> </form> </div>



2. Component TS


import { Component, OnInit } from '@angular/core'; import { FormBuilder, Validators } from '@angular/forms'; import { register } from '../shared/util/component-mapping'; @Component({ selector: 'jhi-test-comp', templateUrl: './test-comp.component.html', styleUrls: ['./test-comp.component.scss'] }) @register export class TestCompComponent implements OnInit { public static COMPONENT_NAME = 'TestCompComponent'; changePasswordForm = this.fb.group({ password: ['', [Validators.required]], confirmPassword: ['', Validators.required] }); hidePassword = true; hideConfirmPassword = true; constructor(private fb: FormBuilder) {} ngOnInit(): void {} submit(): void { this.validateFormBeforeRequest(); if (!this.changePasswordForm.invalid) { // do something } } private validateFormBeforeRequest(): void { this.changePasswordForm.controls['password'].setValidators([ Validators.required, AppValidators.minLength(10), AppValidators.containsCapitalLetter(), AppValidators.containsNumber() ]); this.changePasswordForm.controls['password'].updateValueAndValidity(); this.changePasswordForm.controls['password'].valueChanges.subscribe(() => { this.changePasswordForm.controls['confirmPassword'].updateValueAndValidity(); }); this.changePasswordForm.controls['confirmPassword'].setValidators([ Validators.required, AppValidators.sameValue('password') ]); this.changePasswordForm.controls['confirmPassword'].updateValueAndValidity(); } doShowHintsForCorrectPassword(): boolean { const errors = this.changePasswordForm.controls['password'].errors; if (errors && (errors.minLength || errors.containsNumber || errors.containsCapitalLetter)) { return true; } return false; } doDisplayHintsForCorrectPassword(): string { const errors = this.changePasswordForm.controls['password'].errors; const error = 'Error! '; if (errors) { const hint1 = errors.containsNumber ? 'Should contain numbers' : ''; const hint2 = errors.containsCapitalLetter ? ', Should contain capital letter' : ''; const hint3 = errors.minLength ? ', Too short' : ''; return error + hint1 + hint2 + hint3; } else { return error; } } }



3. Component CSS


.full-width { width: 100%; } .password-reset { height: 100%; padding: 2.7vw; } mat-form-field.mat-form-field { font-size: 2.08vh; line-height: 2.6vh; }




4. app.validators.ts

import { ValidatorFn, AbstractControl } from '@angular/forms'; export class AppValidators { private static satisfiesPattern(value: string, pattern: string): boolean { return new RegExp(pattern).test(value); } static containsNumber(): ValidatorFn { return (control: AbstractControl): { [key: string]: any } | null => { const containsNumber = this.satisfiesPattern(control.value, '\\d'); return !containsNumber ? { containsNumber: { value: control.value } } : null; }; } static containsCapitalLetter(): ValidatorFn { return (control: AbstractControl): { [key: string]: any } | null => { const containsCapitalLetter = this.satisfiesPattern( control.value, '.*[A-Z].*' ); return !containsCapitalLetter ? { containsCapitalLetter: { value: control.value } } : null; }; } static minLength(minLength: number): ValidatorFn { return (control: AbstractControl): { [key: string]: any } | null => { const notEnoughLength = control.value.length < minLength; return notEnoughLength ? { minLength: { requiredLength: minLength, actualLength: control.value.length } } : null; }; } static sameValue(formControlName: string): ValidatorFn { return (control: AbstractControl): { [key: string]: any } | null => { const otherFormControlValue = control.parent.controls[formControlName].value; const notSame = control.value !== otherFormControlValue; return notSame ? { sameValue: { value: control.value, otherValue: otherFormControlValue } } : null; }; } }