We use form all the time and set up validation rules for it. Angular provides a good built-in solution for that. But today I'd like to talk about angular material form fields more.
There is nothing special when we use typically validation with mat-form-field
. We just put error message inside mat-error
component and set up which messages should be displayed for which errors.
ℹ️ Error messages are shown when the control is invalid and the user has interacted with (touched) the element or the parent form has been submitted.
Let's take a look a bit more complex example. I reckon that comparison two passwords fields will be a good example because we should compare value for both fields.
Let say we need to show error that passwords don't match under confirmPassword
field only.
ℹ️ It's not difficult to show error under the field. But we need to say our field about this error to make it to be red. Otherwise, the field will know nothing about the error.
First of all we need to create a form with two fields. I will put all code into one file. You can see all at once.
import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';
import {
FormControl,
FormGroup,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import 'zone.js';
@Component({
selector: 'app-root',
standalone: true,
imports: [
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
],
template: `
<form [formGroup]="form">
<mat-form-field appearance="outline">
<mat-label>Password</mat-label>
<input formControlName="password" matInput/>
@if (hasError('password', 'required')) {
<mat-error>Field is required</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Confirm password</mat-label>
<input formControlName="confirmPassword" matInput/>
@if (hasError('confirmPassword', 'required')) {
<mat-error>Field is required</mat-error>
}
</mat-form-field>
<button color="primary" mat-flat-button>Save</button>
</form>
`,
})
export class App {
readonly form = new FormGroup({
password: new FormControl<string | null>(null, Validators.required),
confirmPassword: new FormControl<string | null>(null, Validators.required),
});
hasError(controlName: string, error: string): boolean {
return this.form.get(controlName).hasError(error);
}
}
bootstrapApplication(App, {
providers: [provideAnimations()],
});
We have set up our form. Now, we need to create and add validator to form.
This validator will compare values for two controls what's names we'll pass and return ValidatorFn
.
function matchValidator(
controlName: string,
compareWith: string
): ValidatorFn | null {
return (form: FormGroup) => {
const value = form.get(controlName).value;
return value && value === form.get(compareWith).value
? null
: { match: true }; // error type
};
}
Then we need to add our validator to the form.
readonly form = new FormGroup(
{
password: new FormControl<string | null>(null, Validators.required),
confirmPassword: new FormControl<string | null>(
null,
Validators.required
),
},
{ validators: [matchValidator('password', 'confirmPassword')] }
);
Let's check it out to be sure. We should see { match: true }
object for error
property inside the form.
Everything works as expected. What's the next? We can show error messages under the field. But how can we get the field become red? How to tell our input about error state? That's why errorStateMatcher
exist.
ℹ️ An ErrorStateMatcher must implement a single method isErrorState which takes the FormControl for this matNativeControl as well as the parent form and returns a boolean indicating whether errors should be shown. (true indicating that they should be shown, and false indicating that they should not.)
@Injectable({ providedIn: 'root' })
export class CrossFieldErrorMatcher implements ErrorStateMatcher {
public isErrorState(
control: FormControl | null,
form: FormGroupDirective | NgForm | null
): boolean {
return form.errors?.match && (control.touched || control.dirty);
}
}
<mat-form-field appearance="outline">
<mat-label>Confirm password</mat-label>
<input formControlName="confirmPassword" matInput [errorStateMatcher]="matcher"/>
@if (hasError('confirmPassword', 'required')) {
<mat-error>Field is required</mat-error>
}
@if (form.hasError('match')) {
<mat-error>Password don't match</mat-error>
}
</mat-form-field>
I used @Injectable
decorator here to inject state matcher to the component. That's it.
Final result:
Is there a way to use errorStateMatcher
globally?
Yes, there is. For example, you want to change default behavior and show error only if form has been submitted. Let's create errorStateMatcher
for that.
export class SubmittedStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted = form && form.submitted;
return !!(control?.invalid && isSubmitted);
}
}
Then we need to provide to your app module it instead of default ErrorStateMatcher
.
providers: [
{
provide: ErrorStateMatcher,
useClass: SubmittedStateMatcher,
},
]
Hope, this article will help you with your validation 🙂
Top comments (1)
Dzmitry Hutaryan, Good tip!
Thanks for sharing