9
votes

I am trying to apply Error Validation style with ControlValueAccessor in custom Material Input Textbox. Ever since applying this custom component, all the red border validation status with formControlName/FormBuilders do not show, for required, minlength, etc. It worked natively (out of the box) with Angular Material textbox, until the custom control was applied.

Goal is to have custom textbox working with Form validation. This showed naturally with matInput textbox.

Update: Posted answer; however not sure if its most efficient one, trying to get Saloo answer working also (if someone can post stackbliz, would be great), open to any more efficient options

enter image description here

Typescript:

import { Component, OnInit, Input, ViewChild, EventEmitter, Output, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'app-input-textbox',
  templateUrl: './input-textbox.component.html',
  styleUrls: ['./input-textbox.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => InputTextboxComponent),
      multi: true
    }
  ]
})

export class InputTextboxComponent implements OnInit, ControlValueAccessor {
  @Input() MaxLength: string;
  @Input() Value: string;
  @Input() type: string;
  @Input() Label: string;
  @Input() PlaceHolder: string;
  @Output() saveValue = new EventEmitter();
  @Output() onStateChange = new EventEmitter();

  disabled: boolean;

  constructor() { }

  ngOnInit() {
  }

  saveValueAction(e) {
    this.saveValue.emit(e.target.value);
  }

  onChange(e) {
    this.Value = e;
  }

  onTouched() {
    this.onStateChange.emit();
  }

  writeValue(value: any) {
    this.Value = value ? value : '';
  }

  registerOnChange(fn: any) { this.onChange = fn; }

  registerOnTouched(fn: any) { this.onTouched = fn; }

  setDisabledState(isDisabled) { this.disabled = isDisabled; }
}

HTML:

<div class="input-wrap">
    <mat-form-field appearance="outline">
        <mat-label>{{Label}}</mat-label>   
        <input matInput 
            [attr.maxlength] = "MaxLength"
            [value]="Value ? Value : ''"
            [placeholder]="PlaceHolder ? PlaceHolder : ''"
            [type]="type ? type: 'text'"
            (input)="onChange($event.target.value)"
        >
    </mat-form-field>
</div>

Trying to incorporate this answer with the natural error styling from Angular Material textbox, Inheriting validation using ControlValueAccessor in Angular

3
can you set up a working stackblitz?Ron
why was this question voted down? I gave solid question, along with my thought out researched answer below, looking for more optimal code,user12425844

3 Answers

8
votes

I had the same problem. I tried all and then finally could resolve using this method:

I added this listener on the custom component. You can also do it 'blur' event.

@HostListener('focusout', ['$event.target'])
  onFocusout() {
    this.onTouched();
  }

And also calling onTouched when setting any value.

 writeValue(value: any) {
    this.onTouched();
    this.Value = value ? value : '';
}
4
votes

Kinda used your answer and the link you provided to come up with this solution:


@Component({
  selector: 'app-custom-input',
  templateUrl: './custom-input.component.html',
  styleUrls: ['./custom-input.component.css'],
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => CustomInputComponent),
    multi: true
  }]
})
export class CustomInputComponent implements OnInit, ControlValueAccessor {
  ...
  _control: NgControl;
  disabled: boolean;

  constructor(@Inject(INJECTOR) private injector: Injector) {
  }

  ngOnInit() {
    this._control = this.injector.get(NgControl);
  }
  
  ...

custom-input.component.html

<div class="input-wrap">
    <mat-form-field appearance="outline">
        <mat-label>{{Label}}</mat-label>   
        <input matInput
            [formControl]="_control.control" // <== this what makes it work
            [attr.maxlength]="MaxLength"
            [placeholder]="PlaceHolder ? PlaceHolder : ''"
            [type]="type ? type: 'text'"
        >
    </mat-form-field>
</div>

Note: I'm not binding to MatInput's outputs (yea). the formControl directive that passes the control to MatInput handles that for us.

made an example for you

3
votes

This will create error validation from Angular Material

Typescript:

import { Component, OnInit, Input, EventEmitter, Output, forwardRef, Injector } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl, NgForm, FormGroupDirective, NgControl } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material';

export class CustomFieldErrorMatcher implements ErrorStateMatcher {
  constructor(private customControl: FormControl,private errors:any) { }

  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    return this.customControl && this.customControl.touched &&(this.customControl.invalid || this.errors);
  }
}

@Component({
  selector: 'app-input-textbox',
  templateUrl: './input-textbox.component.html',
  styleUrls: ['./input-textbox.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => InputTextboxComponent),
      multi: true
    }
  ]
})

export class InputTextboxComponent implements OnInit, ControlValueAccessor {
  @Input() MaxLength: string;
  @Input() FocusIn: boolean;
  @Input() Width: string;
  @Input() Value: string;
  @Input() type: string;
  @Input() Label: string;
  @Input() Hint: string;
  @Input() PlaceHolder: string;
  @Output() saveValue = new EventEmitter();
  @Output() onStateChange = new EventEmitter();
  @Input() errors: any = null;
  disabled: boolean;
  control: FormControl;

  constructor(public injector: Injector) {}

  ngOnInit(){}

  ngAfterViewInit(): void {
    const ngControl: NgControl = this.injector.get(NgControl, null);
    if (ngControl) {
      setTimeout(() => {
        this.control = ngControl.control as FormControl;
      })
    }
  }

  saveValueAction(e) {
    this.saveValue.emit(e.target.value);
  }

  //control value accessor init
  writeValue(value: any) {
    this.Value = value ? value : '';
  }

  onChange(e) {
    this.Value = e;
  }

  onTouched() {
    this.onStateChange.emit();
  }

  registerOnChange(fn: any) { this.onChange = fn; }

  registerOnTouched(fn: any) { this.onTouched = fn; }

  setDisabledState(isDisabled) { this.disabled = isDisabled; }

  errorMatcher() {
    return new CustomFieldErrorMatcher(this.control,this.errors)
  }

  readonly errorStateMatcher: ErrorStateMatcher = {
    isErrorState: (ctrl: FormControl) => (ctrl && ctrl.invalid)
  };

}

HTML

<div class="input-wrap">
    <mat-form-field>
        <mat-label>{{Label}}</mat-label>   
        <input 
            matInput 
            [attr.maxlength] = "MaxLength"
            [value]="Value ? Value : ''"
            [placeholder]="PlaceHolder ? PlaceHolder : ''"
            [type]="type ? type: 'text'"
            [ngModel]="Value" 
            [errorStateMatcher]="errorMatcher()"

            (input)="onChange($event.target.value)"
            (blur)="onTouched()"
            (change)="saveValueAction($event)"
            (ngModelChange)="Value=$event;onChange($event)"
        >
        <mat-hint>{{Hint}}</mat-hint>
    </mat-form-field>
</div>