Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 .

like image 419
Sangy_36 Avatar asked May 01 '19 08:05

Sangy_36


2 Answers

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("");
  }
like image 176
Eliseo Avatar answered Nov 03 '22 10:11

Eliseo


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>
like image 32
Lukáš Kříž Avatar answered Nov 03 '22 10:11

Lukáš Kříž