3
votes

I'm working to authenticate a user with Angular Material. I'm currently trying to get the proper mat-error to display when the confirmation password doesn't match the first entered pw.

Here is my html:

 <mat-form-field hintLabel="Minimum 8 Characters" class="">
                            <mat-label>Password</mat-label>
                            <input 
                            matInput 
                            #input 
                            type="password"
                            formControlName="password">
                            <mat-hint align="end">{{input.value?.length || 0}}/8</mat-hint>
                        </mat-form-field>

                        <mat-form-field>
                            <mat-label>Confirm</mat-label>
                            <input 
                            matInput
                            required  
                            type="password"
                            #confirm
                            formControlName="confirmPassword">
                            <mat-error *ngIf="form.get('confirmPassword').invalid || confirmPasswordMismatch">Password does not match</mat-error>
                            </mat-form-field>

The error displays once the user has focused on password confirmation and unfocuses without entering anything. Once the user enters anything, the error goes away even though the confirmation doesn't match the password.

Here is my TS file:

public get confirmPasswordMismatch() {
        return (this.form.get('password').dirty || this.form.get('confirmPassword').dirty) && this.form.hasError('confirmedDoesNotMatch');
    }

this.form = new FormGroup({
            userName: new FormControl(null, [Validators.required]),
            fullName: new FormControl(null, [Validators.required]),
            email: new FormControl(null, [Validators.required, Validators.pattern(this.EMAIL_REGEX)]),
            password: new FormControl(null),
            confirmPassword: new FormControl(null, ),
        }, (form: FormGroup) => passwordValidator.validate(form));

The desired effect is that the error shows when the user has entered text into pw input when confirm pw is empty and to show an error when both have text but confirm doesn't match pw.

4

4 Answers

11
votes

I solved it like this:

Template:

  <mat-form-field>
    <input matInput type="password" placeholder="Password" formControlName="password" (input)="onPasswordInput()">
    <mat-error *ngIf="password.hasError('required')">Password is required</mat-error>
    <mat-error *ngIf="password.hasError('minlength')">Password must have at least {{minPw}} characters</mat-error>
  </mat-form-field>

  <mat-form-field>
    <input matInput type="password" placeholder="Confirm password" formControlName="password2" (input)="onPasswordInput()">
    <mat-error *ngIf="password2.hasError('required')">Please confirm your password</mat-error>
    <mat-error *ngIf="password2.invalid && !password2.hasError('required')">Passwords don't match</mat-error>
  </mat-form-field>

Component:

export class SignUpFormComponent implements OnInit {

  minPw = 8;
  formGroup: FormGroup;

  constructor(private formBuilder: FormBuilder) { }

  ngOnInit() {
    this.formGroup = this.formBuilder.group({
      password: ['', [Validators.required, Validators.minLength(this.minPw)]],
      password2: ['', [Validators.required]]
    }, {validator: passwordMatchValidator});
  }

  /* Shorthands for form controls (used from within template) */
  get password() { return this.formGroup.get('password'); }
  get password2() { return this.formGroup.get('password2'); }

  /* Called on each input in either password field */
  onPasswordInput() {
    if (this.formGroup.hasError('passwordMismatch'))
      this.password2.setErrors([{'passwordMismatch': true}]);
    else
      this.password2.setErrors(null);
  }
}

Validator:

export const passwordMatchValidator: ValidatorFn = (formGroup: FormGroup): ValidationErrors | null => {
  if (formGroup.get('password').value === formGroup.get('password2').value)
    return null;
  else
    return {passwordMismatch: true};
};

Notes:

  • Thanks to onPasswordInput() being called from either password field, editing the first password field (and thus invalidating the password confirmation) also causes the mismatch error to be displayed in the password confirmation field.
  • The *ngIf="password2.invalid && !password2.hasError('required')" test for the password confirmation field ensures that never both error messages ("mismatch" and "required") are displayed at the same time.
4
votes

Based on your code, it doesn't look like you added any validation for the confirmPassword field: confirmPassword: new FormControl(null, ) so the only validation happening is via the required attribute. Also, the mat-error will only be displayed by the form field if the form control is invalid. That means you can't force an error to be displayed just by using ngIf. Since you only have required validation on that field, it makes sense that you only have an error when the field is empty. To solve this problem, you need to create a validator for mismatch checking and add it to the confirmPassword form control. As an alternative, you can manually add errors to the form control using setErrors() when the field changes by adding an input listener - for example (this is just from memory - may need fixing):

<mat-form-field>
    <mat-label>Confirm</mat-label>
    <input matInput required type="password" #confirm formControlName="confirmPassword"
        (input)="onInput($event.target.value)">
    <mat-error *ngIf="form.get('confirmPassword').invalid>
        Password does not match
    </mat-error>
</mat-form-field>


onInput(value) {
    if (this.form.hasError('confirmedDoesNotMatch')) { // or some other test of the value
        this.form.get('confirmPassword').setErrors([{'confirmedDoesNotMatch': true}]);
    } else {
        this.form.get('confirmPassword').setErrors(null);
    }
}
2
votes

you are essentially validating how 2 fields in a form interact with each other ("password" and "confirm password" fields). This is known as "cross-field validation"

that means, your custom validator cannot be assigned to just 1 field. The custom validator needs to be assigned to the common parent, the form.

Here is the official documented best practice, for validating how 2 fields in a form interact with each other

https://angular.io/guide/form-validation#cross-field-validation

this code snippet worked for me

template:

<form method="post" [formGroup]="newPasswordForm">
  <input type="password" formControlName="newPassword" />
  <input type="password" formControlName="newPasswordConfirm" />
  <div class="err-msg" *ngIf="newPasswordForm.errors?.passwordMismatch && (newPasswordForm.touched || newPasswordForm.dirty)">
        confirm-password does not match password
      </div>
</form>

component.ts:

export class Component implements OnInit {
    this.newPasswordForm = new FormGroup({
      'newPassword': new FormControl('', [
        Validators.required,
      ]),
      'newPasswordConfirm': new FormControl('', [
        Validators.required
      ])
    }, { validators: passwordMatchValidator });
}

export const passwordMatchValidator: ValidatorFn = (formGroup: FormGroup): ValidationErrors | null => {
  return formGroup.get('newPassword').value === formGroup.get('newPasswordConfirm').value ?
    null : { 'passwordMismatch': true };
}

Note that for passwordMatchValidator, it is outside the component class. It is NOT inside the class

1
votes

Henry's answer was very helpful (and I'm surprised it doesn't have more votes), but I've made a tweak to it so that it plays well with Angular Material controls.

The tweak relies on the fact that the FormGroup class has a parent property. Using this property, you can tie the validation message to the password confirmation field, and then refer up the chain in the validator.

public signUpFormGroup: FormGroup = this.formBuilder.group({
  email: ['', [Validators.required, Validators.pattern(validation.patterns.email)]],
  newPassword: this.formBuilder.group({
    password: ['', [
      Validators.required,
      Validators.minLength(validation.passwordLength.min),
      Validators.maxLength(validation.passwordLength.max)]],
    confirmPassword: ['', [Validators.required, passwordMatchValidator]]
  })
});

The validator looks like this:

export const passwordMatchValidator: ValidatorFn = (formGroup: FormGroup): ValidationErrors | null => {
  const parent = formGroup.parent as FormGroup;
  if (!parent) return null;
  return parent.get('password').value === parent.get('confirmPassword').value ?
    null : { 'mismatch': true };
}

and the form then looks like this:

<div formGroupName="newPassword" class="full-width new-password">
  <mat-form-field class="full-width sign-up-password">
    <mat-label>{{ 'sign-up.password' | translate }}</mat-label>
    <input matInput [type]="toggleSignUpPass.type" [maxlength]="max" [minlength]="min" formControlName="password" required/>
    <mat-pass-toggle-visibility #toggleSignUpPass matSuffix></mat-pass-toggle-visibility>
    <mat-icon matSuffix [color]="color$ | async">lock</mat-icon>
    <mat-hint aria-live="polite">{{ signUpFormGroup.get('newPassword').value.password.length }}
      / {{ max }} </mat-hint>
    <mat-error *ngIf="signUpFormGroup?.get('newPassword')?.controls?.password?.hasError('required')">
      {{ 'sign-up.password-error.required' | translate }}
    </mat-error>
    <mat-error class="password-too-short" *ngIf="signUpFormGroup?.get('newPassword')?.controls?.password?.hasError('minlength')">
      {{ 'sign-up.password-error.min-length' | translate:passwordRestriction.minLength }}
    </mat-error>
    <mat-error *ngIf="signUpFormGroup?.get('newPassword')?.controls?.password?.hasError('maxlength')">
      {{ 'sign-up.password-error.max-length' | translate:passwordRestriction.maxLength }}
    </mat-error>
  </mat-form-field>
  <mat-form-field class="full-width sign-up-confirm-password">
    <mat-label>{{ 'sign-up.confirm-password' | translate }}</mat-label>
    <input matInput [type]="toggleSignUpPassConf.type" formControlName="confirmPassword" required />
    <mat-pass-toggle-visibility #toggleSignUpPassConf matSuffix></mat-pass-toggle-visibility>
    <mat-icon matSuffix [color]="color$ | async">lock</mat-icon>
    <mat-error *ngIf="signUpFormGroup?.get('newPassword')?.controls?.confirmPassword?.hasError('required')">
      {{ 'sign-up.confirm-password-error.required' | translate }}
    </mat-error>
    <mat-error class="password-mismatch" *ngIf="signUpFormGroup?.get('newPassword')?.controls?.confirmPassword?.hasError('mismatch')">
      {{ 'sign-up.password-error.mismatch' | translate }}
    </mat-error>
  </mat-form-field>
</div>