Can I access to formControl of my custom ControlValueAccessor in Angular 2+?

Viewed 14061

I would like to create a custom form element with ControlValueAccessor interface in Angular 2+. This element would be a wrapper over a <select>. Is it possible to propagate the formControl properties to the wrapped element? In my case, the validation state is not getting propagated to nested select as you can see on the attached screenshot.

enter image description here

My component is available as following:

  const OPTIONS_VALUE_ACCESSOR: any = {
  multi: true,
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => OptionsComponent)
  };

  @Component({
  providers: [OPTIONS_VALUE_ACCESSOR], 
  selector: 'inf-select[name]',
  templateUrl: './options.component.html'
  })
  export class OptionsComponent implements ControlValueAccessor, OnInit {

  @Input() name: string;
  @Input() disabled = false;
  private propagateChange: Function;
  private onTouched: Function;

  private settingsService: SettingsService;
  selectedValue: any;

  constructor(settingsService: SettingsService) {
  this.settingsService = settingsService;
  }

  ngOnInit(): void {
  if (!this.name) {
  throw new Error('Option name is required. eg.: <options [name]="myOption"></options>>');
  }
  }

  writeValue(obj: any): void {
  this.selectedValue = obj;
  }

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

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

  setDisabledState(isDisabled: boolean): void {
  this.disabled = isDisabled;
  }
  }

This is my component template:

<select class="form-control"
  [disabled]="disabled"
  [(ngModel)]="selectedValue"
  (ngModelChange)="propagateChange($event)">
  <option value="">Select an option</option>
  <option *ngFor="let option of settingsService.getOption(name)" [value]="option.description">
  {{option.description}}
  </option>
  </select>
4 Answers

Here is what in my opinion is the cleanest solution to access FormControl in a ControlValueAccessor based component. Solution was based on what is mention here in Angular Material documentation.

// parent component template
<my-text-input formControlName="name"></my-text-input>
@Component({
  selector: 'my-text-input',
  template: '<input
    type="text"
    [value]="value"
  />',
})
export class MyComponent implements AfterViewInit, ControlValueAccessor  {

  // Here is missing standard stuff to implement ControlValueAccessor interface

  constructor(@Optional() @Self() public ngControl: NgControl) {
    if (ngControl != null) {
      // Setting the value accessor directly (instead of using
      // the providers) to avoid running into a circular import.
      ngControl.valueAccessor = this;
    }
  }

    ngAfterContentInit(): void {
       const control = this.ngControl && this.ngControl.control;
       if (control) {
          // FormControl should be available here
       }
    }
}

Here is a sample showing how to get (and re-use) the underlying FormControl and the underlying ControlValueAccessor.

This is useful when wrapping a component (like an input) since you can just re-use the already existing FormControl and ControlValueAccessor that angular creates which lets you avoid having to re-implement it.

@Component({
  selector: 'resettable-input',
  template: `
     <input type="text" [formControl]="control">
     <button (click)="clearInput()">clear</button>
  `,
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: ResettableInputComponent,
    multi: true
  }]
})
export class ResettableInputComponent implements ControlValueAccessor {

  @ViewChild(FormControlDirective, {static: true}) formControlDirective: FormControlDirective;
  @Input() formControl: FormControl;

  @Input() formControlName: string;

  // get hold of FormControl instance no matter formControl or formControlName is given.
  // If formControlName is given, then this.controlContainer.control is the parent FormGroup (or FormArray) instance.
  get control() {
    return this.formControl || this.controlContainer.control.get(this.formControlName);
  }

  constructor(private controlContainer: ControlContainer) { }

  clearInput() {
    this.control.setValue('');
  }

  registerOnTouched(fn: any): void {
    this.formControlDirective.valueAccessor.registerOnTouched(fn);
  }

  registerOnChange(fn: any): void {
    this.formControlDirective.valueAccessor.registerOnChange(fn);
  }

  writeValue(obj: any): void {
    this.formControlDirective.valueAccessor.writeValue(obj);
  }

  setDisabledState(isDisabled: boolean): void {
    this.formControlDirective.valueAccessor.setDisabledState(isDisabled);
  }
}

If you implement validation (Validator / NG_VALIDATORS), the AbstractControl is passed into your validation function pretty early. You can stash that away.

  validate(c: AbstractControl): ValidationErrors {
    this.myControl = c;
Related