1

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.

1

1 Answer 1

1

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

4
  • If I enter 123 in the password field and then click with the mouse outside the form, then the Confirm Password Input is also highlighted in red. Surely that shouldn't be the case?
    – coder
    Commented Apr 20, 2024 at 1:55
  • 1
    @coder updated with a small change Commented Apr 20, 2024 at 2:44
  • Is it also possible if you first click in the Confirm Password field and then click outside of the field that the error message Confirm Password is required appears? Currently it is not like that.
    – coder
    Commented Apr 21, 2024 at 1:57
  • @coder please raise follow up question, will check Commented Apr 21, 2024 at 3:27

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.