components: md-error with *ngIf doesn't work

Bug, feature request, or proposal:

Bug

What is the expected behavior?

boolean is passed to *ngIf and supposed to switch on when it is true

What is the current behavior?

md-error with *ngIf doesn’t work at all. It only responds on predefined directives like required.

What are the steps to reproduce?

<md-input-container> <input mdInput [(ngModel)]="sample" name="sample" (keyup)="validateInput()"> <md-error *ngIf="isError">{{errMsg}}</md-error> </md-input-container> Validate input on every keyup and set isError to true if the input is invalid.

What is the use-case or motivation for changing an existing behavior?

Which versions of Angular, Material, OS, browsers are affected?

angular 4.0.0, material 2.0.0-beta.3, macOS10.12.4, chrome

Is there anything else we should know?

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 3
  • Comments: 29 (12 by maintainers)

Most upvoted comments

I have problems making Reactive form with FormGroup display md-error. md-hint works with the same code.

Here’s a code and a working plunker: http://plnkr.co/edit/CR06nY?p=preview.

All validations work except the custom one. My workaround for now is to just use 'md-hint` with red font.

Take a look at last two inputs. Password validation error doesn’t appear.

    <form fxFlex="80"
          fxLayout="column"
          id="updateProfileForm"
          name="updateProfileForm"
          [formGroup]="updateProfileForm"
          (ngSubmit)="updateProfile( $event, updateProfileForm.value )"
          novalidate>
      <md-input-container fxFlex="0 0 auto">
        <input mdInput type="text" formControlName="firstName" placeholder="First name" required>
        <md-error>First name is required</md-error>
      </md-input-container>

      <md-input-container fxFlex="0 0 auto">
        <input mdInput type="text" formControlName="lastName" placeholder="Last name" required>
        <md-error>Last name is required</md-error>
      </md-input-container>

      <md-input-container fxFlex="0 0 auto">
        <input mdInput type="email" formControlName="email" placeholder="Email" required>
        <md-error *ngIf="updateProfileForm.get('email').hasError('required')">Email is required</md-error>
        <md-error *ngIf="updateProfileForm.get('email').hasError('email')">Please enter a valid email address</md-error>
      </md-input-container>

      <md-input-container fxFlex="0 0 auto">
        <input mdInput type="password" formControlName="oldPassword" placeholder="Old password">
      </md-input-container>

      <span formGroupName="passwords" fxLayout="column">
        <md-input-container fxFlex="0 0 auto">
          <input mdInput type="password" formControlName="password" placeholder="Password">
        </md-input-container>

        <md-input-container fxFlex="0 0 auto">
          <input mdInput type="password" formControlName="confirmPassword" placeholder="Repeat password">
          <!--TODO investigate why <md-error> doesn't work-->
          <md-hint *ngIf="updateProfileForm.get('passwords').hasError('nomatch')">Passwords do not match</md-hint>
        </md-input-container>
      </span>
      <button type="submit" [disabled]="( !updateProfileForm.valid || !updateProfileForm.dirty )" hidden></button>
    </form>
      this.updateProfileForm = this.formBuilder.group( {
            firstName: [ '', Validators.required ],
            lastName: [ '', Validators.required ],
            email: [ '', Validators.compose( [ Validators.email, Validators.required ] ) ],
            oldPassword: [],
            passwords: this.formBuilder.group( {
              password: [ '' ],
              confirmPassword: [ '' ]
            }, { validator: this.matchingPasswords } )
          } );
  }
  
    private matchingPasswords( control: AbstractControl ) {
    const password = control.get( 'password' );
    const confirm = control.get( 'confirmPassword' );

    if ( !password || !confirm ) {
      return null;
    }

    return password.value === confirm.value ? null : { nomatch: true };
  }

+1 to see this working with optional relying on *ngIf, since the purpose of the md-error is quite trivial.

That would be useful for the server-side validation reflection.

Alright, thanks to your help i managed to figure it out, and can also say that it is working as intended. In short what i have done:

Create a custom validator

import {AbstractControl, NG_VALIDATORS, Validator} from "@angular/forms";
import {Directive, forwardRef} from "@angular/core";

const END_WITH_API_REGEX = /.*\/api\/?$/;

@Directive({
  selector: '[noApi][ngModel],[noApi][formControl]',
  providers: [
    { provide: NG_VALIDATORS, useExisting: forwardRef(() => NoApiValidator), multi: true }
  ]
})
export class NoApiValidator implements Validator {
  validate(c: AbstractControl): { [key: string]: any; } {
    return END_WITH_API_REGEX.test(c.value) ? {validateApi: false} : null
  }
}

Added it to my declarations:


@NgModule({
  declarations: [
    AppComponent, 
      [...],
    NoApiValidator
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
      [...]
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
}

And used it for something:

<md-input-container>
    <input mdInput placeholder="url" [(ngModel)]="url" type="url" required noApi #urlInput="ngModel">
    <md-error *ngIf="urlInput.errors">Something was wrong</md-error>
</md-input-container>

I thank you for your time, especially since it was a user error.

@whitebyte AFAICT by reading through this thread, there are no bugs with using *ngIf, just a misunderstanding of how md-error works.

  1. Errors are hidden until the input is both invalid and touched
  2. This behavior can can be customized.
  3. There are some minor usability issues with wrapping a md-error in ng-container, but it is expected behavior and easy to workaround

There a couple of issues with your examples:

  1. The first one that is ngIf="true" (you can remove this btw), doesn’t have any validation set up. If you add a required attribute it’ll work.
  2. The #emailInput refers to the DOM element, not the instance of ngModel. You can get it working by changing it to #emailInput="ngModel".

Here’s a forked Plunkr with the above-mentioned changes. Note that to enable Angular’s built-in email validation, you have to add the email attribute.

@willshowell I tried that example with the following

customErrorStateMatcher: ErrorStateMatcher = {
        isErrorState: (control: FormControl | null) => {
            if (control) {
                const hasInteraction = control.dirty;

                return (hasInteraction && control.parent.hasError("passwordMatchValidator"));
            }
    
            return false;
        }
    };

With my Confirm Password I also have a required, so I want the required to show if it is empty and the mismatch to show when the passwords are different, but hidden when the passwords are the same. I can’t seem to achieve this logic in the error matcher. What I have above works for the password matcher but ignores the required. If I remove control.parent.hasError("passwordMatchValidator") then the required shows but not the mismatch, so I can get one of them but not both.

So I updated the return to be

return (hasInteraction && (control.hasError("required") || control.parent.hasError("passwordMatchValidator")));

Is this the best way of doing this?

@willshowell thank you for the summary. While I understand that this is intended behavior, this is counter-intuitive and leads to obscure errors. I hope devs will change their mind on this issue.