Passing a FormControl to a child component - No value accessor for form control with unspecified name
Asked Answered
W

6

24

I have an input component customInput that creates a classic input field and adds some layouting-spice to it, no additional logic.

I want to pass a formControl to it, to bind it to the input it contains.

Should be used like this:

<form [formGroup]="form">
  <custom-input [formControl]="form.controls['control']"></custom-input>
</form>

Inside Custom Input:

export class HidInputFieldComponent  {
   @Input() formControl: AbstractControl

   ...
}

<div class="container">
  <input [formControl]="formControl"/>
    <label>label</label>
</div>

Now when i initialize the component, i get

No value accessor for form control with unspecified name

Logging the control in my components constructor, it is undefined.

Am I doing it wrong or isn't there a way around ControlValueAccessor? Since I am not actually building a custom control (I still use classic input) it seems extreme

Whippletree answered 1/11, 2018 at 10:37 Comment(2)
your input MUST be @Input() formControl:FormControl, not AbstractControlFlaming
no. the code above works, the problem is just this console error that prevents rendering. if i force rerender, everything is fineWhippletree
N
36

You don't need to import ControlValueAccessor or anything similar to accomplish that.

All you need to do is to pass the FormControl object to your child component like this:

<form [formGroup]="form">
  <custom-input [control]="form.controls['theControlName']">
  </custom-input>
</form>

That means your custom-input component should look like this:

import {Component, Input} from '@angular/core';
import {FormControl} from '@angular/forms';

@Component({
  selector: 'custom-input',
  templateUrl: './input.component.html',
  styleUrls: ['./input.component.scss']
})
export class InputComponent {
  @Input() control: FormControl;
}

And the template:

<input [formControl]="control">

And that's it.

If you implement the custom input like that you won't have to bring the parent formGroup into the child component logic, it is completely unnecessary there (Unless you need to make some operations on it or on some of the rest of form controls).

Besides, passing the FormControl object to the custom input would give you access to the properties of it without referencing the FormGroup and then getting the specific control, because that's a work done on the parent component.

I hope this solution helps to simplify the work of many people as it's pretty common to make this kind of custom controls.

Nowicki answered 10/1, 2019 at 20:20 Comment(5)
instead of using the get() "form.get('theControlName')" you can also access it directly form.controls['theControlName']Premium
I have tried same solution which was explained but I am still getting error in following line: <custom-input [control]="form.controls['theControlName']"></custom-input> at [control] Error is: Type 'AbstractControl<any, any>' is missing the following properties from type 'FormControl<any>': defaultValue, registerOnChange, registerOnDisabledChange Is there any way to rid of this?Medina
it seems the control type you're passing is not the same you expect in child component. Make sure both types are FormControl type.Nowicki
Just in case anybody else wondered what the actual difference is that does the trick here ... it's the naming. I used "formControl" as name for the input an hence bound to it -> had the strange error. The answer here uses just "control", which apparently circumvents some clash with the "normal" binding via [formControl] -> no error.Wampum
There must be a change in the API because this doesn't work anymore.Dizon
D
10

this answer from Johann Garrido is good, however it introduces extra input [control] that you'll need to always keep in mind when working with custom components.

Plus, it tied to work directly to ReactiveFormsModule because it only accepts FormControl instance.

Better way to do this is in my opinion is to implement ControlValueAccessor interface, but utilize some workaround to not duplicate control handling:

export const NOOP_VALUE_ACCESSOR: ControlValueAccessor = {
  writeValue(): void {},
  registerOnChange(): void {},
  registerOnTouched(): void {}
};

And use NOOP_VALUE_ACCESSOR in component that wraps the form control:

@Component({
  selector: "wrapped-input",
  template: `
    <mat-form-field class="example-full-width">
      <mat-label>Wrapped input</mat-label>

      <!--We pass NgControl to regular MatInput -->
      <input matInput [formControl]="ngControl.control" />
    </mat-form-field>
  `
})
export class WrappedInput {
  constructor(@Self() @Optional() public ngControl: NgControl) {
    if (this.ngControl) {
      // Note: we provide the value accessor through here, instead of
      // the `providers` to avoid running into a circular import.
      // And we use NOOP_VALUE_ACCESSOR so WrappedInput don't do anything with NgControl
      this.ngControl.valueAccessor = NOOP_VALUE_ACCESSOR;
    }
  }
}

That way Angular will think that WrappedInput works like any other component that implements ControlValueAccessor interface (MatInput for example).

Plus, it will work with both ReactiveFormsModule and regular FormsModule.

WrappedInput can be used like so:

<wrapped-input [formControl]="sourceControl"></wrapped-input>

Here's full working stackblitz that you can play with: https://stackblitz.com/edit/angular-wrapped-form-control-example?file=src/app/app.ts

Derek answered 7/12, 2020 at 7:48 Comment(1)
Thanks. To use [formControlName] you need to provide ControlContainer - so here's a combination of that with your NOOP accessor https://mcmap.net/q/582115/-use-formcontrolname-for-custom-input-component-in-reactive-form.Peoria
A
5

I know I'm way late to the party, but I stumbled upon this question with the same issue so I hope this helps someone on the future.

In my case, renaming @Input() formControl: FormControl to @Input() control: FormControl fixed the error.

Ashley answered 31/1, 2023 at 22:28 Comment(1)
Thank you, this was my problem indeed! The error message (in my case: value accessor is null) is completely useless Is this a bug or some hidden automagic by angular form internals?Lofton
Y
4

Use FormGroupDirective

This directive accepts an existing FormGroup instance. It will then use this FormGroup instance to match any child FormControl, FormGroup, and FormArray instances to child FormControlName, FormGroupName, and FormArrayName directives.

Doing this you can access child formControl from parent

  @Component({
    selector: 'app-custom-input',
    templateUrl: './custom-input.html',
    viewProviders:[{ provide: ControlContainer, useExisting: FormGroupDirective}]
  })    
    export class HidInputFieldComponent  {
     constructor(private fcd:FormGroupDirective) {       
    }
    ngOnInit() {
        this.fcd.form.addControl('formControl',new FormControl(''));
    }
    }

    <div class="container">
      <input [formControl]="formControl"/>
        <label>label</label>
    </div>
Yoho answered 1/11, 2018 at 11:0 Comment(2)
I would need to define all the validators in the parent component.. That doesn't seem possible that way, or am i misunderstanding this?Whippletree
you can validation in child form also. if you want to know more about nested form watch this youtube.com/watch?v=CD_t3m2WMM8Unimposing
C
1

Create yourself a custom getCtrl function in the TS file:

  getCtrl(name: string): FormControl {
    const ctrl = this.form.get(name) as FormControl;

    if (!ctrl) throw 'Missing Form Control for ' + name;

    return ctrl;
  }

and then call it as many times as you want in the template:

<div>
  <app-form-control
    [control]="getCtrl('language')"
  ></app-form-control>
</div>
Carboy answered 13/6, 2022 at 12:41 Comment(0)
W
0

You can also try implementing ControlValueAccessor Interface Class.

@Component({
  selector: 'app-custom-input',
  templateUrl: './app-custom-input.component.html'
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi: true,
      useExisting: forwardRef(() => HidInputFieldComponent)
    }
  ]
})
export class HidInputFieldComponent implements OnInit, ControlValueAccessor {
  @Output() setDropdownEvent: EventEmitter<any> = new EventEmitter();

  ngOnInit() {}

  writeValue(value: any) {}

  propagateChange(time: any) {
    console.log(time);
  }

  registerOnChange(fn) {
    this.propagateChange = fn;
  }

  registerOnTouched() {}
}
Wrote answered 1/11, 2018 at 11:9 Comment(2)
I read about that, but i want to find away around it. I think that should be possible in my caseWhippletree
It's not an answer to my problem though, is it?Whippletree

© 2022 - 2024 — McMap. All rights reserved.