angularangular-materialangular-forms

Angular Form Validation: Display Errors on Empty and Mismatched Passwords


I want to perform form validation. If the field is "dirty".

  1. An error should be displayed if there is nothing in the password field.

  2. If the passwords do not match, an error should also be displayed.

Can you explain to me what is wrong with my code? I have already created a code example:

Minimal Reproducible Stackblitz

Full Code:

import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import 'zone.js';
import {
  AbstractControl,
  FormControl,
  FormGroup,
  ReactiveFormsModule,
  Validators,
} from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatButtonModule } from '@angular/material/button';
import { FormsModule } from '@angular/forms';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    ReactiveFormsModule,
    MatInputModule,
    MatFormFieldModule,
    MatCheckboxModule,
    MatButtonModule,
    FormsModule,
  ],
  template: `
    <form [formGroup]="registerForm">
      <mat-form-field>
          <mat-label>Password</mat-label>
          <input matInput formControlName="password" type="password"> 
          @if (registerForm.get('password').hasError('required') && registerForm.get('password').dirty) {
          <mat-error>Password is required</mat-error> }
      </mat-form-field>

      <mat-form-field>
          <mat-label>Confirm Password</mat-label>
          <input matInput formControlName="confirmPassword" type="password"> @if (registerForm.errors?.['mismatch'] && registerForm.dirty) {
          <mat-error>Password does not match</mat-error> }
      </mat-form-field>


      <button mat-raised-button color="primary " type="submit" [disabled]="!registerForm.valid">Sign up</button>
</form>
  `,
})
export class App {
  name = 'Angular';
  registerForm: FormGroup;

  constructor() {
    this.registerForm = new FormGroup(
      {
        password: new FormControl('', [
          Validators.required,
          Validators.minLength(6),
        ]),
        confirmPassword: new FormControl('', [Validators.required]),
      },
      { validators: this.passwordMatchValidator }
    );
  }

  private passwordMatchValidator(control: AbstractControl) {
    return control.get('password')?.value ===
      control.get('confirmPassword')?.value
      ? null
      : { mismatch: true };
  }
}

bootstrapApplication(App, {
  providers: [provideAnimationsAsync()],
});

I use Angular 17 and Angular Material.


Solution

  • We can use the ?. TypeScript safe operator to prevent any errors in the html when accessing the properties. Things to note:

    1. We have set the mismatch validator at the form level to be validated, but we show the error message at the confirm password form field, so we need to inform the form control that the element is in error state, for this we can use errorStateMatcher input binding to update the form, which will show the error.

    Error state matcher code:

    export class MyErrorStateMatcher implements ErrorStateMatcher {
      isErrorState(
        control: FormControl | null,
        form: FormGroupDirective | NgForm | null
      ): boolean {
        return !!(form?.errors?.['mismatch'] && control?.touched);
      }
    }
    ...
    
    ...
    export class App {
      name = 'Angular';
      registerForm: FormGroup;
      matcher = new MyErrorStateMatcher();
    

    html

    ...
    <input matInput formControlName="confirmPassword" type="password" [errorStateMatcher]="matcher"> 
    ...
    
    1. We can use @switch instead of @if since we usually show one error at a time, so it's less code if we use switch

    Please find below full code and working Stackblitz for reference.

    Full Code:

    import { Component } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import 'zone.js';
    import {
      AbstractControl,
      FormControl,
      FormGroup,
      NgForm,
      ReactiveFormsModule,
      Validators,
      FormGroupDirective,
    } from '@angular/forms';
    import { MatInputModule } from '@angular/material/input';
    import { MatFormFieldModule } from '@angular/material/form-field';
    import { MatCheckboxModule } from '@angular/material/checkbox';
    import { MatButtonModule } from '@angular/material/button';
    import { FormsModule } from '@angular/forms';
    import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
    import { CommonModule } from '@angular/common';
    import { ErrorStateMatcher } from '@angular/material/core';
    
    export class MyErrorStateMatcher implements ErrorStateMatcher {
      isErrorState(
        control: FormControl | null,
        form: FormGroupDirective | NgForm | null
      ): boolean {
        return !!(form?.errors?.['mismatch'] && control?.touched);
      }
    }
    
    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [
        ReactiveFormsModule,
        MatInputModule,
        MatFormFieldModule,
        MatCheckboxModule,
        MatButtonModule,
        FormsModule,
        CommonModule,
      ],
      template: `
        <form [formGroup]="registerForm">
          <mat-form-field>
              <mat-label>Password</mat-label>
              <input matInput formControlName="password" type="password"> 
              @if (registerForm?.get('password')?.touched) {
                <mat-error>
                @switch (true) {
                    @case (registerForm.get('password')?.hasError('required')) {
                      Password is required
                    }
                    @case (registerForm.get('password')?.hasError('minlength')) {
                      Password must be 6 characters in length
                    }
                }
              </mat-error>
              }
          </mat-form-field>
    
          <mat-form-field>
              <mat-label>Confirm Password</mat-label>
              <input matInput formControlName="confirmPassword" type="password" [errorStateMatcher]="matcher"> 
              @if (registerForm?.get('confirmPassword')?.touched) {
                <mat-error>
                @switch (true) {
                    @case (registerForm.get('confirmPassword')?.hasError('required')) {
                      Confirm Password is required
                    }
                    @case (registerForm.errors?.['mismatch']) {
                      Password does not match
                    }
                }
              </mat-error>
              }
          </mat-form-field>
    
    
          <button mat-raised-button color="primary " type="submit" [disabled]="!registerForm.valid">Sign up</button>
    </form>
      `,
    })
    export class App {
      name = 'Angular';
      registerForm: FormGroup;
      matcher = new MyErrorStateMatcher();
    
      constructor() {
        this.registerForm = new FormGroup(
          {
            password: new FormControl('', [
              Validators.required,
              Validators.minLength(6),
            ]),
            confirmPassword: new FormControl('', [Validators.required]),
          },
          { validators: this.passwordMatchValidator }
        );
      }
    
      private passwordMatchValidator(control: AbstractControl) {
        return control.get('password')?.value ===
          control.get('confirmPassword')?.value
          ? null
          : { mismatch: true };
      }
    }
    
    bootstrapApplication(App, {
      providers: [provideAnimationsAsync()],
    });
    

    Stackblitz Demo