Gargoyle
Gargoyle

Reputation: 10375

Inject parent FormArray into child component in Angular 14

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.

Upvotes: 1

Views: 7697

Answers (4)

Eliseo
Eliseo

Reputation: 57999

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)

Upvotes: 4

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.

Upvotes: 0

Chris Hamilton
Chris Hamilton

Reputation: 10984

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>

Upvotes: 0

Julian W.
Julian W.

Reputation: 1571

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>

Upvotes: 2

Related Questions