Image of Thomas Mair tmair.dev

Adding ErrorStateMatcher capabilities to the Angular Material Checkbox Component

angularmaterial-uiangular-formserror-state-matcher

The Angular Material Library provides an ErrorStateMatcher to allow control over the error state display of a form element. This blog post describes how we can leverage directives to bring that feature to the checkbox component.

The ErrorStateMatcher

The Angular Material ErrorStateMatcher is provided to be able to control when the error state of a form control ist shown to the user. The default implementation will show the error when the control is invalid and either the user has interacted with the form control (dirty is true) or the form has been submitted. If the default behavior does not suit your needs you can overwrite it globally by providing a global ErrorStateMatcher, for a specific component and all of its children by adding the ErrorStateMatcher to the providers of the component or for a single form control by setting the respective errorStateMatcher property of the component associated with the form control. This setup allows a fine grained control over when the error state of form controls are shown to the user.

Checkbox and the ErrorStateMatcher

The ErrorStateMatcher is available for the material input, select and datepicker components. It is not available for the material checkbox component. However it is quite common to have forms where the user is required to check a required checkbox to confirm the terms of use for a service on a sign up form. To accomplish such a task we often implement a lot of custom code within the component implementing the form and we will duplicate the effort for every form that has the same requirements. It would be nice if there was a way to implement the already existing ErrorStateMatcher approach to just work out of the box with material checkboxes that have a validator. This would allow to use validators on the checkbox to be used and error state to be shown if required.

Using a custom directive to add the ErrorStateMatcher to the material checkbox

By following the eccelent post of Tim Deschryver about extending Angular components that you don’t own we will be able to leverage the ErrorStateMatcher in the Angular material checkbox component. The first step in doing so is to create a directive that will apply the functionality of the ErrorStateMatcher to the material checkbox component.

The directive will do the following:

  1. By using the same selector as the material checkbox component it will target all checkboxes (This also depends on the directive scope. More on that later.)
  2. It will add a class to the component’s host node class list to be able to target the material checkbox with some custom styling.
  3. It will calculate an error state based on the ErrorStateMatcher and the FormControl error state and conditionally add a class to the component’s host node class list to apply styling if the error state should apply to the form control.
  4. Because there is no hook in the angular forms framework to get notified of all state changes of a form control evaluation of the error state is performed in the ngDoCheck lifecycle hook.

Lets see the directive in action:

ts
import {
Directive,
DoCheck,
Input,
OnDestroy,
OnInit,
Optional,
Self,
} from '@angular/core';
import { NgForm, FormGroupDirective, NgControl } from '@angular/forms';
import { MatCheckbox } from '@angular/material/checkbox';
import { ErrorStateMatcher, mixinErrorState } from '@angular/material/core';
import { Subscription } from 'rxjs';
 
const _CustomCheckboxErrorStateBase = mixinErrorState(
class {
constructor(
public _defaultErrorStateMatcher: ErrorStateMatcher,
public _parentForm: NgForm,
public _parentFormGroup: FormGroupDirective,
public ngControl: NgControl
) {}
}
);
 
@Directive({
selector: 'mat-checkbox',
host: {
class: 'checkbox-error-state',
'[class.has-error-state]': 'errorState',
},
})
export class CheckboxErrorStateDirective
extends _CustomCheckboxErrorStateBase
implements OnInit, OnDestroy, DoCheck
{
@Input()
override errorStateMatcher!: ErrorStateMatcher;
 
private changesSubscription!: Subscription;
 
constructor(
private checkbox: MatCheckbox,
defaultErrorStateMatcher: ErrorStateMatcher,
@Optional() @Self() ngControl: NgControl,
@Optional() parentForm: NgForm,
@Optional() parentFormGroup: FormGroupDirective
) {
super(defaultErrorStateMatcher, parentForm, parentFormGroup, ngControl);
}
 
ngOnInit(): void {
// update the error state on state changes
this.changesSubscription = this.checkbox.change.subscribe(() =>
this.updateErrorState()
);
}
 
ngDoCheck() {
if (this.ngControl) {
// We need to re-evaluate this on every change detection cycle, because there are some
// error triggers that we can't subscribe to (e.g. parent form submissions). This means
// that whatever logic is in here has to be super lean or we risk destroying the performance.
this.updateErrorState();
}
}
 
ngOnDestroy(): void {
this.changesSubscription.unsubscribe();
}
}

The styling of the checkbox in the error state depends on the theme. So the following snipped has to be adapted to the corresponding theme.

css
.checkbox-error-state.has-error-state {
color: red;
}
.checkbox-error-state.has-error-state .mat-checkbox-frame {
border-color: red;
}

Applying the directive to all material checkbox components

Angular up to version 13 uses the NgModules to manage the components and directives available within the template of a component. In our case that means the additional functionality of the new directive will only be available if the new directive and the material checkbox component are present in the compilation scope of the template we would like to use the directive in. In other words, we always need to import the new directive alongside the material checkbox module. This is quite cumbersome and error prone so there is a way to ease the import of both the directive and the material checkbox module.

We can simply define a new NgModule and add the new directive and the material checkbox module to the exports of the new module. Additionally we could ban the @angular/material/checkbox import via a linting rule so that we only ever require the augmented @NgModule.

ts
import { NgModule } from '@angular/core';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { CheckboxErrorStateDirective } from './checkbox-error-state.directive';
 
@NgModule({
exports: [MatCheckboxModule, CheckboxErrorStateDirective],
declarations: [CheckboxErrorStateDirective],
})
export class CustomCheckboxModule {}

You can head over to the repository implementing the error state matcher for the material checkbox component or play with the implementation on Stackblitz.