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 about my problem is the context, i try to do a simple form containing two inputs : a password and its' confirmation.
user-form.component.ts
this.newUserForm = this.fb.group({
type: ['', Validators.required],
firstname: ['', Validators.required],
lastname: ['', Validators.required],
login: ['', Validators.required],
matchingPasswordsForm: this.fb.group(
{
password1: ['', Validators.required],
password2: ['', Validators.required],
},
{
validator: MatchingPasswordValidator.validate,
},
),
mail: ['', [Validators.required, Validators.pattern(EMAIL_PATTERN)]],
cbaNumber: [
'411000000',
[Validators.required, Validators.pattern(CBANUMBER_PATTERN)],
],
phone: ['', [Validators.required, Validators.pattern(PHONE_PATTERN)]],
}
My interest is about matchingPasswordsForm FormGroup. You can see the validator on it.
Here the validator:
matching-password.validator.ts
export class MatchingPasswordValidator {
constructor() {}
static validate(c: FormGroup): ValidationErrors | null {
if (c.get('password2').value !== c.get('password1').value) {
return { matchingPassword: true};
}
return null;
}
}
and the HTML.
user-form.component.html
<div class="row" formGroupName="matchingPasswordsForm">
<mat-form-field class="col-md-6 col-sm-12">
<input matInput placeholder="Mot de passe:" formControlName="password1">
<mat-error ngxErrors="matchingPasswordsForm.password1">
<p ngxError="required" [when]="['dirty', 'touched']">{{requiredMessage}}</p>
</mat-error>
</mat-form-field>
<mat-form-field class="col-md-6 col-sm-12">
<input matInput placeholder="Confirmez" formControlName="password2">
<mat-error ngxErrors="matchingPasswordsForm.password2">
<p ngxError="required" [when]="['dirty', 'touched']">{{requiredMessage}}</p>
</mat-error>
<!-- -->
<!-- problem is here -->
<!-- -->
<mat-error ngxErrors="matchingPasswordsForm" class="mat-error">
<p ngxError="matchingPassword" [when]="['dirty', 'touched']">{{passwordMatchErrorMessage}}</p>
</mat-error>
<!-- ^^^^^^^^^^^^^^^^ -->
<!-- /problem is here -->
<!-- -->
</mat-form-field>
</div>
I have surrounded the interesting code with comments.
Now, some explanation : With tag, when password2 is touched, my error is displayed :
But, when I write a wrong password, error is not displayed anymore :
First I thought I was misunderstanding custom validator utilisation. BUT when I replace with the whole thing works perfectly !
replace error by hint
<mat-hint ngxErrors="matchinghPasswordsForm">
<p ngxError="matchingPassword" [when]="['dirty', 'touched']">{{passwordMatchErrorMessage}}</p>
</mat-hint>
I hope I was clear, I really want your point of view before posting an issue on material design github.
If I misunterstood something, please light my fire on what I missed.
A last thing, my tests were done with ngxerrors and *ngif. To be more readable my code sample only use ngxerrors .
Thanks in advance for the time you will take.
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.