I am having some trouble getting validation errors to display on a model driven form with Angular (v4.3.6).
In my model, I have the following:
this.registerForm = formBuilder.group({
'email':[null,Validators.compose([Validators.required, ValidateEmail])],
'firstName':[null, Validators.required],
'lastName':[null, Validators.required],
'passwordGroup': formBuilder.group({
'password':[null, Validators.compose([Validators.required,Validators.minLength(8)])],
'passwordConfirmation':[null, Validators.required],
},{validator: ValidatePasswordConfirmation})
});
The ValidatePasswordConfirmation custom validator referenced is as follows:
export function ValidatePasswordConfirmation(group: FormGroup) {
if(group.value.password !== group.value.passwordConfirmation){
return { 'no-match':true };
}
return null;
}
Lastly, in my template I have the following:
<md-form-field>
<input mdInput name="passwordConfirmation" placeholder="Confirm password" [formControl]="registerForm.controls['passwordGroup'].controls['passwordConfirmation']" [(ngModel)]="model.passwordConfirmation" type="password">
<md-error *ngIf="registerForm.controls['passwordGroup'].controls['passwordConfirmation'].hasError('required')">
Password confirmation is required
</md-error>
<md-error *ngIf="registerForm.controls['passwordGroup'].hasError('no-match')">
Passwords don't match
</md-error>
</md-form-field>
However, the md-error governed by the 'no-match' error never shows up. So, to debug this on the page I added the following:
no-match = {{registerForm.controls['passwordGroup'].hasError('no-match')}}
invalid = {{registerForm.controls['passwordGroup'].invalid}}
Unsurprisingly, the debug lines show true/false as you would expect. However, the 'md-error' is never displayed... except for when the 'required' error is shown. I have a feeling that the issue is due to the [formControl] referring to the passwordConfirmation FormControl, and so without that being invalid, the md-error is not shown. However, I would like this error to display when the outer FormGroup is invalid.
Any pointers on where I'm going wrong here would be really helpful!
Finally, I have tried a few other ways around this such as setting the error on the PasswordConfirmation FormControl, which works, but I would like to know why my current implementation is failing.
I found that I was able to solve the exact same issue using a custom ErrorStateMatcher (the default one requires the control to be in an invalid state before showing any errors)
export class ParentErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted = !!(form && form.submitted);
const controlTouched = !!(control && (control.dirty || control.touched));
const controlInvalid = !!(control && control.invalid);
const parentInvalid = !!(control && control.parent && control.parent.invalid && (control.parent.dirty || control.parent.touched));
return isSubmitted || (controlTouched && (controlInvalid || parentInvalid));
}
}
This is exposed as a variable on my page component like so...
@Component({
templateUrl: 'register.page.component.html',
styleUrls: ['register.page.styles.css']
})
export class RegisterPageComponent implements OnInit {
registerForm: FormGroup;
parentErrorStateMatcher = new ParentErrorStateMatcher();
// Accessors
get name() { return this.registerForm.get('name'); }
get email() { return this.registerForm.get('email'); }
get passwords() { return this.registerForm.get('passwords'); }
get password() { return this.registerForm.get('passwords.password'); }
get confirmPassword() { return this.registerForm.get('passwords.confirmPassword'); }
...
With this form...
this.registerForm = this.formBuilder.group({
name: ['', [
Validators.required,
Validators.maxLength(256)]
],
email: ['', [
Validators.email,
Validators.required,
Validators.maxLength(256)]
],
passwords: this.formBuilder.group({
password: ['', [
Validators.required,
Validators.maxLength(128)
]],
confirmPassword: ['', [
Validators.required
]]
},
{
validator: CustomValidators.doNotMatch('password', 'confirmPassword')
}),
});
And this mark-up for the password fields (see errorStateMatcher and the last mat-error for the confirmPassword input control)...
<div formGroupName="passwords">
<mat-form-field class="full-width">
<input matInput placeholder="Password" type="password" name="password" id="password" formControlName="password" required/>
<mat-error *ngIf="password.errors && password.errors.required">
Please enter your password
</mat-error>
<mat-error *ngIf="password.errors && password.errors.maxLength">
Password must be less than 128 characters long
</mat-error>
</mat-form-field>
<mat-form-field class="full-width">
<input matInput placeholder="Confirm Password" type="password" name="confirmPassword" id="confirmPassword" formControlName="confirmPassword" required
[errorStateMatcher]="parentErrorStateMatcher"/>
<mat-error *ngIf="confirmPassword.errors && confirmPassword.errors.required">
Please confirm your password
</mat-error>
<mat-error *ngIf="passwords.errors && passwords.errors.doNotMatch">
Passwords do not match
</mat-error>
</mat-form-field>
</div>
Feels more marginally more confusing than when I wasn't using Material but I'm happy for the trade-off and really the only extra code is the custom matcher :)
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