angular / components

Component infrastructure and Material Design components for Angular
https://material.angular.io
MIT License
24.37k stars 6.75k forks source link

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

Open Jbz797 opened 6 years ago

Jbz797 commented 6 years ago

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 commented 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?

Jbz797 commented 6 years ago

Okay, I understand the problem. Thanks.

Is it better like this ?

willshowell commented 6 years ago

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 commented 6 years ago

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

willshowell commented 6 years ago

@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);
    }
  }
}
kamok commented 6 years ago

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 commented 6 years ago

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));
    }
    }
  2. 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>
  3. 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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

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

nhducseuit commented 6 years ago

ErrorStateMatcher doesn't work with custom MatFormFieldControl

jssumith commented 6 years ago

look at this blog https://www.c-sharpcorner.com/article/angular-material-design-components-with-reactive-form-part-2/

AliAdravi commented 5 years ago

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 commented 4 years ago

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 commented 4 years ago
  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[bot] commented 2 years ago

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 commented 2 years ago

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 commented 1 year ago

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 commented 1 year ago

Screenshot 2023-01-19 at 08 34 57