I have developed a form in Reactive Forms where the user has the possibility to set a new password. He enters the old and the new password and confirms the new password. I have considered the following cases:
- New password must not contain the old password
- New password and confirmation password are matched
The development works as well. Now I have one thing that is not nice. The validation is triggered only when I change something in the input field. But now I want to change the new password when I want to confirm the password, so I know if there is an error or not. I have read that this can be done with the function updateValueAndValidity. Do you know how to do this?
My Code:
// TS
changePasswordForm: FormGroup;
submitted = false;
ngOnInit() {
// To initialize forms
this.initChangePasswordForm();
}
// Creation of the changePasswordForm
private initChangePasswordForm() {
// General
this.changePasswordForm = this.formBuilder.group({
old_password: [null, Validators.pattern('^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[^\\w\\s]).{8,}$')],
new_password: [null, this.customComparePasswordValidator],
confirm_password: [null, Validators.required]
});
}
customComparePasswordValidator(control: FormControl) {
const newPassword = control.value;
if (newPassword && newPassword.length) {
const oldPassword = control.parent.value.old_password;
if (oldPassword && oldPassword.length && newPassword.toLowerCase().includes(oldPassword.toLowerCase())) {
return { newPasswordIncludesOldPassword: true };
}
const pattern = new RegExp('^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[^\\w\\s]).{8,}$');
if (!pattern.test(newPassword)) {
return { newPasswordInvalid: true };
}
} else {
return { required: true };
}
}
// HTML
<input matInput type="password" placeholder="aktuelles Passwort" formControlName="old_password" required>
<p *ngIf="changePasswordForm.get('old_password').invalid && (changePasswordForm.get('old_password').dirty || changePasswordForm.get('old_password').touched)">
<mat-error class="custom-validation-error" *ngIf="changePasswordForm.get('old_password').hasError('required')">aktuelles Passwort eingeben</mat-error>
<mat-error class="custom-validation-error" *ngIf="changePasswordForm.get('old_password').hasError('pattern')">8 oder mehr Zeichen mit einer Mischung aus Groß- und Kleinbuchstaben, Ziffern und Symbolen verwenden</mat-error>
</p>
<input matInput type="password" placeholder="neues Passwort" formControlName="new_password" required>
<p *ngIf="changePasswordForm.get('new_password').invalid && (changePasswordForm.get('new_password').dirty || changePasswordForm.get('new_password').touched)">
<mat-error *ngIf="changePasswordForm.get('new_password').hasError('required')">neues Passwort eingeben</mat-error>
<mat-error *ngIf="changePasswordForm.get('new_password').hasError('newPasswordInvalid')">8 oder mehr Zeichen mit einer Mischung aus Groß- und Kleinbuchstaben, Ziffern und Symbolen verwenden</mat-error>
<mat-error *ngIf="changePasswordForm.get('new_password').hasError('newPasswordIncludesOldPassword')">Neues und altes Passwort dürfen nicht gleich sein</mat-error>
</p>
<input matInput type="password" placeholder="Passwort bestätigen" formControlName="confirm_password" appConfirmEqualValidator="new_password" required>
<p *ngIf="changePasswordForm.get('confirm_password').invalid && (changePasswordForm.get('confirm_password').dirty || changePasswordForm.get('confirm_password').touched)">
<mat-error *ngIf="changePasswordForm.get('confirm_password').hasError('required')">Passwort erneut eingeben</mat-error>
<mat-error *ngIf="changePasswordForm.get('confirm_password').hasError('notEqual') && !changePasswordForm.get('confirm_password').hasError('required')">Passwort stimmt nicht überein</mat-error>
</p>
My StackBlitz: https://stackblitz.com/edit/example-angular-material-reactive-form-sr1keu?file=app/app.component.html
CodePudding user response:
As you say, a validator only "check" when you change the FormControl. So you can "force" to validity one control when change the second. So, e.g. you simply can do, in the FormControl "new_password" write
<input matInput ... formControlName="new_password"
(input)="changePasswordForm.get('confirm_password').updateValueAndValidity()" />
(idem with the other FormControl)
You can also subscribe to valueChanges after create the form -don't forget unsubscribe-
private initChangePasswordForm() {
// After create the formGroup
this.changePasswordForm = this.formBuilder.group({..}
//subscribe to changes "new_password"
this.changePasswordForm.get('new_password').valueChanges
.subscribe(_=>{
this.changePasswordForm.get('confirm_password').updateValueAndValidity()
})
Another approach is create a customValidator over the whole form. As you use material, You need use setErrors. The validator can be like
validatorRepeatPassword(form: FormGroup) {
const errorOldPassword = {};
const errorNewPassword = {};
const errorConfirmPassword = {};
const pattern = new RegExp(
'^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[^\\w\\s]).{8,}$'
);
if (!form.value.old_password) errorOldPassword['required'] = true;
else {
if (!pattern.test(form.value.old_password))
errorOldPassword['pattern'] =
'8 or more characters with a mixture of upper and lower case letters,use numbers and symbols';
}
if (!form.value.new_password) errorNewPassword['required'] = true;
else {
if (!pattern.test(form.value.new_password))
errorNewPassword['pattern'] =
'8 or more characters with a mixture of upper and lower case letters,use numbers and symbols';
if (
form.value.old_password &&
form.value.new_password
.toLowerCase()
.includes(form.value.old_password.toLowerCase())
)
errorNewPassword['newPasswordIncludesOldPassword'] = true;
}
if (!form.value.confirm_password) errorConfirmPassword['required'] = true;
else {
if (
form.value.new_password &&
form.value.confirm_password != form.value.new_password
)
errorConfirmPassword['notEqual'] = true;
}
form.controls.old_password.setErrors(
Object.keys(errorOldPassword).length ? errorOldPassword : null
);
form.controls.new_password.setErrors(
Object.keys(errorNewPassword).length ? errorNewPassword : null
);
form.controls.confirm_password.setErrors(
Object.keys(errorConfirmPassword).length ? errorConfirmPassword : null
);
return {
...errorOldPassword,
...errorNewPassword,
...errorConfirmPassword,
};
}
And use
this.changePasswordForm = this.formBuilder.group(
{
old_password: null,
new_password: null,
confirm_password: null,
},
{ validator: this.validatorRepeatPassword }
);
The last aproach (I only make with new_password and confirm password is make the validator with the two controls. A function like
matchValidator(field: string,not:boolean=false) {
return (control: AbstractControl) => {
const parent = control.parent;
const controlCompare = parent ? parent.get(field) : null;
if (controlCompare && controlCompare.value && control.value) {
const invalid = controlCompare.value != control.value;
if (invalid!=controlCompare.invalid)
{
setTimeout(()=>{
controlCompare.updateValueAndValidity();
})
}
return invalid && !not ? { match: 'no match' } : null;
}
};
}
Allow us write, e.g.
this.changePasswordForm = this.formBuilder.group(
{
old_password: null,
new_password: [null,this.matchValidator('confirm_password',true)],
confirm_password: [null,this.matchValidator('confirm_password',true)]
}
);
See that you use a function to pass parameters -that should be fixed-