76

I have a custom form control component in my Angular application, which implements ControlValueAccessor interface.

However, I want to access the FormControl instance, associated with my component. I'm using reactive forms with FormBuilder and providing form control using formControlName attribute.

SO, how do I access FormControl instance from inside of my custom form component?

Slava Fomin II
  • 26,865
  • 29
  • 124
  • 202

5 Answers5

70

This solution was born from the discussion in the Angular repository. Please, make sure to read it or even better to participate if you are interested in this problem.


I've studied the code of FormControlName directive and it's inspired me to write the following solution:

@Component({
  selector: 'my-custom-form-component',
  templateUrl: './custom-form-component.html',
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: CustomFormComponent,
    multi: true
  }]
})
export class CustomFormComponent implements ControlValueAccessor, OnInit {

  @Input() formControlName: string;

  private control: AbstractControl;


  constructor (
    @Optional() @Host() @SkipSelf()
    private controlContainer: ControlContainer
  ) {
  }


  ngOnInit () {

    if (this.controlContainer) {
      if (this.formControlName) {
        this.control = this.controlContainer.control.get(this.formControlName);
      } else {
        console.warn('Missing FormControlName directive from host element of the component');
      }
    } else {
      console.warn('Can\'t find parent FormGroup directive');
    }

  }

}

I'm injecting the parent FormGroup to the component and then getting the specific FormControl from it using control name obtained through formControlName binding.

However, be advised, that this solution is tailored specifically for the use case where FormControlName directive is used on host element. It won't work in other cases. For this you will need to add some additional logic. If you think, that this should be addressed by Angular, make sure to visit the discussion.

Slava Fomin II
  • 26,865
  • 29
  • 124
  • 202
  • from where do you have `this.control` ? – DAG Dec 12 '17 at 16:54
  • @DAG as I stated in the answer: `I'm injecting the parent FormGroup to the component and then getting the specific FormControl from it using control name obtained through formControlName binding.` – Slava Fomin II Dec 13 '17 at 06:32
  • 7
    @SlavaFominII: Instead of injecting parent form group and then accessing the control using formControllerName input binding, can't we just pass the form control as the input binding? I had a similar requirement where I wanted to access the form control from custom form component and I passed that form control as an input binding to custom form component. – Ritesh Waghela Jan 03 '18 at 07:21
  • 2
    anyone else impressed with this should look here https://material.angular.io/guide/creating-a-custom-form-field-control#-code-ngcontrol-code- – Joey Gough Feb 06 '19 at 16:54
  • 1
    IMO sending in the control defeats the purpose of an answer. Of course you can send in the control and do anything with it... (And you wouldn't need anything in the ctor from this answer). The spirit of the question is asking how to get access to the very control you're customizing FROM INSIDE. This should be doable. – Ben Racicot Jan 18 '20 at 23:19
  • Anybody who is coming to this in 2020 and beyond, check out the low-score answer from @rui below, [this related question](https://stackoverflow.com/questions/45556839/inheriting-validation-using-controlvalueaccessor-in-angular), and [this article](http://prideparrot.com/blog/archive/2019/2/applying_validation_custom_form_component) linked from that question. Injecting `FormControl` *instead* of using the `NG_VALUE_ACCESSOR` provider is probably the most robust solution. – Coderer May 21 '20 at 09:53
  • I am trying to do the same, but for forms using ngModel. Any idea how to get it to work? When injecting a ControlContainer in a template form, i cannot call controlContainer.control.get because control is null – Davy Dec 17 '21 at 08:20
62

Using formControlName as an input parameter doesn't work when binding via the [formControl] directive.

Here is a solution that works both ways without any input parameters.

export class MyComponent implements AfterViewInit {

  private control: FormControl;

  constructor(
    private injector: Injector,
  ) { }

  // The form control is only set after initialization
  ngAfterViewInit(): void {
    const ngControl: NgControl = this.injector.get(NgControl, null);
    if (ngControl) {
      this.control = ngControl.control as FormControl;
    } else {
      // Component is missing form control binding
    }
  }
}
Randy
  • 2,270
  • 1
  • 15
  • 24
  • 5
    This solution is definitely better than the accepted answer. – Denys Avilov Apr 10 '19 at 12:31
  • Although this solution worked great for me too, and seems far more elegant than the accepted one, in Angular 7 I get this tslint error: `get is deprecated: from v4.0.0 use Type or InjectionToken` – AsGoodAsItGets May 10 '19 at 15:29
  • It should be easy enough to adapt this solution to use InjectionToken. – Randy Jul 30 '19 at 00:39
  • 1
    Try ```const ngControl = this.injector.get(NgControl as Type);``` – Charles Jan 09 '20 at 12:09
  • @Randy What about case when control data in `[formControl]="form.get('data')` is removed and added again? I think we will end up with reference to first data control, not current which is in our form. – Adam Michalski Sep 02 '20 at 08:35
  • @AdamMichalski you are correct. In that case it would be best to handle this in the ngOnChanges lifecycle hook. This would prevent stale references from sticking around. – Randy Oct 05 '20 at 17:35
  • 1
    `ngAfterViewInit()` was giving me inconsistent state error, but changing it to `ngOnInit()` worked like a charm! Thanks a lot way better than the accepted answer. – Alan Sereb Jul 02 '21 at 15:43
21

Building upon previous answers and documentation found in a comment, here is what my opinion is the cleanest solution for a ControlValueAccessor based component.

// No FormControl is passed as input to MyComponent
<my-component formControlName="myField"></my-component>
export class MyComponent implements AfterViewInit, ControlValueAccessor  {

  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
       }
    }
}

Please be aware that with this solution you won't need to specify the NG_VALUE_ACCESSOR provider on the component as it will result on a CI Circular Dependency. The constructor will set correctly the valueAccessor.

bertonc96
  • 772
  • 2
  • 13
  • 24
Rui Marques
  • 8,567
  • 3
  • 60
  • 91
  • 4
    Nice solution. You may need to remove `provide: NG_VALUE_ACCESSOR` in the component declaration to prevent a circular dependency. See: https://material.angular.io/guide/creating-a-custom-form-field-control#ngcontrol – crashbus Feb 03 '21 at 11:13
  • I just realized with this answer, that creating custom components with NG_VALUE_ACCESSOR in providers is not they way to go... I am wondering why that method is recommended everywhere. What am I missing? Is there a catch? – Jette Feb 21 '23 at 11:45
  • 1
    @Jette note that this was answered almost 3 years ago. Not sure if there are better ways now as Angular keeps improving and I currently do not actively use the framework. – Rui Marques Feb 21 '23 at 12:32
  • @RuiMarques Good point... I will read up on it – Jette Feb 24 '23 at 15:19
4

As @Ritesh has already written in the comment you can pass form control as an input binding:

<my-custom-form-component [control]="myForm.get('myField')" formControlName="myField">
</my-custom-form-component>

And then you can get form control instance inside your custom form component like this:

@Input() control: FormControl;
sbedulin
  • 4,102
  • 24
  • 34
Yuri Beliakov
  • 623
  • 7
  • 23
4

Here is a simplified/cleaned up version of the accepted answer that works for both FormControlName and FormControl inputs:

export class CustomFormComponent implements ControlValueAccessor, OnInit {

  @Input() formControl: FormControl;

  @Input() formControlName: string;

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

  constructor(private controlContainer: ControlContainer) { }
}
Brad C
  • 2,868
  • 22
  • 33