Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to pass NgControl status to child component in Angular, implementing ControlValueAccessor?

Tags:

angular

Provided

{
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => TestingComponent),
  multi: true
}

Injected NgControl

constructor(@Self() @Optional() public control: NgControl) {
  this.control && (this.control.valueAccessor = this);
}

And yet something is missing here?

enter image description here

Although @Eliseo answer is very explanatory there is still one addition... If you want to use both external validators and internal ones then parent NgControl Validators must be set accordingly. Furthermore you need to use ngDoCheck lifecycle hook to handle NgControl touch status if you want to use validation as me below is a final working solution

@Component({
    selector: 'app-testing',
    templateUrl: 'testing.component.html'
})
export class TestingComponent implements ControlValueAccessor, DoCheck, AfterViewInit {
    @Input()
    public required: boolean;

    @ViewChild('input', { read: NgControl })
    private inputNgModel: NgModel;

    public value: number;
    public ngControlTouched: boolean;

    constructor(@Optional() @Self() public ngControl: NgControl) {
        if (this.ngControl != null) this.ngControl.valueAccessor = this;
    }

    ngDoCheck() {
        if (this.ngControlTouched !== this.ngControl.touched) {
            this.ngControlTouched = this.ngControl.touched;
        }
    }

    ngAfterViewInit() {
        // Setting ngModel validators to parent NgControl
        this.ngControl.control.setValidators(this.inputNgModel.validator);
    }

    /**
     * ControlValueAccessor Implementation
     * Methods below
     */
    writeValue(value: number): void {
        this.value = value;
        this.onChange(this.value);
    }

    onChange: (_: any) => void = (_: any) => {};

    onTouched: () => void = () => {};

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

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

// Template
<input
    #input="ngModel"
    type="text"
    class="form-control"
    [class.is-invalid]="input.invalid && (input.dirty || input.touched || ngControlTouched)"
    [(ngModel)]="value"
    (ngModelChange)="onChange(value)"
    (blur)="onTouched()"
    [required]="required"
/>

// Usage
<app-testing [(ngModel)]="propertyDetails.whatThreeWords" name="testing" required></app-testing>
like image 576
Timothy Avatar asked Jan 26 '23 01:01

Timothy


1 Answers

You has two options:

Inject NgControl, for this you need remove the provider and make the constructor in the way

constructor(public control:NgControl){
    if (this.control != null) {
      this.control.valueAccessor = this;
    }
  }

Then you can decorate your input like

<input [ngClass]="{'ng-invalid':control.invalid,'ng-valid':control.valid...}">

Or copy the class of the customFormControl to the input.

Your input is like

<input [ngClass]="class">

If in the constructor of your custom form control import the ElementRef

constructor(private el:ElementRef){}

And create a function "copyClass"

  copyClass()
  {
    setTimeout(()=>{
       this.class=this.elementRef.nativeElement.getAttribute('class')
    })
  }

You can call this function in writeValue,Change and OnTouched.

The most simple example I can imagine is in this stackblitz

NOTE: If your problem is that you're using material angular in your component, the tecnique is using a customErrorMatcher, take a look to the official docs and, if you want to this answer in stackoverflow

UPDATE Another aproach is set the same validators to the input. For this, we use viewChild to get the input and, in ngAfterViewInit equals the validators

 @ViewChild('input',{static:false,read:NgControl}) input

 ngAfterViewInit()
  {
    if (this.control != null) {
       this.input.control.setValidators(this.control.control.validator)
    }

  }

see another stackblitz

at last update if we want to has a custom error inside the control, we can use the function validate to get the control and not inject in constructor. The component becomes like

@Component({
  ...
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomFormControlComponent),
      multi: true
    },
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => CustomFormControlComponent),
      multi: true,
    }]

})
export class CustomFormControlComponent implements ControlValueAccessor,
      Validator, AfterViewInit {
  ...
  control:any
  @ViewChild('input', { static: false, read: NgControl }) input
  constructor() {
  }
  ngAfterViewInit() {
     this.validate(null)
  }
  validate(control: AbstractControl): ValidationErrors | null{
    if (!this.control)
      this.control=control;

    if (this.control && this.input) 
       this.input.control.setValidators(this.control.validator)

    if (control.value=="qqq")
      return {error:"Inner error:The value is 1"}

    return null
  }

a new stackblitz

like image 115
Eliseo Avatar answered Jan 28 '23 15:01

Eliseo