1

In my parent component, I have a FormGroup with a FormArray, and I want to handle that array in a child component. The parent's HTML does this:

<ng-container [formGroup]="formGroup">
  <app-child formArrayName="theArrayName">

I assumed in the child I would inject the NgControl and then have access:

@Component({
  ...,
  providers: [
    {
        provide: NG_VALUE_ACCESSOR,
        useExisting: forwardRef(() => ChildComponent),
        multi: true
    }
  ]
})
export class ChildComponent implements ControlValueAccessor {
    constructor(private readonly control: NgControl) {
      this.formArray = control.control as FormArray<...>
    }

I always get a null injector saying there's no provider for NgControl.

Gargoyle
  • 9,590
  • 16
  • 80
  • 145

4 Answers4

2

NG_VALUE_ACCESSOR is a provider to create a custom form control not a custom form array. You should define a ChildComponent as a FormControl. Refer this how to do it.

And then you should use FormArray like

<ng-container [formGroup]="formGroup">
  <ng-container formArrayName="theArrayName" >
    <ng-container *ngFor="let control of formGroup.controls.theArrayName.controls; let i = index">
      <app-child [formControlName]="i"></app-child>
    </ng-container>  
  </ng-container>
</ng-container>
Julian W.
  • 1,501
  • 7
  • 20
  • So I need the child control to completely handle the array itself. Basically I have the array as input, and then I map that into a 3rd party grid control where each row of the array is a row of the grid. Their control doesn't work with forms though, so when I hit their "done" type button, then I'd convert from their grid rows back to the FormArray. – Gargoyle Aug 15 '22 at 04:46
2

Complementary the Chris Hamilton's answer

<app-child [formArray]="form.get('formArrayNme')"></app-child>

The problem when you mannage a formArray in a child that you pass as input is that you can not use the "typical" constructor of manage FormArrays. You should define a function (*)

    //if is a FormArray of FormControls
    getControl(index:number)
    {
        return this.formArray.at(index) as FormControl
    }
    //if is a FormArray of FormGroup
    getGroup(index:number)
    {
        return this.formArray.at(index) as FormGroup
    }

And use

    <!--if is a formArray of FormControls-->
    <div *ngFor="let control of FormArray;let i=index">
       <input [formControl]=getControl(i)>
    </div>
    
    <!--if is a formArray of FormGroups-->
    <div *ngFor="let group of FormArray;let i=index" [formGroup]="getGroup(i)>
       <input formControlName="prop1">
       <input formControlName="prop2">
    </div>

If we want to use the typical FormArray with formArrayName we need viewProvider the FormGroupDirective and know the name. We can do using a child-control like

    @Component({
      selector: 'child-array',
      templateUrl: 'child-array.html',
      viewProviders:[
         { provide: ControlContainer, useExisting: FormGroupDirective }]
    })
    
    export class ChildComponent  {
      array:FormArray
      arrayName:string="fool"
      @Input() name: string;
      @Input('array') set _(value)
      {
        this.array=value as FormArray
        this.arrayName=Object.keys(this.array.parent.controls)
            .find(key=>this.array.parent.get(key)==this.array)
      }
    }

Now we can use

      <!--if is a formArray of FormControls-->
      <div [formArrayName]="arrayName">
        <div *ngFor="let control of array.controls;let i=index">
          <input [formControlName]="i">
        </div>
      </div>
    
      <!--if is a formArray of FormGroups-->
      <div [formArrayName]="arrayName">
        <div *ngFor="let control of array.controls;let i=index" [formGroupName]="i">
          <input formControlName="prop1">
          <input formControlName="prop2">
        </div>
      </div>

This second approach (in the case of formArray of FormControls) can be see in this stackblitz

(*)I know that some authors use the variable of the loop to get the value of the formControl or the FormGroup

<div *ngFor="let group of formArray.controls" [formGroup]="group">

Unfortunaly, this don't work since Angular 12, because group is only an "AbstractControl". If you has strict mode you received an error saying you that an AbstractControl is not a FormGroup (works in early Angular versions).

Some suggest use the $any, but (personal opinion) is a "ugly work-around" or even de-activate the strict mode (it's a very very very bad idea and this last is not a personal opinion)

Gargoyle
  • 9,590
  • 16
  • 80
  • 145
Eliseo
  • 50,109
  • 4
  • 29
  • 67
0

Maybe I'm missing some information, but the simplest way to do that is to just use an input property.

parent ts

export class ParentComponent {
  formGroup = new FormGroup(...);

  get formArray() {
    return this.formGroup.get('theArrayName') as FormArray;
  }
}

child ts

export class ChildComponent implements OnInit {
  @Input() formArray = new FormArray([]);

  ngOnInit() {
    console.log(this.formArray);
  }
}

parent html

<app-child [formArray]="formArray"></app-child>
Chris Hamilton
  • 9,252
  • 1
  • 9
  • 26
0

Julian lin could work, but there is a simple way to do it as well without the ControlValueAccessor if you know you are gonna use a FormGroup instead the array.

So in the ChildComponent you have

export class ChildComponent implements OnInit{
 formGroup: FormGroup
 constructer (private formCtrl: FormGroupDirective){}
 ngOnInit(){
   // only available after ngOnInit
   this.formGroup = formCtrl.form;
 }
}

in the parent component template you would set it something like this

<ng-container *ngFor="let someGroup of formArray.controls">
  <app-child-component [formGroup]="someGroup" ></app-child-component>
</ng-container>

then you can use the parent to still detect if there is an validation error in the child forms.