How to use errorStateMatcher in Angular material
Tags: angular, material, typescript
Published: 2024-01-03 22:24:19

We use form all the time and set up validation rules for it. Angular provides a good built-in solution for that. But today I'd like to talk about angular material form fields more.

There is nothing special when we use typically validation with `mat-form-field`. We just put error message inside `mat-error` component and set up which messages should be displayed for which errors.

---

> ℹ️ _Error messages are shown when the control is invalid and the user has interacted with (touched) the element or the parent form has been submitted._

---

Let's take a look a bit more complex example. I reckon that comparison two passwords fields will be a good example because we should compare value for both fields.
Let say we need to show error that passwords don't match under `confirmPassword` field only.

---

ℹ️ _It's not difficult to show error under the field. But we need to say our field about this error to make it to be red. Otherwise, the field will know nothing about the error._

---

First of all we need to create a form with two fields. _I will put all code into one file. You can see all at once._

```typescript
import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';
import {
  FormControl,
  FormGroup,
  ReactiveFormsModule,
  Validators,
} from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import 'zone.js';

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

      <mat-form-field appearance="outline">
        <mat-label>Confirm password</mat-label>
        <input formControlName="confirmPassword" matInput/>
        @if (hasError('confirmPassword', 'required')) {
          <mat-error>Field is required</mat-error>
        }
      </mat-form-field>

      <button color="primary" mat-flat-button>Save</button>
    </form>
  `,
})
export class App {
  readonly form = new FormGroup({
    password: new FormControl<string | null>(null, Validators.required),
    confirmPassword: new FormControl<string | null>(null, Validators.required),
  });

  hasError(controlName: string, error: string): boolean {
    return this.form.get(controlName).hasError(error);
  }
}

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

We have set up our form. Now, we need to create and add validator to form.
This validator will compare values for two controls what's names we'll pass and return `ValidatorFn`.

```typescript
function matchValidator(
  controlName: string,
  compareWith: string
): ValidatorFn | null {
  return (form: FormGroup) => {
    const value = form.get(controlName).value;
    return value && value === form.get(compareWith).value
      ? null
      : { match: true }; // error type
  };
}
```

Then we need to add our validator to the form.

```typescript
readonly form = new FormGroup(
    {
      password: new FormControl<string | null>(null, Validators.required),
      confirmPassword: new FormControl<string | null>(
        null,
        Validators.required
      ),
    },
    { validators: [matchValidator('password', 'confirmPassword')] }
  );
```

Let's check it out to be sure. We should see `{ match: true }` object for `error` property inside the form.

![Form error state](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gboj9tysvnvsxd13e7qj.png)


Everything works as expected. What's the next? We can show error messages under the field. But how can we get the field become red? How to tell our input about error state? That's why `errorStateMatcher` exist.

---

> ℹ️ An ErrorStateMatcher must implement a single method isErrorState which takes the FormControl for this matNativeControl as well as the parent form and returns a boolean indicating whether errors should be shown. (true indicating that they should be shown, and false indicating that they should not.)

---

```typescript
@Injectable({ providedIn: 'root' })
export class CrossFieldErrorMatcher implements ErrorStateMatcher {
  public isErrorState(
    control: FormControl | null,
    form: FormGroupDirective | NgForm | null
  ): boolean {
    return form.errors?.match && (control.touched || control.dirty);
  }
}
```

```html
<mat-form-field appearance="outline">
  <mat-label>Confirm password</mat-label>
  <input formControlName="confirmPassword" matInput [errorStateMatcher]="matcher"/>
  @if (hasError('confirmPassword', 'required')) {
    <mat-error>Field is required</mat-error>
  }
  @if (form.hasError('match')) {
    <mat-error>Password don't match</mat-error>
  }
</mat-form-field>
```

I used `@Injectable` decorator here to inject state matcher to the component. That's it.

Final result:
{% embed https://stackblitz.com/edit/error-state-matcher-example?embed=1&file=src%2Fmain.ts %}

### Is there a way to use `errorStateMatcher` globally?

Yes, there is. For example, you want to change default behavior and show error only if form has been submitted. Let's create `errorStateMatcher` for that.

```typescript
export class SubmittedStateMatcher implements ErrorStateMatcher {
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    const isSubmitted = form && form.submitted;
    return !!(control?.invalid && isSubmitted);
  }
}
```

Then we need to provide to your app module it instead of default `ErrorStateMatcher`.

```typescript
providers: [
  {
    provide: ErrorStateMatcher,
    useClass: SubmittedStateMatcher,
  },
]
```

Hope, this article will help you with your validation 🙂