Reputation: 81
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.
Upvotes: 7
Views: 7371
Reputation: 315
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 :)
Upvotes: 9
Reputation: 471
This code is working perfectly fine in my case. Code structure is as follows:
// component.ts
import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms';
function validate(v: FormGroup): any {
let a = k.get('a');
let b = k.get('b');
if (a.value != undefined && (b.value == undefined || a.value == "none" || b.value == " ")) {
return { ab: true };
}
return null;
}
@Component({
})
export class component implements OnInit, OnDestroy {
constructor(){}
ngOnInit() {
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})
});
}
}
HTML error message display if condition, is used in same way as mentioned above in your code.
Upvotes: 0
Reputation: 16384
I already met this problem with custom validation like },{validator: ValidatePasswordConfirmation}
on Angular Material inputs, and it seems like it's Material's issue. Here is my issue at Angular Material page: https://github.com/angular/material2/issues/7084. It has no answer, so the issue is still happening. My decision was to use validation after submitting form, it's not very beautiful and right, but it's working. Hope the issue will be solved in the next release.
Or if you want a real-time validation, you can do something like this:
checkArePasswordsValid() {
this.passwordsAreEqual = this.passwordValue === this.passwordConfirmationValue ? true : false;
}
and use this boolean value for validation.
Upvotes: 0