2

Both mat-error show when only one error appeared.

I'm trying to make custom validators with mat-error. Both input for email and confirm password are red when each of them has a true value for hasError('').

I think my MyErrorStateMatcher class logic is wrong somehow. Please help! I have tried anything I could. Thank you in advance!

Image

As you can see in the image. When confirmPassword throws an error, the email field is also red.


My ErrorStateMatcher:

export class MyErrorStateMatcher implements ErrorStateMatcher {
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    const invalidCtrl = !!(control && control.invalid && (control.dirty || control.touched));
    const invalidParent = !!(control && (control.parent.hasError('notTheSame') || control.parent.hasError('emailUsed')));

    return ((invalidCtrl || invalidParent));
  }
}

HTML: (focus on email and confimPassword)

<form [formGroup]="signupForm" (ngSubmit)="signup(signupForm)">
        <mat-form-field style="width: 100%;">
          <input matInput formControlName="email" placeholder="Email address" type="email" [errorStateMatcher]="matcher" required>
          <mat-error *ngIf="signupForm.controls.email.hasError('required')">Email required!</mat-error>
          <mat-error *ngIf="signupForm.controls.email.hasError('email')">Email invalid!</mat-error>
          <mat-error *ngIf="signupForm.hasError('emailUsed')">This email already exists!</mat-error>
        </mat-form-field>
        <mat-form-field style="width: 100%;">
          <input matInput formControlName="username" placeholder="User name" (blur)="signupForm.value.username != null ? isValidUsername(signupForm.value.username) : ''" required />
          <mat-error>Please enter your new username!</mat-error>
          <mat-error *ngIf="usernameInvalid">Username already exists!</mat-error>
        </mat-form-field>
        <mat-form-field style="width: 100%;">
          <input matInput formControlName="password" placeholder="New password" [type]="show ? 'text' : 'password'" required />
          <mat-icon matSuffix (click)="show = !show" style="cursor: pointer;">{{show ? 'visibility' : 'visibility_off'}}</mat-icon>
          <mat-error>Please enter your password!</mat-error>
        </mat-form-field>
        <mat-form-field style="width: 100%;">
          <input matInput formControlName="confirmPassword" placeholder="Confirm password" type="password" [errorStateMatcher]="matcher" required>
          <mat-error *ngIf="signupForm.controls.confirmPassword.hasError('required')">Please confirm your password!</mat-error>
          <mat-error *ngIf="signupForm.hasError('notTheSame') && signupForm.value.confirmPassword != ''">Password is not the same!</mat-error>
        </mat-form-field>
        <br>
      <button mat-raised-button class="sessionBtn" color="primary" [disabled]="signupForm.invalid">Submit!</button>
</form>

TS:

signupForm = new FormGroup({
    firstName: new FormControl(),
    lastName: new FormControl(),
    email: new FormControl('', [
      Validators.required,
      Validators.email
    ]),
    username: new FormControl(),
    password: new FormControl('', [
      Validators.required
    ]),
    confirmPassword: new FormControl('', [
      Validators.required
    ])
  }, { validators: [this.checkPassword, this.checkExistingEmail] });
  matcher = new MyErrorStateMatcher();

/////////Custom validator////////

  checkPassword(signupForm: FormGroup) {
    let password = signupForm.value.password;
    let confirmPassword = signupForm.value.confirmPassword;

    return password === confirmPassword ? null : { notTheSame: true };
  }

  checkExistingEmail(signupForm: FormGroup) {
    let inputEmail = signupForm.value.email;
    let dbEmail = "test@test.com";

    return inputEmail !== dbEmail ? null: { emailUsed: true };
  }

Error happend with input email and input confirmPassword, both have [errorStateMatcher]="matcher"

Nhok V
  • 566
  • 3
  • 11
  • apart from @G. Tranter answer another approach is you can use pattern validator and pass regex to check duplicate email – indrajeet Jul 03 '19 at 20:34
  • You must understand the aim of has a customErrorMatches. see this great link https://itnext.io/materror-cross-field-validators-in-angular-material-7-97053b2ed0cf: A `` only show if the input inside the `` is invalid. Using a customErrorMatches you can say that the mat-error is showed in a different state (in the link show when the form has an error and the control is dirty ¡althought the control is valid!). In your code, all input that has `[errorStateMatcher]="matcher"` becomes invalid if is invalid the form, so remove the `[errorStateMatcher]` in your email input – Eliseo Jul 03 '19 at 20:55
  • @Eliseo it might be a good idea to remove them, but I need it for later to make a custom validator for the email itself to check duplicate email in the database. Is there a way to combine all custom validator into one errorStateMatcher? – Nhok V Jul 03 '19 at 22:48
  • If your checkExistingEmail only depend of email, put the validator in the email (not in the form), if depends others fields, you can use two different customErrorMatcher – Eliseo Jul 04 '19 at 06:07
  • You mean do it like this?: ```email: new FormControl('', [ Validators.required, Validators.email, this.checkExistingEmail <== put this here? ])``` Is it a good practice to make 2 different customErrorMatcher. What if I have many validators? – Nhok V Jul 04 '19 at 14:11
  • @NhokV, the customErrorMatcher is because you want to show a in a control that is **valid**, you needn't a customErrorMatcher if you want show a in a control that is invalid. If you has severals controls that you want to show an error when is valid, you can use serverals customsErrorMatcher or only one, see my answer about this. But **repeat**: in your case you only need control the password – Eliseo Jul 04 '19 at 17:35

2 Answers2

3

The email address field shows an error because the error state matcher checks the parent - which is the form - which is in error because the password fields do not match. You need to use different error state matchers for the email field and password fields because the conditions are different - email does not need to be in error if the password fields don't match.

G. Tranter
  • 16,766
  • 1
  • 48
  • 68
  • Is there any way to combine them with one errorStateMatcher? – Nhok V Jul 03 '19 at 22:46
  • Not a "good" way that I can see. You can always hack - like add a 'name' property to the controls that you can check in the matcher. E.g. `signupForm['confirmPassword']['name'] = 'confirmPassword'` and in the matcher `return (invalidCtrl || (control['name'] === 'confirmPassword' && invalidParent))`. Or (a little better) you can create separate instances of the same matcher class, and add a name property to the instance using the constructor. But IMO, one-size-fits-all approaches are more difficult to read/understand, less maintainable, and more prone to bugs. – G. Tranter Jul 04 '19 at 14:02
3

Creating a customErrorMatcher

Well, if we want to show an error in a <mat-form-field> when the input is valid, we use a customErrorMatcher.

This is a class like

class CrossFieldErrorMatcher implements ErrorStateMatcher {
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    //when we want to show the error
    return true 
    //when we want not show the error
    return false
  }
}

Normally we has in our component

  errorMatcher=new CrossFieldErrorMatcher()
  //and the .html
  <mat-form-field>
    <input matInput formControlName='verifyPassword' 
        [errorStateMatcher]="errorMatcher">
    <mat-error *ngIf="....">
      Passwords do not match!
    </mat-error>
  </mat-form-field>

Well, we are change the things a bit, adding a constructor in our customErrorMatcher

class CrossFieldErrorMatcher implements ErrorStateMatcher {
  constructor(private name:string){}  //<--add a constructor
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    //when we want to show the error, but we can use "name"
    return true 
    //when we want not show the error
    return false
  }
}

Then, our component becomes

  errorMatcher(name:string)
  {
    return new CrossFieldErrorMatcher(name);
  }

  //and the .html
  <mat-form-field>
    <input matInput formControlName='verifyPassword' 
        [errorStateMatcher]="errorMatcher('password')">
    <mat-error *ngIf="....">
      Passwords do not match!
    </mat-error>
  </mat-form-field>
Eliseo
  • 50,109
  • 4
  • 29
  • 67