I'm trying to add custom password validation to a password field. The password has to have a minimum of 8 characters and meet at least two of the criteria below but does not have to meet all four:
I've got part of the validation working but am having an issue where if the password is 8+ characters long but does not meet at least two of the criteria above, the error message will not show. The error regarding the characters will only display if the password is less than 8 characters.
I've done a search across SO and haven't had success implementing answers to similar questions. I suspect the issue I'm having has to do with my custom validation function not being associated with the password in ngModel.
The Question: How can I get the error message to show on the form field when the password is 8+ characters long but does NOT meet the character requirements above?
Here's the associated code.
From user-form.html:
<mat-form-field *ngIf="newPassword" fxFlex="100%">
<input matInput #password="ngModel" placeholder="Password" type="password" autocomplete="password"
[(ngModel)]="model.password" name="password" minlength="8" (keyup)="validatePassword(model.password)" required>
<mat-error *ngIf="invalidPassword">
Password must contain at least two of the following: numbers, lowercase letters, uppercase letters, or special characters.
</mat-error>
<mat-error *ngIf="password.invalid && (password.dirty || password.touched)">
<div *ngIf="password.errors.required">
Password is required
</div>
<div *ngIf="password.errors.minlength">
Password must be at least 8 characters
</div>
</mat-error>
</mat-form-field>
From user-form.component.ts:
export class UserFormComponent implements OnInit {
@Input()
user: User;
public model: any;
public invalidPassword: boolean;
constructor() {}
ngOnInit() {
this.model = this.user;
}
passwordFails(checks: boolean[]): boolean {
let counter = 0;
for (let i = 0; i < checks.length; i++) {
if (checks[i]) {
counter += 1;
}
}
return counter < 2;
}
validatePassword(password: string) {
let hasLower = false;
let hasUpper = false;
let hasNum = false;
let hasSpecial = false;
const lowercaseRegex = new RegExp("(?=.*[a-z])");// has at least one lower case letter
if (lowercaseRegex.test(password)) {
hasLower = true;
}
const uppercaseRegex = new RegExp("(?=.*[A-Z])"); //has at least one upper case letter
if (uppercaseRegex.test(password)) {
hasUpper = true;
}
const numRegex = new RegExp("(?=.*\\d)"); // has at least one number
if (numRegex.test(password)) {
hasNum = true;
}
const specialcharRegex = new RegExp("[!@#$%^&*(),.?\":{}|<>]");
if (specialcharRegex.test(password)) {
hasSpecial = true;
}
this.invalidPassword = this.passwordFails([hasLower, hasUpper, hasNum, hasSpecial]);
}
}
I played around with this further to see if I could figure out how to use the Validators in Angular. I was finally able to do that with these updates:
Create password-validator.directive.ts:
import { Directive, forwardRef, Attribute} from "@angular/core";
import { Validator, AbstractControl, NG_VALIDATORS} from "@angular/forms";
@Directive({
selector: '[validatePassword]',
providers: [
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => PasswordValidator), multi: true}
]
})
export class PasswordValidator implements Validator {
constructor (
@Attribute('validatePassword')
public invalidPassword: boolean
) {}
validate(ctrl: AbstractControl): {[key: string]: any} {
let password = ctrl.value;
let hasLower = false;
let hasUpper = false;
let hasNum = false;
let hasSpecial = false;
const lowercaseRegex = new RegExp("(?=.*[a-z])");// has at least one lower case letter
if (lowercaseRegex.test(password)) {
hasLower = true;
}
const uppercaseRegex = new RegExp("(?=.*[A-Z])"); //has at least one upper case letter
if (uppercaseRegex.test(password)) {
hasUpper = true;
}
const numRegex = new RegExp("(?=.*\\d)"); // has at least one number
if (numRegex.test(password)) {
hasNum = true;
}
const specialcharRegex = new RegExp("[!@#$%^&*(),.?\":{}|<>]");
if (specialcharRegex.test(password)) {
hasSpecial = true;
}
let counter = 0;
let checks = [hasLower, hasUpper, hasNum, hasSpecial];
for (let i = 0; i < checks.length; i++) {
if (checks[i]) {
counter += 1;
}
}
if (counter < 2) {
return { invalidPassword: true }
} else {
return null;
}
}
}
Update in user-form.component.html:
<mat-form-field *ngIf="newPassword" fxFlex="100%">
<input matInput #password="ngModel" placeholder="Password" type="password" autocomplete="password"
[(ngModel)]="model.password" name="password" minlength="8" validatePassword="password" required>
<mat-error *ngIf="password.invalid && (password.dirty || password.touched)">
<div *ngIf="password.errors.invalidPassword">
Password must have two of the four: lowercase letters, uppercase letters, numbers, and special characters
</div>
<div *ngIf="password.errors.required">
Password is required
</div>
<div *ngIf="password.errors.minlength">
Password must be at least 8 characters
</div>
</mat-error>
</mat-form-field>
Update in user-form.module.ts:
import {PasswordValidator} from "../password-validator.directive"; //imported to modules
@NgModule({
imports: [
//some modules
],
declarations: [
// some modules
PasswordValidator // added this to declarations
],
exports: [
// stuff
],
providers: [
//stuff
]
})
export class UserFormModule { }