Open Jbz797 opened 6 years ago
mat-error
only shows when the FormControl is invalid, but you've added the validation to a parent FormGroup. You'll need to use a Custom Error Matcher to accomplish this.
I've been meaning to add an example for this because it's a common need to validate matching passwords/emails/etc. Would you mind repurposing this issue for tracking such an example?
Okay, I understand the problem. Thanks.
Is it better like this ?
How about [input] Add example of using mat-error with parent formgroup validation
See this answer for pretty much the same thing. I think it's a little more convoluted than necessary, but looks to get the job done. I'll try and add an official example soon.
Thank you very much, I found the solution with this answer.
@ewaschen I'm answering your question from https://github.com/angular/material2/issues/4027#issuecomment-346077541 over here to keep the discussion related to the issue topics
If it works, the way you've outlined (by checking dirty
and the two different possible validation errors) is totally fine. If you add more validation to the control though, you'll have to remember to add it to your errorStateMatcher too (like min length or special character required).
Another approach (forgive me for not testing this) would be to inject ErrorStateMatcher
and ||
your custom logic with it.
@Component({...})
export MyComponent {
constructor(private defaultMatcher: ErrorStateMatcher) { }
customErrorStateMatcher = {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const invalidParent = control && control.touched && control.parent.invalid;
return invalidParent || this.defaultMatcher.isErrorState(control, form);
}
}
}
I'm running into the same issue. Here's my Are Equals matcher. It does not use ErrorStateMatcher.
export function areEqual(group: FormGroup): Object {
let valid = false;
// return true if all values of group is the same
valid = Object.values(group.value).every( (val, _i, arr) => val === arr[0] );
if (valid) {
return null;
}else {
return { areEqual: true };
}
}
You use it in your FormGroup, as such.
passwords: this.fb.group({
password: ['', [Validators.required]],
password_verify: ['', [Validators.required]]
}, { validator: areEqual })
Is this why my mat-error
won't render? The values are definitely being returned correctly, as I've checked with replacing <mat-error>
with <p>
.
Here's where I'm using it in my html
<div formGroupName="passwords">
<mat-form-field>
<input matInput formControlName="password" placeholder="Password" type="password" required>
<mat-error *ngIf="register_password.invalid">{{ getRegisterPasswordError() }}</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput formControlName="password_verify" placeholder="Verify Password" type="password" required>
<mat-error *ngIf="register_password_verify.invalid">{{ getRegisterPasswordVerifyError() }}</mat-error>
</mat-form-field>
<mat-error class="form-group-error" *ngIf="register_password_group.invalid && !register_password_verify.pristine">{{ getRegisterPasswordGroupError() }}</mat-error>
</div>
The error only renders when it's outside of mat-form-field
, so I am left to doing some CSS hacking to get it look right. If I move the second mat-error
that's outside of mat-form-field
, to inside it, it won't render.
Ok, I've followed the stack post linked by @Jbz797, and here's the TLDR version of that poorly written post.
Add this class. You can add it to your current .ts
, or import it from somewhere else. Doesn't matter.
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));
}
}
[errorStateMatcher]="parentErrorStateMatcher"
to the template input
that needs the particular error. Add it to whichever mat-form-field
needs to render that parent error. Eg, for passwords
group with a password
control and password_verify
control, add it to password_verify
's input. Here's mine.
<mat-form-field>
<input matInput formControlName="password_verify" placeholder="Verify Password" type="password" required [errorStateMatcher]="parentErrorStateMatcher">
<mat-error *ngIf="register_password_verify.invalid">{{ getRegisterPasswordVerifyError() }}</mat-error>
<mat-error *ngIf="register_password_group.invalid && !register_password_verify.pristine">{{ getRegisterPasswordGroupError() }}</mat-error>
</mat-form-field>
mat-error
that belongs to the GROUP will be rendered. In the example above, it's the last mat-error
.Thanks for this thread!
I actually don't know what's the purpose of the submitted logic in that matcher class. it was causing some minor issues, where the formfield where you bind it to is showing up false positives (red when no error). I just removed it.
ErrorStateMatcher approach is not so convenient. Imagine that validation function resides in the component class (because it depends on a @input() field. And that the component does not use form at all (because it's not part of the form). Creating a new class that extends ErrorStateMatcher does not allow me to invoke that validation function. It would be nice to be able to connect a simple, unconditional *ngIf on mat-error tag and it should popup without extending nothing
@vcartera81 you don't necessarily need to extend and instantiate an ErrorStateMatcher class. You could very much do something like this:
// this is untested
@Component({...})
export MyComponent {
@Input() showError: boolean;
myErrorStateMatcher = {
isErrorState = () => this.showError;
}
}
I agree that it isn't the most convenient approach, but the ErrorStateMatcher is flexible enough to support a huge variety of use cases while maintaining the default error behavior defined in the spec.
@willshowell you are right, I didn't thought of such approach :) thanks!
ErrorStateMatcher doesn't work with custom MatFormFieldControl
I have tried all the above suggestions but nothing worked for me! Finally I wrote a method to call on submit button
save(data) {
this.markAsTouched(this.myForm); // touch all the controls
if(this.myForm.invalid)
return;
// Rest of the code to save
}
And here is the method which will call recursively:
markAsTouched(formGroup:FormGroup) {
Object.keys(formGroup.controls).forEach((key:any) =>{
let control = formGroup.get(key)
if(control instanceof FormGroup) {
this.markTouched(control)
} else if(control instanceof FormControl) {
(<FormControl>control).markAsTouched();
}
})
}
https://stackblitz.com/edit/angular-mlo6y6
Is this issue the same as my issue in this stackblitz? The red outline does not appear when in a dirty error state. Is this intentional or a bug?
- Then, add
[errorStateMatcher]="parentErrorStateMatcher"
to the templateinput
that needs the particular error. Add it to whichevermat-form-field
needs to render that parent error. Eg, forpasswords
group with apassword
control andpassword_verify
control, add it topassword_verify
's input. Here's mine.Ok, I've followed the stack post linked by @Jbz797, and here's the TLDR version of that poorly written post.
- Add this class. You can add it to your current
.ts
, or import it from somewhere else. Doesn't matter.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)); } }
- Then, add
[errorStateMatcher]="parentErrorStateMatcher"
to the templateinput
that needs the particular error. Add it to whichevermat-form-field
needs to render that parent error. Eg, forpasswords
group with apassword
control andpassword_verify
control, add it topassword_verify
's input. Here's mine.<mat-form-field> <input matInput formControlName="password_verify" placeholder="Verify Password" type="password" required [errorStateMatcher]="parentErrorStateMatcher"> <mat-error *ngIf="register_password_verify.invalid">{{ getRegisterPasswordVerifyError() }}</mat-error> <mat-error *ngIf="register_password_group.invalid && !register_password_verify.pristine">{{ getRegisterPasswordGroupError() }}</mat-error> </mat-form-field>
- Now, the
mat-error
that belongs to the GROUP will be rendered. In the example above, it's the lastmat-error
.Thanks for this thread!
@kamok : It is solved my issues but now when I submit the form even it is a valid form and control it is showing as error for confirm password field.So I removed submitted from the return and everything is working fine.But I wanted to know the use of submitted value.So could u please explain what is the use of submitted in that code.
Just a heads up that we kicked off a community voting process for your feature request. There are 20 days until the voting process ends.
Find more details about Angular's feature request process in our documentation.
I was able to get this workin with nested forms. It's a simple solution with a custom error matcher. It only works a single level down unfortunately but that was enough for my use case.
This may come late to the show, but here's a solution that solves the problem with submit state:
import { FormControl, FormGroupDirective, NgForm } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';
export class ParentErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const isSubmitted = form?.submitted;
const controlDirtyOrTouched = control?.dirty || control?.touched;
const controlInvalid = control?.invalid;
const parentDirtyOrTouched = control?.parent?.dirty || control?.parent?.touched;
const parentInvalid = control?.parent?.invalid;
return ((controlDirtyOrTouched && parentDirtyOrTouched) || isSubmitted) && (controlInvalid || parentInvalid);
}
}
import { ParentErrorStateMatcher } from '@shared/helpers/error-state.matcher';
@UntilDestroy()
@Component({
selector: 'cdb-user-add-update',
templateUrl: './user-add-update.component.html',
styleUrls: ['./user-add-update.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserAddUpdateComponent implements OnInit, OnDestroy {
matcher: ParentErrorStateMatcher;
ngOnInit() {
this.matcher = new ParentErrorStateMatcher();
}
}
<div formGroupName="passwordGroup" fxLayout="row" fxLayoutGap="20px">
<!-- Password -->
<div fxFlex="50" fxLayout="column">
<mat-form-field>
<input
matInput
type="password"
formControlName="password"
name="password"
placeholder="{{ 'User.Password' | transloco }}"
[required]="(isEditing$ | async) === false"
[dimmed]="type.value === AccessType.Service"
autocomplete="off"
[errorStateMatcher]="matcher"
/>
</mat-form-field>
<small *ngIf="password.errors?.required && password.touched" class="mat-warn">
{{ 'User.PasswordRequired' | transloco }}
</small>
<small *ngIf="passwordGroup.errors?.invalidPassword && password.touched" class="mat-warn">
{{ 'User.PasswordInvalid' | transloco : { constraints: passwordConstraints } }}
</small>
</div>
<!-- Password confirmation -->
<div fxFlex="50" fxLayout="column">
<mat-form-field>
<input
matInput
type="password"
formControlName="passwordConfirmed"
name="passwordConfirmed"
placeholder="{{ 'User.PasswordConfirmed' | transloco }}"
[required]="(isEditing$ | async) === false"
[dimmed]="type.value === AccessType.Service"
autocomplete="off"
[errorStateMatcher]="matcher"
/>
</mat-form-field>
<small *ngIf="passwordConfirmed.errors?.required && passwordConfirmed.touched" class="mat-warn">
{{ 'User.PasswordRequired' | transloco }}
</small>
<small
*ngIf="passwordGroup.errors?.invalidPasswordConfirmed && passwordConfirmed.touched"
class="mat-warn"
>
{{ 'User.PasswordInvalid' | transloco : { constraints: passwordConstraints } }}
</small>
<small *ngIf="passwordGroup.errors?.mismatchedPasswords && passwordConfirmed.touched" class="mat-warn">
{{ 'User.PasswordMismatch' | transloco }}
</small>
</div>
</div>
Bug, feature request, or proposal:
<mat-error>
doesn't show when I use an email maching validator for emails inputs.What is the expected behavior? To display the error.
What is the current behavior? No error is displayed
What are the steps to reproduce?
Set a custom validator for check if two emails inputs are equals, like this :
The template :
Which versions of Angular, Material, OS, TypeScript, browsers are affected? Angular 5.0.2, Material 5.0.0-rc0, MacOS Sierra, Firefox
Additional Information If i replace the
<mat-error>
tag by a<p>
tag (or anything else), it's work.