angulartypescriptvalidationformgroups

FormGroup confirm password validator doesn't work


I am trying to implement confirm password validator in the register form but doesn't work. I am using Angular 15 in my app.

My code:

import { Component } from '@angular/core';
import { AbstractControl, AbstractControlOptions, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AccountService } from '../account.service';
import { Router } from '@angular/router';

@Component({
  selector: 'app-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.scss']
})
export class RegisterComponent {

  constructor(private fb: FormBuilder, private accountService: AccountService, private router: Router) {}

  complexPassword = "(?=^.{8,20}$)(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()_+}{":;'?/>.<,])(?!.*\s).*$";

  registerForm = this.fb.group({
    displayName: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(50)]],
    email: ['', [Validators.required, Validators.minLength(5), Validators.maxLength(50), Validators.email]],
    gender: [''],
    firstName: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(50)]],
    lastName: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(50)]],
    dateOfBirth: [null],
    street: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(50)]],
    postalCode: ['', [Validators.required, Validators.minLength(4), Validators.maxLength(20)]],
    city: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(50)]],
    password: ['', [Validators.required, Validators.pattern(this.complexPassword)]],
    confirmPassword: ['', Validators.required]
  },
  { validators: this.passwordMatchValidator} as AbstractControlOptions
  );

  //  passwordMatchValidator(g: FormGroup) {
   passwordMatchValidator(group: AbstractControl) {
    return group.get('password').value === group.get('confirmPassword').value
      ? null
      : { mismatch: true };
  }

  onSubmit() {
    this.accountService.register(this.registerForm.value).subscribe({
      next: () => this.router.navigateByUrl('/shop')
    })
  }
<div class="d-flex justify-content-center mt-5">
    <div class="col-3">
        <form  [formGroup]="registerForm" (ngSubmit)="onSubmit()">
            <div class="text-center mb-4">
                <h1 class="mb-3">Sign up</h1>
            </div>   
            
            <app-text-input [formControl]="registerForm.controls['displayName']" [label]="'Display Name'"></app-text-input>
            <app-text-input [formControl]="registerForm.controls['email']" [label]="'Email'"></app-text-input>
            <app-text-input [formControl]="registerForm.controls['password']" [label]="'Password'" [type]="'password'"></app-text-input>
            <app-text-input [formControl]="registerForm.controls['confirmPassword']" [label]="'Confirm Password'" [type]="'password'"></app-text-input>
            <app-text-input [formControl]="registerForm.controls['gender']" [label]="'Gender'"></app-text-input>
            <app-text-input [formControl]="registerForm.controls['firstName']" [label]="'First Name'"></app-text-input>
            <app-text-input [formControl]="registerForm.controls['lastName']" [label]="'Last Name'"></app-text-input>
            <app-text-input [formControl]="registerForm.controls['dateOfBirth']" [label]="'Date of Birth'"></app-text-input>
            <app-text-input [formControl]="registerForm.controls['street']" [label]="'Street'"></app-text-input>
            <app-text-input [formControl]="registerForm.controls['postalCode']" [label]="'Postal Code'"></app-text-input>
            <app-text-input [formControl]="registerForm.controls['city']" [label]="'City'"></app-text-input>
                      
            <div class="d-grid">
              <button [disabled]="registerForm.invalid" class="btn btn-lg btn-primary mt-3" type="submit">Sign up</button>
            </div>
        </form>
    </div>
</div>

I am using a reusable text input component:

import { Component, Input, Self } from '@angular/core';
import { ControlValueAccessor, FormControl, NgControl } from '@angular/forms';

@Component({
  selector: 'app-text-input',
  templateUrl: './text-input.component.html',
  styleUrls: ['./text-input.component.scss']
})
export class TextInputComponent implements ControlValueAccessor {
  @Input() type = 'text';
  @Input() label = '';
  
  constructor(@Self() public controlDir: NgControl) {
    this.controlDir.valueAccessor = this;
  }

  writeValue(obj: any): void {
  }
  registerOnChange(fn: any): void {
  }
  registerOnTouched(fn: any): void {
  }

  get control(): FormControl {
    return this.controlDir.control as FormControl;
  }
}
<div class="form-floating mb-3">
    <input 
      
        type={{type}} 
        [formControl]="control" 
        placeholder="{{label}}"
        class="form-control"
        [ngClass]="(control.touched) ? control.invalid ?  'is-invalid' : 'is-valid' : null"
    >
    <label for="floatingInput">{{label}}</label>
    <div *ngIf="control.errors?.['required']" class="invalid-feedback">Please enter your {{label}}</div>
    <div *ngIf="control.errors?.['email']" class="invalid-feedback">Invalid email address</div>
    <div *ngIf="control.errors?.['pattern']" class="invalid-feedback">Password not complex enough</div>
    <div *ngIf="control.errors?.['confirmPassword']" class="invalid-feedback">Passwords must match</div>
</div>

As you can see on the screenshot I got no error message about password mismatching.

enter image description here

Please understand I'm not a professional developer. Thanks in advance for any advice!


Solution

  • When you use validators over the whole form who is invalid is the form -not the formControl-

    So your ngClass should be like:

    [ngClass]="(control.touched) ? control.invalid 
                      || registerForm.errors?.mismatch ?  'is-invalid' : 'is-valid' : null"
    

    NOTE: It's unnecessary create a custom formControl (a component that implements ControlValueAccesor) if only want to add a label and a type. You can use viewProviders, see, e.g. this SO