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
selector
as 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
ErrorStateMatcher
and theFormControl
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. - 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 ,publicngControl :NgControl ) {}});@Directive ({selector : 'mat-checkbox',host : {class : 'checkbox-error-state','[class.has-error-state]': 'errorState',},})export classCheckboxErrorStateDirective extends_CustomCheckboxErrorStateBase implementsOnInit ,OnDestroy ,DoCheck {@Input ()overrideerrorStateMatcher !:ErrorStateMatcher ;privatechangesSubscription !:Subscription ;constructor(privatecheckbox :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 changesthis.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 NgModule
s 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 classCustomCheckboxModule {}
You can head over to the repository implementing the error state matcher for the material checkbox component or play with the implementation on Stackblitz.