I'm using Angular Material on a project right now, trying to implement the formly library with custom types. Right now I'm working on Signup, Login, Password Forget and Password Reset Feature. Since all of those have similar form input, I want to configure some custom types. One of the custom types I'm trying to do is password + password verify input feature.
This means I want to have a password input with a text input to verify it. I want to use this custom type inside the signup feature and inside the password reset feature.
To implement this, I created a custom type component with angular reactive forms using angular material:
formly-verify-password.component.html
<ng-container *ngIf="setValidationMsg$ | async">
<form [formGroup]="passwordForm">
<!-- Password Input -->
<mat-form-field appearance="outline">
<mat-label [transloco]="'forms.PasswordInputLabel'"></mat-label>
<input
matInput
type="password"
formControlName="password"
autocomplete="on"
[errorStateMatcher]="errorMatcher"
[type]="passwordHidden ? 'password' : 'text'"
/>
<mat-icon
style="cursor: pointer"
matSuffix
(click)="passwordHidden = !passwordHidden"
>{{ passwordHidden ? 'visibility_off' : 'visibility' }}</mat-icon
>
<mat-error
*ngIf="!passwordForm.get('password')?.errors?.['required'] && passwordForm.get('password')?.errors?.['passwordTooWeak']"
[innerHtml]="sanitize(getMissingPasswordStrength())"
>
</mat-error>
<mat-error
*ngIf="passwordForm.get('password')?.errors?.['required']"
[innerHtml]="passwordRequired"
>
</mat-error>
</mat-form-field>
<!-- Password Repeat Input -->
<mat-form-field appearance="outline">
<mat-label [transloco]="'forms.PasswordRepeatInputLabel'"></mat-label>
<input
type="password"
matInput
formControlName="passwordRepeat"
autocomplete="on"
[errorStateMatcher]="errorMatcher"
[type]="passwordHidden ? 'password' : 'text'"
/>
<mat-error
*ngIf="!passwordForm.get('passwordRepeat')?.errors?.['required'] &&
passwordForm.get('passwordRepeat')?.errors?.['notmatched']"
[innerHtml]="passwordRepeatNotMatching"
>
</mat-error>
<mat-error
*ngIf="passwordForm.get('passwordRepeat')?.errors?.['required']"
[innerHtml]="passwordRepeatRequired"
>
</mat-error>
</mat-form-field>
</form>
</ng-container>
formly-verify-password.component.ts
@Component({
selector: 'formly-password-verify',
standalone: true,
imports: [
TranslocoModule,
MaterialModule,
FormsModule,
ReactiveFormsModule,
FormlyModule,
NgIf,
AsyncPipe,
MaterialModule,
FormsModule,
TranslocoModule,
],
templateUrl: './formly-password-verify.component.html',
styles: [
`
form {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
`,
],
})
export class FormlyPasswordVerifyComponent
extends FieldType<FieldTypeConfig>
implements OnInit, OnDestroy
{
formBuilder = inject(FormBuilder);
sanitizer = inject(DomSanitizer);
translocoService = inject(TranslocoService);
errorMatcher = new ErrorStateMatcher();
onDestroy = new Subject<void>();
passwordHidden = true;
passwordForm = this.formBuilder.group({
password: ['', Validators.required],
passwordRepeat: ['', Validators.required],
});
passwordRequired = this.sanitizer.bypassSecurityTrustHtml('');
passwordRepeatRequired = this.sanitizer.bypassSecurityTrustHtml('');
passwordRepeatNotMatching = this.sanitizer.bypassSecurityTrustHtml('');
setValidationMsg$ = combineLatest([
this.translocoService.selectTranslate('formErrors.PasswordRequired'),
this.translocoService.selectTranslate('formErrors.PasswordRepeatRequired'),
this.translocoService.selectTranslate('formErrors.PasswordNotMatching'),
]).pipe(
tap(([requiredMsg, repeatRequiredMsg, passwordRepeatNotMatching]) => {
this.passwordRequired =
this.sanitizer.bypassSecurityTrustHtml(requiredMsg);
this.passwordRepeatRequired =
this.sanitizer.bypassSecurityTrustHtml(repeatRequiredMsg);
this.passwordRepeatNotMatching = passwordRepeatNotMatching;
})
);
ngOnInit(): void {
this.passwordForm.valueChanges
.pipe(debounceTime(0), takeUntil(this.onDestroy))
.subscribe((value) => {
if (this.passwordForm.valid === true) {
this.formControl.setValue(value.password);
}
});
this.passwordForm
.get('passwordRepeat')
?.addAsyncValidators(
createPasswordMatchingValidator(this.passwordForm, 'password')
);
this.passwordForm
.get('password')
?.addAsyncValidators(createPasswordStrengthValidator());
this.passwordForm
.get('password')
?.valueChanges.pipe(takeUntil(this.onDestroy))
.subscribe(() =>
this.passwordForm.get('passwordRepeat')?.updateValueAndValidity()
);
}
sanitize(html: string) {
return this.sanitizer.bypassSecurityTrustHtml(html);
}
getMissingPasswordStrength() {
const password = this.passwordForm.get('password')?.value;
if (password !== null && password !== undefined) {
const errors = getPasswordStrengthMistake(password);
const errorMessage = errors.reduce(
(accumulator: string, current: string, index: number) => {
switch (current) {
case PasswordStrengthComplexity.NUMBER:
accumulator += this.translocoService.translate(
'formErrors.NumberMissing'
);
break;
case PasswordStrengthComplexity.MIN:
accumulator += this.translocoService.translate(
'formErrors.CompareCharacterMinNumbers',
{ currentNumber: password.length, allowedNumber: 12 }
);
break;
case PasswordStrengthComplexity.MAX:
accumulator += this.translocoService.translate(
'formErrors.CompareCharacterMaxNumbers',
{ currentNumber: password.length, allowedNumber: 32 }
);
break;
}
if (index !== errors.length - 1) {
accumulator += ', ';
}
return accumulator;
},
''
);
return this.translocoService.translate('formErrors.Error', {
text: errorMessage,
});
}
return '';
}
ngOnDestroy(): void {
this.onDestroy.next();
}
}
The Output value of the custom type is just the password text, if its validated correctly.
The problem: When the user presses the submit button on the formly form, containing this custom type, the validation inside this custom type doesn't get triggered, since its outside the scope of the formly parents validation. Is there a way to propagate the validation process into the angular reactive forms formgroup in the custom type? I don't want to have the validation process outside, since IMO this should be the feature from the custom type itself.
