Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[input] Add example of using mat-error with parent formgroup validation #8513

Open
Jbz797 opened this issue Nov 17, 2017 · 20 comments
Open

[input] Add example of using mat-error with parent formgroup validation #8513

Jbz797 opened this issue Nov 17, 2017 · 20 comments
Labels
area: material/form-field docs This issue is related to documentation feature This issue represents a new feature or feature request rather than a bug or bug fix help wanted The team would appreciate a PR from the community to address this issue P4 A relatively minor issue that is not relevant to core functions

Comments

@Jbz797
Copy link

Jbz797 commented Nov 17, 2017

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 :

private matchEmail(AC: AbstractControl) {
    return AC.get('mail').value === AC.get('mailconfirm').value ? null : { mailmismatch: true };
}

this.administratifForm = this.fb.group({
        (...),
        mail: this.fb.control('', [Validators.required, Validators.email]),
        mailconfirm: this.fb.control('', [Validators.required]),
        (...),
    }, {
    validator: this.matchEmail,
    },
);

The template :

<mat-form-field>
    <input matInput placeholder="Vérification d'email" formControlName="mailconfirm">
    <mat-error *ngIf="administratifForm.get('mailconfirm').hasError('required')">
        Ce champ est requis
    </mat-error>
    <mat-error *ngIf="administratifForm.hasError('mailmismatch')">
        Les adresses mail ne correspondent pas
    </mat-error>
</mat-form-field>

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.

@willshowell
Copy link
Contributor

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?

@Jbz797 Jbz797 changed the title mat-error doesn't show with a custom validator <mat-error> doesn't show with an email maching validator Nov 17, 2017
@Jbz797
Copy link
Author

Jbz797 commented Nov 17, 2017

Okay, I understand the problem. Thanks.

Is it better like this ?

@willshowell
Copy link
Contributor

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.

@Jbz797 Jbz797 changed the title <mat-error> doesn't show with an email maching validator [input] Add example of using mat-error with parent formgroup validation Nov 17, 2017
@jelbourn jelbourn added docs This issue is related to documentation feature This issue represents a new feature or feature request rather than a bug or bug fix help wanted The team would appreciate a PR from the community to address this issue P4 A relatively minor issue that is not relevant to core functions labels Nov 17, 2017
@Jbz797
Copy link
Author

Jbz797 commented Nov 17, 2017

Thank you very much, I found the solution with this answer.

@willshowell
Copy link
Contributor

willshowell commented Nov 21, 2017

@ewaschen I'm answering your question from #4027 (comment) 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);
    }
  }
}

@kamok
Copy link

kamok commented Jan 4, 2018

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.

@kamok
Copy link

kamok commented Jan 4, 2018

Ok, I've followed the stack post linked by @Jbz797, and here's the TLDR version of that poorly written post.

  1. 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));
  }
}
  1. Then, add [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>
  1. Now, the mat-error that belongs to the GROUP will be rendered. In the example above, it's the last mat-error.

Thanks for this thread!

@kamok
Copy link

kamok commented Feb 6, 2018

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.

@vcartera81
Copy link

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

@willshowell
Copy link
Contributor

@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.

@vcartera81
Copy link

@willshowell you are right, I didn't thought of such approach :) thanks!

@nhducseuit
Copy link

ErrorStateMatcher doesn't work with custom MatFormFieldControl

@jssumith
Copy link

@AliAdravi
Copy link

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();
    }
      
  })
}

@worthy7
Copy link

worthy7 commented Jul 22, 2020

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?

@bejgumshirisha
Copy link

  1. Then, add [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.

Ok, I've followed the stack post linked by @Jbz797, and here's the TLDR version of that poorly written post.

  1. 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));
  }
}
  1. Then, add [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>
  1. Now, the mat-error that belongs to the GROUP will be rendered. In the example above, it's the last mat-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.

@angular-robot
Copy link
Contributor

angular-robot bot commented Feb 1, 2022

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.

@grimurd
Copy link

grimurd commented Mar 18, 2022

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.

See it on stackblitz

@micobarac
Copy link

micobarac commented Jan 19, 2023

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>

@micobarac
Copy link

Screenshot 2023-01-19 at 08 34 57

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: material/form-field docs This issue is related to documentation feature This issue represents a new feature or feature request rather than a bug or bug fix help wanted The team would appreciate a PR from the community to address this issue P4 A relatively minor issue that is not relevant to core functions
Projects
None yet
Development

No branches or pull requests