4

I wrote a simple custom Validator to compare two password fields and signal an error if they don't match. Here it is:

@Directive({
  selector: '[appMatchPassword]',
  providers: [
    { provide: NG_VALIDATORS, useExisting: MatchPasswordDirective, multi: true }
  ]
})
export class MatchPasswordDirective implements Validator {

  @Input('appMatchPassword') otherControl: string = '';

  constructor() { }

  registerOnValidatorChange(fn: () => void): void {
  }

  validate(control: AbstractControl): ValidationErrors | null {

    const compareTo = control.parent?.get(this.otherControl);
    if (!compareTo) return null;

    if (compareTo?.value !== control.value) return {
      appMatchPassword: true
    }

    return null;
  }

}

The template references it like this:

    <mat-form-field>
      <mat-label>Password</mat-label>
      <input matInput type="password" name="password" [(ngModel)]="user.password"
            required minlength="4" #iPassword="ngModel">
      <mat-error *ngIf="iPassword.errors?.required">Password Required.</mat-error>
      <mat-error *ngIf="iPassword.errors?.minlength">Minimum 4 characters</mat-error>
    </mat-form-field>

    <mat-form-field>
      <mat-label>Verify Password</mat-label>
      <input matInput type="password" name="vpassword" [(ngModel)]="vpassword"
            required appMatchPassword="password" #fvp="ngModel">
      <mat-error *ngIf="fvp.errors?.appMatchPassword">Passwords do not match</mat-error>
    </mat-form-field>

This works as desired if the user puts a value into the password field and a different value in the vpassword field. The error is displayed and goes away if the user corrects the value in the vpassword field.

However, if the user goes back and edits the password field the error does not go away. The validator only gets called after its own AbstractControl changes value.

Is there any good way to trigger the Validator when the password field gets changed?

AlanObject
  • 9,613
  • 19
  • 86
  • 142
  • 1
    Does this answer your question? [Confirm password validation in Angular 6](https://stackoverflow.com/questions/51605737/confirm-password-validation-in-angular-6) – Yaseer Jul 25 '21 at 09:39
  • @Yasser Yes it does. Had I seen that first I wouldn't have posted the question. However I think Aviad's answer below is a bit more up-to date and concise so I will mark that as answer and hopefully it will help the next guy. – AlanObject Jul 25 '21 at 16:09

1 Answers1

4

You have to use cross-field validation and apply your validator directive to the form itself, not to the password field.

If the validator issues an error, it will be present on the form's errors array, not on that of the particular field's.

Additionally, if you want to display the mat-error inside the mat-form-field it will not show because the field itself is not invalid (only the entire form) - so you can either move the mat-error outside, or use a custom error state matcher

In short here's how your directive should look (note that now the control passed to the validate method is the entire form, not a particular field)

validate(control: AbstractControl): ValidationErrors | null {
  const pass = control.get('password')?.value;
  const vpass = control.get('vpassword')?.value;

  console.log(pass,vpass);

  if (pass !== vpass)
    return {
      appMatchPassword: true
    };

  return null;
}

And here's how your template should look:

<!-- the directive is applied to the form -->
<form #frm="ngForm" appMatchPassword>
  <mat-form-field>
    <mat-label>Password</mat-label>
    <input matInput type="password" name="password" [(ngModel)]="user.password"
      required minlength="4" #iPassword="ngModel">
    <mat-error *ngIf="iPassword.errors?.required">Password Required.</mat-error>
    <mat-error *ngIf="iPassword.errors?.minlength">Minimum 4 characters</mat-error>
  </mat-form-field>

  <mat-form-field>
    <mat-label>Verify Password</mat-label>

    <!-- the directive is NOT applied to the particular field -->
    <!--   also note the `matcher` property, see below... -->
    <input matInput type="password" name="vpassword" [(ngModel)]="vpassword" required
           [errorStateMatcher]="matcher">

    <!-- this line changed, it now looks at frm.errors instead of fvp.errors -->
    <mat-error *ngIf="frm.errors?.appMatchPassword">Passwords do not match</mat-error>
  </mat-form-field>
</form>

And here's a dummy error state matcher that simply always shows any mat-error existing in the field:

export class AlwaysShowErrors implements ErrorStateMatcher {
  isErrorState(control: FormControl, form: FormGroupDirective | NgForm): boolean {
    return true;
  }
}

Instantiate it as a matcher field in the component class thus:

matcher = new AlwaysShowErrors();

StackBlitz example

Aviad P.
  • 32,036
  • 14
  • 103
  • 124