Search code examples
angulartypescriptangular-materialangular-reactive-formsangular-validation

How to make this password match validation work


I have this code to check if both passwords match, however, it's not working as intended. I've tried what I've seen in some older questions but with no success.

I have this in my component's template:

<form [formGroup]="passwordForm">

  <mat-form-field>
    <input #thePassword matInput [type]="hidePasswordButton ? 'password' : 'text'" [formControl]="password"
      (input)="onPasswordInput(thePassword.value)" />

    <mat-label>Enter Password</mat-label>

    <mat-error *ngIf="password.hasError('required')">
      Password is <strong>required</strong>
    </mat-error>

    <mat-error *ngIf="password.hasError('minlength')">
      Must have <strong>at least 8 characters.</strong>
    </mat-error>

    <button mat-icon-button matSuffix (click)="hidePasswordButton = !hidePasswordButton"
      [attr.aria-label]="'Hide password'" [attr.aria-pressed]="hidePasswordButton">
      <mat-icon>{{ hidePasswordButton ? "visibility_off" : "visibility" }}</mat-icon>
    </button>
  </mat-form-field>

  <mat-form-field *ngIf="!isInputDisabled()">
    <input #ConfirmedPassword matInput [type]="hideConfirmedPasswordButton ? 'password' : 'text'"
      [formControl]="confirmedPassword" (input)="onConfirmedPasswordInput(ConfirmedPassword.value)" />

    <mat-label>Confirm Password</mat-label>

    <mat-error *ngIf="passwordForm.errors?.['mismatch']">
      Passwords <strong>don't match</strong>
    </mat-error>

    <mat-error *ngIf="confirmedPassword.hasError('required')">
      <strong>You must confirm your password</strong>
    </mat-error>

    <button mat-icon-button matSuffix (click)="hideConfirmedPasswordButton = !hideConfirmedPasswordButton"
      [attr.aria-label]="'Hide password'" [attr.aria-pressed]="hideConfirmedPasswordButton">
      <mat-icon>{{ hideConfirmedPasswordButton ? "visibility_off" : "visibility" }}</mat-icon>
    </button>
  </mat-form-field>
</form>

Then the ts file is something like this:

import { matchingPasswords } from 'src/types/password-match-validator';

password = new FormControl('', [Validators.required, Validators.minLength(8), Validators.pattern(/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)/)]);

  confirmedPassword = new FormControl('', [Validators.required, Validators.minLength(8)]);

  passwordForm: FormGroup = new FormGroup({
    password: this.password,
    confirmedPassword: this.confirmedPassword
  },
    {
      validators: [matchingPasswords]
    });

The matchingPasswords validator comes from this class:

import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";

export const matchingPasswords : ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
    const password = control.get('password');
    const confirmedPassword = control.get('confirmedPassword');
    return password 
           && confirmedPassword 
           && password.value !== confirmedPassword.value 
           ? { 'mismatch': true } 
           : null;
           
}

This approach kinda works in the sense that if I type something in the password input, say "Abcd1234" and then some random stuff in the confirmation then the mat-error triggers, however, if I type something like "123456aA" in the password input and then something like "123456aB" in the confirmation one the error never shows up even tho both passwords are not exactly the same. At least it doesn't let me submit the form, but I need to show that error message.

I've already tried with the this.fb.group approach, but that didn't work either.


Solution

  • The reason why the error for passwordForm.errors?.['mismatch'] is due to the default behavior of ErrorStateMatcher which shows the error when these conditions are fulfilled:

    1. The control is invalid.
    2. Either the control is touched or the form is submitted.

    The confirmedPassword FormControl is required to show an error other than the error from itself, which is the "mismatch" error from the FormGroup.

    You need to override the default ErrorStateMatcher behavior in order to resolve the issue.

    1. Create a custom ErrorStateMatcher.
    export class ConfirmedPasswordErrorStateMatcher implements ErrorStateMatcher {
      isErrorState(
        control: FormControl | null,
        form: FormGroupDirective | NgForm | null
      ): boolean {
        return !!(
          control &&
          (control.invalid || form?.errors?.['mismatch']) &&
          (control.dirty || control.touched || (form && form.submitted))
        );
      }
    }
    
    1. Declare the ConfirmedPasswordErrorStateMatcher instance in the component.
    confirmedPasswordErrorStateMatcher = new ConfirmedPasswordErrorStateMatcher();
    
    1. Add [errorStateMatcher]="confirmedPasswordErrorStateMatcher" to the confirmedPassword control.
    <input
      #ConfirmedPassword
      matInput
      [type]="hideConfirmedPasswordButton ? 'password' : 'text'"
      [formControl]="confirmedPassword"
      (input)="onConfirmedPasswordInput(ConfirmedPassword.value)"
      [errorStateMatcher]="confirmedPasswordErrorStateMatcher"
    />
    

    Demo @ StackBlitz