skip to Main Content

I am working on a reactive registration form in Angular 16.

I have made a custom validation to check if the values in the form fields password and confirm_password match:

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

export const matchpassword : ValidatorFn = (control: AbstractControl):ValidationErrors|null => {

     let password = control.get('password');
     let confirm_password = control.get('confirm_password');
     if (password && confirm_password && password?.value != confirm_password?.value) {
        return { password_match : true }
     }
    return null; 
}

In componentsregistrationregistration.component.ts I have:

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { matchpassword } from "../../custom-validators/matchpassword.validator";

@Component({
  selector: 'app-registration',
  templateUrl: './registration.component.html',
  styleUrls: ['./registration.component.scss']
})
export class RegistrationComponent implements OnInit {

  public registrationForm!: FormGroup;
  public isSuccess: Boolean = false;

  constructor(private formBuilder: FormBuilder) { }

  get form() { return this.registrationForm.controls }

  public registerUser() {

    if (this.registrationForm.status !== 'INVALID') {
      // Show success alert
      this.isSuccess = true;

      // Reset form
      this.registrationForm.reset();
    } else {
      return;
    }
  }

  ngOnInit() {
    this.registrationForm = this.formBuilder.group({
      firstName: ['', [Validators.required, Validators.minLength(3)]],
      lastName: ['', [Validators.required, Validators.minLength(3)]],
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(6), matchpassword]],
      confirm_password: ['', matchpassword],
      terms: ['', Validators.requiredTrue],
    }, {
      validators: matchpassword
    });
  }
}

In componentsregistrationregistration.component.html I have:

<div *ngIf="isSuccess" class="alert alert-success alert-dismissible fade show text-center">
    <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
    Your signup was successful!
</div>

<form
    [formGroup]="registrationForm"
    (ngSubmit)="registerUser()"
    novalidate
  >
    <div class="form-element mb-2">
      <label for="firstName" class="form-label"> First name </label>
      <input
        type="text"
        id="firstName"
        formControlName="firstName"
        class="form-control form-control-sm"
      />
      <span
        class="invalid-feedback"
        *ngIf="form['firstName']?.touched && form['firstName'].errors?.['required']"
        >The <i>First name</i> field is required</span
      >
      <span
        class="invalid-feedback"
        *ngIf="form['firstName'].touched && form['firstName'].errors?.['minlength']"
        >The <i>First name</i> must be at least 3 characters long</span
      >
    </div>

    <div class="form-element mb-2">
      <label for="lastName" class="form-label"> Last name </label>
      <input
        type="text"
        id="lastName"
        formControlName="lastName"
        class="form-control form-control-sm"
      />
      <span
        class="invalid-feedback"
        *ngIf="form['lastName'].touched && form['lastName'].errors?.['required']"
        >The <i>Last name</i> field is required</span
      >
      <span
        class="invalid-feedback"
        *ngIf="form['lastName'].touched && form['lastName'].errors?.['minlength']"
        >The <i>Last name</i> must be at least 3 characters long</span
      >
    </div>

    <div class="form-element mb-2">
      <label for="email" class="form-label"> Email address </label>
      <input
        type="email"
        id="email"
        formControlName="email"
        class="form-control form-control-sm"
      />
      <span
        class="invalid-feedback"
        *ngIf="form['email'].touched && form['email'].errors?.['required']"
        >The <i>Email</i> field is required</span
      >
      <span
        class="invalid-feedback"
        *ngIf="form['email'].touched && form['email'].errors?.['email']"
        >Please provide a valid email address</span
      >
    </div>

    <div class="form-element mb-2">
      <label for="password" class="form-label"> Password </label>
      <input
        type="password"
        id="password"
        formControlName="password"
        class="form-control form-control-sm"
      />
      <span
        class="invalid-feedback"
        *ngIf="form['password'].touched && form['password'].errors?.['required']"
        >The <i>Password</i> field is required</span
      >
      <span
        class="invalid-feedback"
        *ngIf="form['password'].touched && form['password'].errors?.['minlength']"
        >The password must have al least 6 characters</span
      >
      <span
        class="invalid-feedback"
        *ngIf="form['password'].touched && form['password'].errors?.['passwordcomplexity']"
        >The password is not complex enough</span
      >
      <span
        class="invalid-feedback"
        *ngIf="form['password'].touched && registrationForm.errors?.['password_match']
        "
        >The passwords do not match</span
      >
    </div>

    <div class="form-element mb-2">
      <label for="confirm_password" class="form-label">
        Confirm password
      </label>
      <input
        type="password"
        id="confirm_password"
        formControlName="confirm_password"
        class="form-control form-control-sm"
      />
    </div>

    <div class="form-element mb-2">
      <input
        type="checkbox"
        formControlName="terms"
        id="terms"
        class="me-1"
      />
      <span class="text-terms text-muted"
        >I accept the
        <a href="#" class="text-success">Terms & conditions</a></span
      >
      <span
        class="invalid-feedback terms"
        *ngIf="form['terms'].dirty && form['terms'].errors?.['required']"
        >You must accept our Terms & conditions</span
      >
    </div>

    <div class="pt-2">
      <button
        type="submit"
        class="btn btn-sm btn-success w-100"
        [disabled]="!registrationForm.valid"
      >
        Submit
      </button>
    </div>
  </form>

Stackblitz

There is a stackblitz with all the code HERE

The goal

If the passwords do not match, I want the error message "The passwords do not match" to show on the password field, as below:

What I want

The problem

Although the password is seen as invalid (the submit button is disabled), the error message does not show:

What is happening

What am I doing wrong?

2

Answers


  1. Chosen as BEST ANSWER

    In case somebody might find the complete code that worked for me...

    The custom validators custom-validatorsmatchpassword.validator.ts

    import {AbstractControl, ValidationErrors, ValidatorFn} from '@angular/forms';
     
     export const matchpassword: ValidatorFn = (
       control: AbstractControl
     ): ValidationErrors | null => {
       let password = control.get('password');
       let confirm_password = control.get('confirm_password');
       if (
         password &&
         confirm_password &&
         password?.value != confirm_password?.value
       ) {
         return { password_match: true };
       }
       return null;
     };
     
    

    and custom-validatorspasswordcomplexity.validator.ts

    import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";
    export const passwordcomplexity : ValidatorFn = (control: AbstractControl):ValidationErrors|null => {
        let password = control.value;
    
        let oneUppercase = /[A-Z]/.test(password);
        let oneLowercase = /[a-z]/.test(password);
        let oneDigit = /d/.test(password);
        let oneSpecialCharacter = /[`!@#$%^&*()_+-=[]{};':"\|,.<>/?~]/.test(password);
         
        if (!(oneUppercase && oneLowercase && oneDigit && oneSpecialCharacter)) {
          return { passwordcomplexity : true }
        }
    
        return null; 
    }
    

    In appcomponentsregistrationregistration.component.ts I have:

    import { Component, OnInit } from '@angular/core';
    import { FormBuilder, FormGroup, Validators } from '@angular/forms';
    import { matchpassword } from "../../custom-validators/matchpassword.validator";
    import { passwordcomplexity } from '../../custom-validators/passwordcomplexity.validator';
    
    @Component({
      selector: 'app-registration',
      templateUrl: './registration.component.html',
      styleUrls: ['./registration.component.scss']
    })
    export class RegistrationComponent implements OnInit {
    
      public registrationForm!: FormGroup;
      public isSuccess: Boolean = false;
    
      constructor(private formBuilder: FormBuilder) { }
    
      get form() { return this.registrationForm.controls }
    
      public registerUser() {
    
        if (this.registrationForm.status !== 'INVALID') {
          // Show success alert
          this.isSuccess = true;
    
          // Reset form
          this.registrationForm.reset();
        } else {
          return;
        }
      }
    
      ngOnInit() {
        this.registrationForm = this.formBuilder.group(
          {
            firstName: ['', [Validators.required, Validators.minLength(3)]],
            lastName: ['', [Validators.required, Validators.minLength(3)]],
            email: ['', [Validators.required, Validators.email]],
            password: ['',[Validators.required, Validators.minLength(6), passwordcomplexity]],
            confirm_password: [''],
            terms: ['', Validators.requiredTrue],
          },
          {
            validators: [matchpassword],
          }
        );
      }
    }
    

    In srcappcomponentsregistrationregistration.component.html I have:

    <div *ngIf="isSuccess" class="alert alert-success alert-dismissible fade show text-center">
        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
        Your signup was successful!
    </div>
    
    <form [formGroup]="registrationForm" (ngSubmit)="registerUser()" novalidate>
        <div class="form-element mb-2">
          <label for="firstName" class="form-label">
            First name
          </label>
          <input type="text" id="firstName" formControlName="firstName" class="form-control form-control-sm" />
          <span class="invalid-feedback"
            *ngIf="form['firstName']?.touched && form['firstName'].errors?.['required']">The <i>First name</i> field is required</span>
            <span class="invalid-feedback" *ngIf="form['firstName'].touched && form['firstName'].errors?.['minlength']">The <i>First name</i> must be at least 3 characters long</span>
        </div>
    
        <div class="form-element mb-2">
          <label for="lastName" class="form-label">
            Last name
          </label>
          <input type="text" id="lastName" formControlName="lastName" class="form-control form-control-sm" />
          <span class="invalid-feedback" *ngIf="form['lastName'].touched && form['lastName'].errors?.['required']">The <i>Last name</i> field is required</span>
          <span class="invalid-feedback" *ngIf="form['lastName'].touched && form['lastName'].errors?.['minlength']">The <i>Last name</i> must be at least 3 characters long</span>
        </div>
    
        <div class="form-element mb-2">
          <label for="email" class="form-label">
            Email address
          </label>
          <input type="email" id="email" formControlName="email" class="form-control form-control-sm" />
          <span class="invalid-feedback" *ngIf="form['email'].touched && form['email'].errors?.['required']">The
            <i>Email</i> field is
            required</span>
          <span class="invalid-feedback" *ngIf="form['email'].touched && form['email'].errors?.['email']">Please provide
            a valid email
            address</span>
    
        </div>
    
        <div class="form-element mb-2">
          <label for="password" class="form-label">
            Password
          </label>
          <input type="password" id="password" formControlName="password" class="form-control form-control-sm" />
          <span class="invalid-feedback" *ngIf="form['password'].touched && form['password'].errors?.['required']">The
            <i>Password</i> field is
            required</span>
          <span class="invalid-feedback" *ngIf="form['password'].touched && form['password'].errors?.['minlength']">The
            password must have al least 6 characters</span>
          <span class="invalid-feedback"
            *ngIf="form['password'].touched && form['password'].errors?.['passwordcomplexity']">The password is not
            complex enough</span>
          <span class="invalid-feedback" *ngIf="form['password'].touched && registrationForm.errors?.['password_match']
            ">The passwords do not match</span>
        </div>
    
        <div class="form-element mb-2">
          <label for="confirm_password" class="form-label">
            Confirm password
          </label>
          <input type="password" id="confirm_password" formControlName="confirm_password"
            class="form-control form-control-sm" />
        </div>
    
        <div class="form-element mb-2">
          <input type="checkbox" formControlName="terms" id="terms" class="me-1">
          <span class="text-terms text-muted">I accept the <a href="#" class="text-success">Terms &
              conditions</a></span>
          <span class="invalid-feedback terms" *ngIf="form['terms'].dirty && form['terms'].errors?.['required']">You
            must accept our Terms &
            conditions</span>
        </div>
    
        <div class="pt-2">
          <button type="submit" class="btn btn-sm btn-success w-100" [disabled]="!registrationForm.valid">
            Submit
          </button>
        </div>
    </form>
    

    In order to prevent validation messages overlap, in the SCSS I do this to display only the first message in the DOM:

    .invalid-feedback {
        display: none;
    
        &:first-of-type {
            display: inline-block;
        }
    }
    

    DEMO

    There is demo stackblitz HERE.


  2. The matchpassword validator needs to be added to the form group rather than to some single control. Adding a validator to a control doesn’t give you access to the other controls.

    this.registrationForm = this.formBuilder.group(
      {
        firstName: ['', [Validators.required, Validators.minLength(3)]],
        lastName: ['', [Validators.required, Validators.minLength(3)]],
        email: ['', [Validators.required, Validators.email]],
        password: [
          '',
          [Validators.required, Validators.minLength(6), passwordcomplexity],
        ],
        confirm_password: ['', Validators.required],
        terms: ['', Validators.requiredTrue],
      },
      {
        validators: [matchpassword],
      }
    );
    

    After fixing that, you need to adjust your error message to be displayed when the form group has an error rather than the form control:

    <span
      class="invalid-feedback"
      *ngIf="
        form['password'].touched &&
        registrationForm.errors?.['password_match']
      "
      >The passwords do not match</span
    >
    

    Updated stackblitz: https://stackblitz.com/edit/stackblitz-starters-wbynzu?file=src%2Fapp%2Fcomponents%2Fregistration%2Fregistration.component.ts

    Note: There are now some issues with overlapping error messages, which I didn’t fix, as this is not related to the question

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search