Adding ErrorStateMatcher capabilities to the Angular Material Checkbox Component
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:
- By using the same
selectoras the material checkbox component it will target all checkboxes (This also depends on the directive scope. More on that later.) - 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.
- It will calculate an error state based on the
ErrorStateMatcherand theFormControlerror 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. - 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
ngDoChecklifecycle hook.
Lets see the directive in action:
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.
.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.
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.