21

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

HDJEMAI
  • 9,436
  • 46
  • 67
  • 93
Heady
  • 935
  • 3
  • 10
  • 23
  • your input MUST be @Input() formControl:FormControl, not AbstractControl – Eliseo Nov 01 '18 at 11:28
  • no. the code above works, the problem is just this console error that prevents rendering. if i force rerender, everything is fine – Heady Nov 02 '18 at 10:06

6 Answers6

31

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.

Johann Garrido
  • 370
  • 3
  • 7
  • instead of using the get() "form.get('theControlName')" you can also access it directly form.controls['theControlName'] – yehonatan yehezkel Apr 23 '20 at 18:43
  • 1
    I have tried same solution which was explained but I am still getting error in following line: `` at `[control]` Error is: `Type 'AbstractControl' is missing the following properties from type 'FormControl': defaultValue, registerOnChange, registerOnDisabledChange` Is there any way to rid of this? – Adeel Ahmed Jun 03 '23 at 18:50
  • it seems the control type you're passing is not the same you expect in child component. Make sure both types are FormControl type. – Johann Garrido Jun 06 '23 at 05:34
9

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

im.pankratov
  • 1,768
  • 1
  • 19
  • 19
  • Thanks. To use `[formControlName]` you need to provide `ControlContainer` - so here's a combination of that with your NOOP accessor https://stackoverflow.com/a/68353595/16940. – Simon_Weaver Jul 12 '21 at 23:32
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>
Chellappan வ
  • 23,645
  • 3
  • 29
  • 60
  • 1
    I would need to define all the validators in the parent component.. That doesn't seem possible that way, or am i misunderstanding this? – Heady Nov 01 '18 at 13:59
  • 2
    you can validation in child form also. if you want to know more about nested form watch this https://www.youtube.com/watch?v=CD_t3m2WMM8 – Chellappan வ Nov 01 '18 at 16:35
2

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.

Mike Bagnasco
  • 47
  • 1
  • 9
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>
Bullsized
  • 362
  • 4
  • 7
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() {}
}
Suresh Kumar Ariya
  • 9,516
  • 1
  • 18
  • 27