I come to you for talking about a problem with angular material. In fact, I think it\'s an issue, but I prefer looking for a misunterstanding first.
The first thing abou
Alex is correct. You have to use an ErrorStateMatcher. I had to do a lot of research to figure this out, and there wasn’t a single source that gave me the whole answer. I had to cobble together the information I learned from multiple sources to make my own solution to the problem. Hopefully the following example will save you from the headache that I experienced.
Here is an example of a form which uses Angular Material elements for a user registration page.
<form [formGroup]="userRegistrationForm" novalidate>
<mat-form-field>
<input matInput placeholder="Full name" type="text" formControlName="fullName">
<mat-error>
{{errors.fullName}}
</mat-error>
</mat-form-field>
<div formGroupName="emailGroup">
<mat-form-field>
<input matInput placeholder="Email address" type="email" formControlName="email">
<mat-error>
{{errors.email}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Confirm email address" type="email" formControlName="confirmEmail" [errorStateMatcher]="confirmValidParentMatcher">
<mat-error>
{{errors.confirmEmail}}
</mat-error>
</mat-form-field>
</div>
<div formGroupName="passwordGroup">
<mat-form-field>
<input matInput placeholder="Password" type="password" formControlName="password">
<mat-error>
{{errors.password}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Confirm password" type="password" formControlName="confirmPassword" [errorStateMatcher]="confirmValidParentMatcher">
<mat-error>
{{errors.confirmPassword}}
</mat-error>
</mat-form-field>
</div>
<button mat-raised-button [disabled]="userRegistrationForm.invalid" (click)="register()">Register</button>
</form>
As you can see, I am using <mat-form-field>
, <input matInput>
, and <mat-error>
tags from Angular Material. My first thought was to add the *ngIf
directive to control when the <mat-error>
sections show up, but this has no effect! The visibility is actually controlled by the validity (and "touched" status) of the <mat-form-field>
, and there is no provided validator to test equality to another form field in HTML or Angular. That is where the errorStateMatcher
directives on the confirmation fields come into play.
The errorStateMatcher
directive is built in to Angular Material, and provides the ability to use a custom method to determine the validity of a <mat-form-field>
form control, and allows access to the validity status of the parent to do so. To begin to understand how we can use errorStateMatcher for this use case, let's first take a look at the component class.
Here is an Angular Component class that sets up validation for the form using FormBuilder.
export class App {
userRegistrationForm: FormGroup;
confirmValidParentMatcher = new ConfirmValidParentMatcher();
errors = errorMessages;
constructor(
private formBuilder: FormBuilder
) {
this.createForm();
}
createForm() {
this.userRegistrationForm = this.formBuilder.group({
fullName: ['', [
Validators.required,
Validators.minLength(1),
Validators.maxLength(128)
]],
emailGroup: this.formBuilder.group({
email: ['', [
Validators.required,
Validators.email
]],
confirmEmail: ['', Validators.required]
}, { validator: CustomValidators.childrenEqual}),
passwordGroup: this.formBuilder.group({
password: ['', [
Validators.required,
Validators.pattern(regExps.password)
]],
confirmPassword: ['', Validators.required]
}, { validator: CustomValidators.childrenEqual})
});
}
register(): void {
// API call to register your user
}
}
The class sets up a FormBuilder
for the user registration form. Notice that there are two FormGroup
s in the class, one for confirming the email address, and one for confirming the password. The individual fields use appropriate validator functions, but both use a custom validator at the group level, which checks to make sure that the fields in each group are equal to each other, and returns a validation error if they are not.
The combination of the custom validator for the groups and the errorStateMatcher directive is what provides us the complete functionality needed to appropriately show validation errors for the confirmation fields. Let's take a look at the custom validation module to bring it all together.
I chose to break the custom validation functionality into its own module, so that it can easily be reused. I also chose to put other things related to my form validation in that module, namely, regular expressions and error messages, for the same reason. Thinking ahead a little, it is likely that you will allow a user to change their email address and password in a user update form as well, right? Here is the code for the entire module.
import { FormGroup, FormControl, FormGroupDirective, NgForm, ValidatorFn } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material';
/**
* Custom validator functions for reactive form validation
*/
export class CustomValidators {
/**
* Validates that child controls in the form group are equal
*/
static childrenEqual: ValidatorFn = (formGroup: FormGroup) => {
const [firstControlName, ...otherControlNames] = Object.keys(formGroup.controls || {});
const isValid = otherControlNames.every(controlName => formGroup.get(controlName).value === formGroup.get(firstControlName).value);
return isValid ? null : { childrenNotEqual: true };
}
}
/**
* Custom ErrorStateMatcher which returns true (error exists) when the parent form group is invalid and the control has been touched
*/
export class ConfirmValidParentMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
return control.parent.invalid && control.touched;
}
}
/**
* Collection of reusable RegExps
*/
export const regExps: { [key: string]: RegExp } = {
password: /^(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{7,15}$/
};
/**
* Collection of reusable error messages
*/
export const errorMessages: { [key: string]: string } = {
fullName: 'Full name must be between 1 and 128 characters',
email: 'Email must be a valid email address (username@domain)',
confirmEmail: 'Email addresses must match',
password: 'Password must be between 7 and 15 characters, and contain at least one number and special character',
confirmPassword: 'Passwords must match'
};
First let's take a look at the custom validator function for the group, CustomValidators.childrenEqual()
. Since I come from an object-oriented programming background, I chose to make this function a static class method, but you could just as easily make it a standalone function. The function must be of type ValidatorFn
(or the approprate literal signature), and take a single parameter of type AbstractControl
, or any derivative type. I chose to make it FormGroup
, since that is the use case it's for.
The function's code iterates over all of the controls in the FormGroup
, and ensures that their values all equal that of the first control. If they do, it returns null
(indicates no errors), otherwise is returns a childrenNotEqual
error.
So now we have an invalid status on the group when the fields are not equal, but we still need to use that status to control when to show our error message. Our ErrorStateMatcher, ConfirmValidParentMatcher
, is what can do this for us. The errorStateMatcher directive requires that you point to an instance of a class which implements the provided ErrorStateMatcher class in Angular Material. So that is the signature used here. ErrorStateMatcher requires the implementation of an isErrorState
method, with the signature shown in the code. It returns true
or false
; true
indicates that an error exists, which makes the input element's status invalid.
The single line of code in this method is quite simple; it returns true
(error exists) if the parent control (our FormGroup) is invalid, but only if the field has been touched. This aligns with the default behavior of <mat-error>
, which we are using for the rest of the fields on the form.
To bring it all together, we now have a FormGroup with a custom validator that returns an error when our fields are not equal, and a <mat-error>
which displays when the group is invalid. To see this functionality in action, here is a working plunker with an implementation of the code mentioned.
Also, I've blogged this solution here.
obsessiveprogrammer's answer was correct for me, however I had to change the childrenEqual
function with angular 6 and strictNullChecks
(which is an option recommended by the angular team) to this:
static childrenEqual: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const f = control as FormGroup;
const [firstControlName, ...otherControlNames] = Object.keys(f.controls || {});
if(f.get(firstControlName) == null) {
return null;
}
otherControlNames.forEach(controlName => {
if(f.get(controlName) == null) {
return null;
}
})
const isValid = otherControlNames.every(controlName => f.get(controlName)!.value === f.get(firstControlName)!.value);
return isValid ? null : { childrenNotEqual: true };
}
How to create a custom validation:
if the internal property 'isValid' of the component is false, then set the input status in error, and display a message.
HTML:
<input matInput [formControl]="inputControl" [placeholder]="placeholder" [readonly]="readonly" [errorStateMatcher]="matcher"> <mat-error *ngIf="!isValid"> Input not valid. </mat-error>
TS:
isValid = true; changeValitationStatus() { this.matcher = new InputErrorStateMatcher(!this.isValid); } matcher = new InputErrorStateMatcher(!this.isValid); class InputErrorStateMatcher implements ErrorStateMatcher { constructor(private errorstate: boolean) {} isErrorState(control: FormControl|null, form: FormGroupDirective|NgForm|null):boolean { return this.errorstate; } }
And in this way you have a validation using only the formControl.