I wrote a simple custom Validator to compare two password fields and signal an error if they don't match. Here it is:
@Directive({
selector: '[appMatchPassword]',
providers: [
{ provide: NG_VALIDATORS, useExisting: MatchPasswordDirective, multi: true }
]
})
export class MatchPasswordDirective implements Validator {
@Input('appMatchPassword') otherControl: string = '';
constructor() { }
registerOnValidatorChange(fn: () => void): void {
}
validate(control: AbstractControl): ValidationErrors | null {
const compareTo = control.parent?.get(this.otherControl);
if (!compareTo) return null;
if (compareTo?.value !== control.value) return {
appMatchPassword: true
}
return null;
}
}
The template references it like this:
<mat-form-field>
<mat-label>Password</mat-label>
<input matInput type="password" name="password" [(ngModel)]="user.password"
required minlength="4" #iPassword="ngModel">
<mat-error *ngIf="iPassword.errors?.required">Password Required.</mat-error>
<mat-error *ngIf="iPassword.errors?.minlength">Minimum 4 characters</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Verify Password</mat-label>
<input matInput type="password" name="vpassword" [(ngModel)]="vpassword"
required appMatchPassword="password" #fvp="ngModel">
<mat-error *ngIf="fvp.errors?.appMatchPassword">Passwords do not match</mat-error>
</mat-form-field>
This works as desired if the user puts a value into the password
field and a different value in the vpassword
field. The error is displayed and goes away if the user corrects the value in the vpassword
field.
However, if the user goes back and edits the password
field the error does not go away. The validator only gets called after its own AbstractControl changes value.
Is there any good way to trigger the Validator when the password
field gets changed?
You have to use cross-field validation and apply your validator directive to the form itself, not to the password field.
If the validator issues an error, it will be present on the form's errors array, not on that of the particular field's.
Additionally, if you want to display the mat-error
inside the mat-form-field
it will not show because the field itself is not invalid (only the entire form) - so you can either move the mat-error
outside, or use a custom error state matcher
In short here's how your directive should look (note that now the control passed to the validate
method is the entire form, not a particular field)
validate(control: AbstractControl): ValidationErrors | null {
const pass = control.get('password')?.value;
const vpass = control.get('vpassword')?.value;
console.log(pass,vpass);
if (pass !== vpass)
return {
appMatchPassword: true
};
return null;
}
And here's how your template should look:
<!-- the directive is applied to the form -->
<form #frm="ngForm" appMatchPassword>
<mat-form-field>
<mat-label>Password</mat-label>
<input matInput type="password" name="password" [(ngModel)]="user.password"
required minlength="4" #iPassword="ngModel">
<mat-error *ngIf="iPassword.errors?.required">Password Required.</mat-error>
<mat-error *ngIf="iPassword.errors?.minlength">Minimum 4 characters</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Verify Password</mat-label>
<!-- the directive is NOT applied to the particular field -->
<!-- also note the `matcher` property, see below... -->
<input matInput type="password" name="vpassword" [(ngModel)]="vpassword" required
[errorStateMatcher]="matcher">
<!-- this line changed, it now looks at frm.errors instead of fvp.errors -->
<mat-error *ngIf="frm.errors?.appMatchPassword">Passwords do not match</mat-error>
</mat-form-field>
</form>
And here's a dummy error state matcher that simply always shows any mat-error
existing in the field:
export class AlwaysShowErrors implements ErrorStateMatcher {
isErrorState(control: FormControl, form: FormGroupDirective | NgForm): boolean {
return true;
}
}
Instantiate it as a matcher
field in the component class thus:
matcher = new AlwaysShowErrors();
StackBlitz example
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With