Sangy_36
Sangy_36

Reputation: 73

How to dynamically add a mat-error to a mat-input-field?

I want to show an error when user exceeds maxLength by adding < mat-error > dynamically to the DOM.

I already have a attribute directive in place to limit the max length of an input field. I have it as a directive, since this gets applied to a lot of input fields across different files in the project. But now the problem is, I have to show a mat-error when the user exceeds the limit. I don't want to add < mat-error > under every input fields across all files by myself , i want a modular solution. Can this be done by using the existing directive itself?

<mat-form-field floatLabel="auto">
      <input [formControl]="displayNameControl"
        mongoIndexLimit
        [charLength]="charLength"
        matInput
        name="displayName"
        placeholder="Stack Name"
        autocomplete="off"
        required />
    </mat-form-field>

And this is my directive

import { Directive, OnInit, NgModule, ElementRef, OnChanges, Input, SimpleChanges, Renderer2 } from '@angular/core';

@Directive({
  selector: '[mongoIndexLimit]'
})
export class MongoIndexLimitDirective implements OnInit, OnChanges {
  @Input() public charLength?: number;
  private maxLength = 5;
  constructor(
    private el: ElementRef<HTMLElement>,
    private renderer: Renderer2
  ) { }

  public ngOnInit() {
    this.el.nativeElement.setAttribute('maxLength', this.maxLength.toString());
  }

  public ngOnChanges(changes: SimpleChanges) {
    if (changes.charLength.currentValue >= 5) {
      const child = document.createElement('mat-error');
      this.renderer.appendChild(this.el.nativeElement.parentElement.parentElement.parentElement, child);
    }
  }

}

When i try the above i was able to append a < mat-error > element to the DOM, but angular doesn't treat it as a compiler < mat-error > angular material. it is just a dummy < mat-error > and not a material component.

I desire the result to be an input component with maxLength set and a dynamically generated mat-error which shows when the limit exceeds, just like how it is in the example below.

https://material.angular.io/components/input/examples (titled Input with custom Error state matcher)

Sorry for my bad English .

Upvotes: 5

Views: 15840

Answers (3)

Eliseo
Eliseo

Reputation: 57939

Of course you can add dinamically a mat-error. There are an amazing article in NetBasal about this.

A simple version that I make is in stackblitz. In this stackblitz I attach the directive to the mat-form-field and make a work-around to attach a new component mat-error-component. This allow me use css and animations.

The key is use ViewContainerRef to add dinamically a component using ComponentFactoryResolver

Well, the code of the directive:

export class MongoIndexLimitDirective implements AfterViewInit {
  ref: ComponentRef<MatErrorComponent>;
  constructor(
    private vcr: ViewContainerRef,
    private resolver: ComponentFactoryResolver,
    private formField:MatFormField
  ) { }

  public ngAfterViewInit()
  {
    this.formField._control.ngControl.statusChanges.subscribe(res=>this.onChange(res))

  }

  public onChange(res) {
    if (this.formField._control.ngControl.invalid)
    {
      this.setError('error')
    }      
    else
      this.setError('')
  }
  setError(text: string) {
    if (!this.ref) {
     const factory = this.resolver.resolveComponentFactory(MatErrorComponent);
     this.formField._elementRef
     this.ref = this.vcr.createComponent(factory);
   }
   this.ref.instance.error=text;
}

The MatErrorComponent (I called as it for my convenience. Be carafully you need put in the entryComponents of the main module) looks like more complex than real it is because the "animations", but essencially is a <mat-error>{{message}}</mat-error>

@Component({
  selector: 'custom-error',
  template:`
  <div [@animation]="_state" style="margin-top:-1rem;font-size:.75rem">
      <mat-error >
      {{message}}
    </mat-error>
    </div>
  `,
  animations: [
    trigger('animation', [
      state('show', style({
        opacity: 1,
      })),
      state('hide',   style({
        opacity: 0,
        transform: 'translateY(-1rem)'
      })),
      transition('show => hide', animate('200ms ease-out')),
      transition('* => show', animate('200ms ease-in'))
      
    ]),
  ]
  
})
export class MatErrorComponent{
  _error:any
  _state:any
  message;

  @Input() 
  set error(value)
  {
    if (value && !this.message)
    {
      this.message=value;
      this._state='hide'
      setTimeout(()=>
      {
        this._state='show'
      })
    }
    else{
    this._error=value;
    this._state=value?'show':'hide'
    }
  }

Updated a better aproach of the mat-error-component.

We can take account the differents errors and improve the transition like

@Component({
  selector: '[custom-error]',
  template: `
  <div [@animation]="increment" *ngIf="show" style="margin-top:-1rem;font-size:.75rem">
      <mat-error >
      {{message}}
    </mat-error>
    </div>
  `,
  animations: [
    trigger('animation', [
      transition(':increment', [
        style({ opacity: 0}),
        animate('200ms ease-in', style({ opacity: 1 })),
      ]),
      transition(':enter', [
        style({ opacity: 0, transform: 'translateY(-1rem)' }),
        animate('200ms ease-in', style({ opacity: 1, transform: 'translateY(0)' })),
      ]),
      transition(':leave', [
        animate('200ms ease-out', style({ opacity: 0, transform: 'translateY(-1rem)' }))
      ])])
  ]

})
export class MatErrorComponent {
  show: boolean
  message: string;
  increment:number=0;

  @Input()
  set error(value) {
    if (value)
    {
      if (this.message!=value)
        this.increment++;

      this.message = value;
    }

    this.show = value ? true : false;
  }
} 

This allow as that when the message error change, a new animation happens -in this case change opacity from 0 to 1 if, e.g. in our directive change the function onChange to

  public onChange(res) {
    if (this.control.invalid)
    {
      if (this.control.errors.required)
        this.setError(this.formField._control.placeholder+' required')
      else
        this.setError(this.formField._control.placeholder+' incorrect')
    }      
    else
      this.setError('')
  }

See the improve stackblitz

Update 2 There was a problem with blur. If at first the control is invalid, the status not change, so we need add blur event. For this we use renderer2 and ViewContent to get the input

@ContentChild(MatInput,{read:ElementRef}) controlElementRef:ElementRef

And change the ngAfterViewInit

public ngAfterViewInit()
  {
    this.control=this.formField._control.ngControl;
    this.renderer.listen(this.controlElementRef.nativeElement,'blur',()=>this.onChange(null))
    this.control.statusChanges.subscribe(res=>this.onChange(res))

  }

stackblitz take account "blur"

If we can we can has a predefined errors, add at last a "error" to custom Errors, so if our custom error return some like {error:'error text'} we can show the errors.

The important part is

export const defaultErrors = {
  minlength: ({ requiredLength, actualLength }) =>
    `Expect ${requiredLength} but got ${actualLength}`,
  email: error=>'The email is incorrect',
  error:error=>error,
  required: error => `This field is required`

};

And OnChnage becomes like

public onChange(res) {
    if (this.control.invalid && this.control.touched) {
      let error: string = this.formField._control.placeholder + " incorrect";
      Object.keys(defaultErrors).forEach(k => {
        console.log(k,this.control.hasError(k),this.control.errors[k])
        if (this.control.hasError(k)) error = defaultErrors[k](this.control.errors[k]);
      });
      this.setError(error);
    } else this.setError("");
  }

Upvotes: 7

Yes, you can insert dynamically mat-error for matInput

<mat-form-field>
    <mat-label>{{item.label}}</mat-label>
    <input type="text" matInput [formControlName]="item.name">
    <mat-error *ngIf="form.get(item.name).invalid">{{getErrorMessage(form.get(item.name))}}</mat-error>
</mat-form-field>

Upvotes: 1

Antoniossss
Antoniossss

Reputation: 32517

You don't add mat-error dynamicly, you just put it there. It cooperates strictly with mat-form-field and will become visible if matInput will be in invalid state.

Threat it as a container for error message. All you have to do is to adjust message (in case you can have multiple validation rules and want custom messages for all of them).

Code from Angular Material docs

<div class="example-container">
  <mat-form-field>
    <input matInput placeholder="Enter your email" [formControl]="email" required>
    <mat-error *ngIf="email.invalid">{{getErrorMessage()}}</mat-error>
  </mat-form-field>
</div>

Upvotes: 0

Related Questions