password and confirm password field validation angular2 reactive forms
For those who want to add a custom validator without being forced to pass from the form group validation, it's possible to add the validator after defining the form.
One advantage of this approach is that the error is added to the form control and not to the form group. This way it's more easy to display the error associated to the field since we can check the error directly on the field/form control itself.
This is how I implemented it:
Custom validator
import { AbstractControl, ValidatorFn } from '@angular/forms';
export class MismatchValidator {
static mismatch(otherInputControl: AbstractControl): ValidatorFn {
return (inputControl: AbstractControl): { [key: string]: boolean } | null => {
if (inputControl.value !== undefined
&& inputControl.value.trim() != ""
&& inputControl.value !== otherInputControl.value) {
return { 'mismatch': true };
}
return null;
};
}
}
Applying the custom validator to the form control
ngOnInit() {
this.initForm();
// The validators are set after defining the form in order to be able to access the password control and pass it to the custom validator as a parameter
this.setValidators();
}
private initForm() {
this.myForm = this.formBuilder.group({
password: new FormControl(''),
passwordConfirmation: new FormControl('')
});
}
private setValidators() {
const formValidators = {
"password": Validators.compose([
Validators.required,
//....
]),
"passwordConfirmation": Validators.compose([
Validators.required,
MismatchValidator.mismatch(this.myForm.get("password"))
])
}
this.passwordRecoveryForm.get("password").setValidators(
formValidators["password"]
);
this.passwordRecoveryForm.get("passwordConfirmation").setValidators(
formValidators["passwordConfirmation"]
);
}
The validators are set after defining the form in order to be able to access the password control and pass it to the custom validator as a parameter.
Best would be to have a nested group inside the form group, where we have a custom validator checking the form group with password
and confirmPass
, so when either of the fields are changed, the validator is fired, as of previously it only fires when confirmPass
field is modified.
So instead do something like this inside the outer formgroup:
// ...
passwords: this.fb.group({
password: ['', [...]],
confirmPass: ['', [...]]
}, {validators: this.checkPasswords}) // add a validator for the whole group
// ...
and then the validator could look like this:
checkPasswords: ValidatorFn = (group: AbstractControl): ValidationErrors | null => {
let pass = group.get('password').value;
let confirmPass = group.get('confirmPassword').value
return pass === confirmPass ? null : { notSame: true }
}
Showing the validation error could then be done like this:
*ngIf="addEditUserForm.hasError('notSame', 'passwords')"
Of course you don't need to have a nested group, but it's better to not have the custom validator fire every time when any changes happen to the form. This way it's only fired when changes happen to this inner form group.
I had some problems implementing this, and when i got it implemented with a custom validator and errorStateMatcher, i got the problem that the formbuilder.group function were deprecated, but after some inspection I found that my validator had to change to comply to the group function.
The Controller looks like this:
// ...
addEditUserForm: FormGroup;
passwordMatcher = new ComparisonErrorStateMatcher();
constructor(private formBuilder: FormBuilder) {
this.addEditUserForm = this.formBuilder.group({
password: ['', Validators.required],
confirmPass: ['', Validators.required],
}, { validators: [MatchValidator.validate] }); // add validator to group
}
// ...
My validator looked like this:
export class MatchValidator {
// (group: AbstractControl) is the form group but needs to be AbstractControl instead of (group: FormGroup) to remove deprecation warning.
static validate(group: AbstractControl): ValidationErrors | null {
const password = group.get('password')?.value;
const confirmPassword = group.get('confirmPass')?.value;
return password === confirmPass ? null : { notMatching: true }
};
}
And my ErrorStateMatcher looked like this:
export class ComparisonErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const invalidCtrl = !!(control && control.invalid && control.parent?.dirty);
const invalidParent = !!(control && control.parent && control.parent.invalid && control.parent.dirty);
return (invalidCtrl || invalidParent) && (control?.touched ?? false);
}
}
Lastly the HTML would have to look something like this:
<form [formGroup]="addEditUserForm">
<mat-form-field >
<mat-label>password</mat-label>
<input formControlName="password"
matInput />
<mat-error *ngIf="newPasswordForm.hasError('required')">
password is required
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>confirm password</mat-label>
<input formControlName="confirmPass"
[errorStateMatcher]="passwordMatcher"
matInput />
<mat-error *ngIf="newPasswordForm.hasError('notMatching')">
passwords don't match
</mat-error>
</mat-form-field>
</form>
This creates a form with two input fields that are required to match.
This is what eventually worked for me -
this.addEditUserForm = this.builder.group({
firstName: ['', Validators.required],
lastName: ['', Validators.required],
title: ['', Validators.required],
email: ['', Validators.required],
password: ['', Validators.required],
confirmPass: ['', Validators.required]
},{validator: this.checkIfMatchingPasswords('password', 'confirmPass')});
checkIfMatchingPasswords(passwordKey: string, passwordConfirmationKey: string) {
return (group: FormGroup) => {
let passwordInput = group.controls[passwordKey],
passwordConfirmationInput = group.controls[passwordConfirmationKey];
if (passwordInput.value !== passwordConfirmationInput.value) {
return passwordConfirmationInput.setErrors({notEquivalent: true})
}
else {
return passwordConfirmationInput.setErrors(null);
}
}
}