Angular 6 reactive form validation not working with custom validator

Mr Shantastic picture Mr Shantastic · Oct 3, 2018 · Viewed 13.2k times · Source

So I created a custom validator to validate that a "New Password" field and a "Confirm Password" field match. On the component level, it works great. The error message only shows when the two inputs don't match.

The problem is, I disable the submit button until the form is valid, but when I use this custom validator, the form as a whole, never passes validation. When for form is filled out, all of the component validations pass, but the form validation does not.

What am I missing here?

The custom validator code looks like this:

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

export const matchingInputsValidator: ValidatorFn = (control: FormGroup): ValidationErrors | null => {
  let pass = control.get('NewPassword').value;
  let confirmPass = control.get('ConfirmPassword').value;
  return pass === confirmPass ? { matchingInputs: true } : { matchingInputs: false };
};

I import the custom validator into my component and declare my form controls in the component like this:

expiredPasswordForm: FormGroup;
oldPassword = new FormControl('', Validators.compose([Validators.required, Validators.minLength(4)]));
newPassword = new FormControl('', Validators.compose([Validators.required, Validators.minLength(4)]));
confirmPassword = new FormControl('', Validators.compose([Validators.required, Validators.minLength(4)]));

And in my ngOnInit() lifecycle hook, I create my form group like this:

this.expiredPasswordForm = this.fb.group({
      'OldPassword': this.oldPassword,
      'NewPassword': this.newPassword,
      'ConfirmPassword': this.confirmPassword
    }, { validator: matchingInputsValidator});

And my HTML for my form looks like this:

<form [formGroup]="expiredPasswordForm" ngNoForm action="" method="post" class="form-horizontal">

        <label for="oldPassword" class="col-sm-4 col-md-6 col-lg-6 col-xl-6 control-label" translate>Old Password</label>
        <input [formControl]="oldPassword" type="password" class="form-control" id="oldPassword"
               placeholder="Old Password" name="OldPassword" [ngClass]="{'is-invalid':oldPassword.invalid && (oldPassword.dirty || oldPassword.touched)}">
        <div *ngIf="oldPassword.invalid && (oldPassword.dirty || oldPassword.touched)" class="invalid-feedback">
          <span *ngIf="oldPassword.errors.required">Old Password is required</span>
        </div>

        <label for="newPassword" class="col-sm-4 col-md-6 col-lg-6 col-xl-6 control-label mt-2" translate>New Password</label>
        <input [formControl]="newPassword" type="password" class="form-control" id="newPassword"
               placeholder="New Password" name="NewPassword" [ngClass]="{'is-invalid':newPassword.invalid && (newPassword.dirty || newPassword.touched)}">
        <div *ngIf="newPassword.invalid && (newPassword.dirty || newPassword.touched)" class="invalid-feedback">
          <span *ngIf="newPassword.errors.required">A new password is required</span>
        </div>

        <label for="confirmPassword" class="col-sm-4 col-md-6 col-lg-6 col-xl-6 control-label mt-2" translate>Confirm Password</label>
        <input [formControl]="confirmPassword" type="password" class="form-control" id="confirmPassword"
               placeholder="Confirm Password" name="ConfirmPassword" [ngClass]="{'is-invalid':confirmPassword.invalid && (confirmPassword.dirty || confirmPassword.touched)}">
        <div *ngIf="confirmPassword.invalid && (confirmPassword.dirty || confirmPassword.touched)" class="invalid-feedback">
          <span *ngIf="confirmPassword.errors.required">You must confirm your new password!</span>
        </div>
        <div *ngIf="!expiredPasswordForm.errors?.matchingInputs">
          <span class="form-error-message">Passwords do not match!</span>
        </div>

        <div class="text-center">
          <button [disabled]="!expiredPasswordForm.valid" type="submit" class="btn btn-primary mt-4 mb-2">Update Password</button>
        </div>

      </form>

I hope this is enough information. Why does this work on the component level, but not on the form level? Thank you in advance!

Answer

Guerric P picture Guerric P · Oct 3, 2018

The problem is with your custom validator.

You could have figured it out because it's the only validator that is set on FormGroup level and not on a FormControl

The object returned by a validator is of type ValidationErrors, it's an object containing pairs of keys/values. For every pair, the key represents the name of the error, and the value represents associated data (details to display for example).

So if you want your validator to return no error, you have to return nothing at all:

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

export const matchingInputsValidator: ValidatorFn = (control: FormGroup): ValidationErrors | null => {
   let pass = control.get('NewPassword').value;
   let confirmPass = control.get('ConfirmPassword').value;
   return pass === confirmPass ? null : { matchingInputs: false };
}