angulararraylistformarrayformgroupsdynamic-forms

How to handle the Formarray in the Nested array in Angular


enter image description here

I need to render the Dynamic fields in the Angular. But, that one is solved. Then what the issue is when i render the Nested Array or a Group it's not rendering properly.

We've to add or remove functionality also needed.

As of now, we're not able to render the below data in the Form.

{
      type: 'group',
      name: 'current_job',
      label: 'Current Job',
      children: [
        {
          type: 'text',
          label: 'Company',
          name: 'company',
          value: 'Skillmine',
        },
        {
          type: 'array',
          name: 'company_addresses',
          label: 'Company Addresses',
          value: [
            { _id: '1', street: '1600 Amphitheatre Parkway', city: 'Mountain View', state: 'CA' },
            { _id: '2', street: 'C. Montes Urales 445', city: 'Ciudad de México', state: 'Distrito Federal' }
          ],
          children: [
            {
              type: 'text',
              label: 'Street',
              name: 'street',
            },
            {
              type: 'text',
              label: 'City',
              name: 'city',
            },
            {
              type: 'text',
              label: 'State',
              name: 'state',
            }
          ]
        }
      ]
    },

This is my App.component.html code..

<form [formGroup]="form" (ngSubmit)="onSubmit()">
  <div *ngFor="let field of formConfig">
    <div [ngSwitch]="field.type">
      <label *ngIf="field.label">{{ field.label }}</label>

      <!-- Handle form controls -->
      <input *ngSwitchCase="'text'" [formControlName]="field.name" />
      <input *ngSwitchCase="'number'" type="number" [formControlName]="field.name" />
      <select *ngSwitchCase="'select'" [formControlName]="field.name">
        <option *ngFor="let option of field.options" [value]="option.key">{{ option.value }}</option>
      </select>

      <!-- Handle nested arrays -->
      <div *ngSwitchCase="'array'" [formArrayName]="field.name">
        <div *ngFor="let group of getFormArray(field.name)?.controls; let i = index" [formGroupName]="i">
          <ng-container *ngFor="let child of field.children">
            <label *ngIf="child.label">{{ child.label }}</label>
            <input *ngIf="child.type === 'text'" [formControlName]="child.name" />
            <input *ngIf="child.type === 'number'" type="number" [formControlName]="child.name" />

            <!-- Handle nested arrays within arrays -->
            <ng-container *ngIf="child.type === 'array'">
              <div [formArrayName]="child.name">
                <div *ngFor="let innerGroup of getFormArray(child.name)?.controls; let j = index" [formGroupName]="j">
                  <ng-container *ngFor="let innerChild of child.children">
                    <label *ngIf="innerChild.label">{{ innerChild.label }}</label>
                    <input *ngIf="innerChild.type === 'text'" [formControlName]="innerChild.name" />
                    <input *ngIf="innerChild.type === 'number'" type="number" [formControlName]="innerChild.name" />
                  </ng-container>
                  <button type="button" (click)="removeArrayControl(child.name, j)">Remove</button>
                </div>
                <button type="button" (click)="addArrayControl(child.name)">Add</button>
              </div>
            </ng-container>
          </ng-container>
          <button type="button" (click)="removeArrayControl(field.name, i)">Remove</button>
        </div>
        <button type="button" (click)="addArrayControl(field.name)">Add</button>
      </div>

      <!-- Handle nested groups -->
      <div *ngSwitchCase="'group'" [formGroupName]="field.name">
        <ng-container *ngFor="let child of field.children">
          <label *ngIf="child.label">{{ child.label }}</label>
          <input *ngIf="child.type === 'text'" [formControlName]="child.name" />
          <input *ngIf="child.type === 'number'" type="number" [formControlName]="child.name" />

          <!-- Handle nested arrays within groups -->
          <ng-container *ngIf="child.type === 'array'">
            <div [formArrayName]="child.name">
              <div *ngFor="let innerGroup of getFormArray(child.name)?.controls; let j = index" [formGroupName]="j">
                <ng-container *ngFor="let innerChild of child.children">
                  <label *ngIf="innerChild.label">{{ innerChild.label }}</label>
                  <input *ngIf="innerChild.type === 'text'" [formControlName]="innerChild.name" />
                  <input *ngIf="innerChild.type === 'number'" type="number" [formControlName]="innerChild.name" />
                </ng-container>
                <button type="button" (click)="removeArrayControl(child.name, j)">Remove</button>
              </div>
              <button type="button" (click)="addArrayControl(child.name)">Add</button>
            </div>
          </ng-container>
        </ng-container>
      </div>
    </div>
  </div>
  <button type="submit">Submit</button>
</form>

This is my App.Component.ts code..

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormArray, FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
  form!: FormGroup;
  formConfig: FieldConfig[] = [
    {
      type: 'text',
      label: 'Given Name',
      name: 'given_name',
      value: 'Baskaran',
    },
    {
      type: 'array',
      name: 'mobile_numbers',
      label: 'Mobile Numbers',
      value: [
        { _id: '1', mobile_number: '+9393393939' },
        { _id: '2', mobile_number: '+9393838484' }
      ],
      children: [
        {
          type: 'text',
          name: 'mobile_number',
        }
      ]
    },
    {
      type: 'group',
      name: 'current_job',
      label: 'Current Job',
      children: [
        {
          type: 'text',
          label: 'Company',
          name: 'company',
          value: 'Skillmine',
        },
        {
          type: 'array',
          name: 'company_addresses',
          label: 'Company Addresses',
          value: [
            { _id: '1', street: '1600 Amphitheatre Parkway', city: 'Mountain View', state: 'CA' },
            { _id: '2', street: 'C. Montes Urales 445', city: 'Ciudad de México', state: 'Distrito Federal' }
          ],
          children: [
            {
              type: 'text',
              label: 'Street',
              name: 'street',
            },
            {
              type: 'text',
              label: 'City',
              name: 'city',
            },
            {
              type: 'text',
              label: 'State',
              name: 'state',
            }
          ]
        }
      ]
    },
  ];

  constructor(private fb: FormBuilder) { }

  ngOnInit(): void {
    this.form = this.fb.group(this.createFormGroup(this.formConfig));
  }

  createFormGroup(config: FieldConfig[]): { [key: string]: FormControl | FormArray | FormGroup } {
    const group: { [key: string]: FormControl | FormArray | FormGroup } = {};

    config.forEach(field => {
      if (field.type === 'array') {
        group[field.name] = this.createFormArray(field);
      } else if (field.type === 'group') {
        group[field.name] = this.fb.group(this.createFormGroup(field.children || []));
      } else {
        group[field.name] = this.createFormControl(field);
      }
    });

    return group;
  }

  createFormControl(config: FieldConfig): FormControl {
    return new FormControl(config.value || '', config.validators || []);
  }

  createFormArray(config: FieldConfig): FormArray {
    const formArray = this.fb.array<FormGroup>([]);

    const initialValues = config.value || [];
    initialValues.forEach((value: any) => {
      const groupConfig = this.createGroupConfig(config.children || [], value);
      formArray.push(this.fb.group(groupConfig));
    });

    return formArray;
  }

  createGroupConfig(config: FieldConfig[], value: any): { [key: string]: FormControl | FormArray | FormGroup } {
    return config.reduce((acc, child) => {
      if (child.type === 'array') {
        acc[child.name] = this.createFormArray(child);
      } else if (child.type === 'group') {
        acc[child.name] = this.fb.group(this.createFormGroup(child.children || []));
      } else {
        acc[child.name] = this.createFormControl({ ...child, value: value[child.name] });
      }
      return acc;
    }, {} as { [key: string]: FormControl | FormArray | FormGroup });
  }

  getFormArray(controlName: string): FormArray | null {
    const control = this.form.get(controlName);
    return control instanceof FormArray ? control : null;
  }

  addArrayControl(controlName: string): void {
    const array = this.getFormArray(controlName);
    if (array) {
      const config = this.formConfig.find(field => field.name === controlName);
      if (config && config.children) {
        const newGroup = this.fb.group(this.createGroupConfig(config.children, {}));
        array.push(newGroup);
      }
    }
  }

  removeArrayControl(controlName: string, index: number): void {
    const array = this.getFormArray(controlName);
    if (array) {
      array.removeAt(index);
    }
  }

  onSubmit(): void {
    console.log(this.form.value);
  }
}

export interface FieldConfig {
  type: 'text' | 'number' | 'select' | 'array' | 'group';
  label?: string;
  name: string;
  options?: { key: string; value: string }[];
  value?: any;
  validators?: any[];
  children?: FieldConfig[];
  _id?: string;
}

Solution

  • See your function 'getFormArray'

      getFormArray(controlName: string): FormArray | null {
        const control = this.form.get(controlName) as FormArray;
        return control instanceof FormArray ? control : null;
      }
    

    As you pass, when inner FormArray 'child.name', you're passing simply, e.g. "company_addresses", you should pass "current_job.company_addresses", so replace it

    Yes,the method 'get' of a formGroup can use "dot" notation.

    <ng-container *ngIf="child.type === 'array'">
                <div [formArrayName]="child.name">
                 <!--you loop over gorm.get("field.name"+"."+child.name")-->
    
                  <div *ngFor="let innerGroup of getFormArray(field.name+'.'+child.name)?.controls; let j = index" [formGroupName]="j">
                    <ng-container *ngFor="let innerChild of child.children">
                      <label *ngIf="innerChild.label">{{ innerChild.label }}</label>
                      <input *ngIf="innerChild.type === 'text'" [formControlName]="innerChild.name" />
                      <input *ngIf="innerChild.type === 'number'" type="number" [formControlName]="innerChild.name" />
                    </ng-container>
                    <!--see that you should remove the j element of "field.name"+"."+child.name"-->
                    <button type="button" (click)="removeArrayControl(field.name+'.'+child.name, j)">Remove</button>
                  </div>
                  <!--Carefull, pass the formArrayName and the childName-->
                  <button type="button" (click)="addArrayControl(field.name,child.name)">Add</button>
                </div>
              </ng-container>
    

    NOTE: Change your addArrayControl to take account the changes.