angularangular-formsangular-ngmodelformarrayformgroups

How to Data bind to deep nested arrays? Angular Forms


Background:

I am trying to make a three-layered form front-end, the user should be able to fill out the basic details as well as the list object with the title. The list in its entirety needs to be an array the user can add to make a new list in addition to adding individual nested list items:

I am using Angular Form Groups and Form Arrays to try and achieve this:

Desired Form Inputs / Data Structure:

This is the data structure I want to achieve:

{ "name": "", "email": "", "list": 
[ { "list_title": "", 
"list_items": [ { "list_item_1": {}, "list_item_2": {} } ] } 
] }

Above you can see the desired data to be captured. In the outer form, you can see three fields. I would like to nest list titles and list items within the list field. List items is another array nested within that. The idea is to get to a position where the user can add items and list objects with new items ect..,

Code so far

I have decided to do this in a two components and have am able to access the outer items but struggling to access my inner array to data bind.

Parent-comp.html:

<p>form-comp works!</p>
<div class="form-container">
  <form (ngSubmit)="submit()" [formGroup]="myForm">
    <h1>User Registration</h1>
    <div class="form-group">
      <label for="firstname"></label>`
      <input type="text" placeholder="First Name" formControlName="name" />
      <input type="text" placeholder="Surname" formControlName="email" />
      <br />
      <div formArrayName="list">
        <ng-container *ngFor="let myList of listArray.controls; index as i">
          <div [formGroupName]="i">
            <input
              type="text"
              name="firstname"
              placeholder="List Title"
              formControlName="name"
              formControlName="list_title"
            />
            <br />
          </div>
        
          <!-- // map items here -->
          <app-products></app-products>


        </ng-container>

        <button (click)="addList()">Add List</button>
      </div>
      <button type="submit">Submit</button>
    </div>

    <br />
    <div class="form-check">
      {{ myForm.value | json }}
      <br />
      {{ myForm.valid | json }}
    </div>
  </form>
</div> 

Parent-comp-tsx

export class FormCompComponent implements OnInit {
  myForm!: FormGroup;

  constructor (private fb : FormBuilder) {

  }

  ngOnInit(): void {
    this.myForm = new FormGroup({
      name: new FormControl('', Validators.required),
      email: new FormControl('', Validators.required),
      list: new FormArray([this.initListFormGroup()]),

    });

  }

  addList() {
    this.listArray.push(this.initListFormGroup());
  }

  initListFormGroup() {
    return new FormGroup({
      list_title: new FormControl('', Validators.required),
      list_items: new FormArray([ ProductsComponent.addListItem()])
    });
  }

  get listArray() {
    return this.myForm.get('list') as FormArray;
  }

  submit() {
    console.log(this.myForm.value);
  }
}

Thhrough the code above I am able to push new list parent groups to the array and data bind by looping through these fine. I am also calling a static method on the child component to generate a new item with :

ProductsComponent.addListItem()

Child-comp.html:

<form [formGroup]="childForm">
<input type="text" name="list_item" placeholder="List Item" formControlName="name" formControlName="list_item_1" />
</form>

export class ProductsComponent {

  @Input()
  public childForm!: FormGroup;

  constructor() {}
  static addListItem(): FormGroup {
    return new FormGroup({
      list_item_1: new FormGroup(''),
      list_item_2: new FormGroup(''),
    });
  }
}

So far I am able to generate my data structure and can see this on screen when I return myForm.value However struggling to map over the child array:

      <!-- // map items here -->
          <app-products></app-products>

Above is where I believe I need to map over the inner array but not sure what the equivalent function would be to return that Array since its essentially buried.

Here is the reference tutorial at the step where my use case begins to depart:

https://youtu.be/DEuTcG8DxUI?t=652

Please let me know if this question makes sense I am eager to solve it, thank you!


Solution

  • This may look complex but the concept is similar that what you work with FormArray in the parent form and you need to ensure that you need to provide the FormGroup index for rendering the nested object in the array.

    <!-- // map items here -->
    <ng-container formArrayName="list_items">
      <ng-container
        *ngFor="
          let listItem of getListItemArrayFromList(i).controls;
          index as j
        "
      >
        <app-products [childForm]="asListItemFormGroup(listItem)"></app-products>
      </ng-container>
    </ng-container>
    
    <button (click)="addListItem(i)">Add List Item</button>
    
    getListItemArrayFromList(i: number) {
      return (this.listArray.get(`${i}`) as FormGroup).get(
        'list_items'
      ) as FormArray;
    }
    
    asListItemFormGroup(listItem: AbstractControl) {
      return listItem as FormGroup;
    }
    
    addListItem(formGroupIndex: number) {
      this.getListItemArrayFromList(formGroupIndex).push(
        ProductsComponent.addListItem()
      );
    }
    

    Note that based on your provided data, list_item_1 and list_item_2 are objects, hence you should use formGroupName attribute instead of formControlName attribute, and create a FormGroup template with FormControl(s).

    <form [formGroup]="childForm">
      <input
        type="text"
        name="list_item"
        placeholder="List Item 1"
        formGroupName="list_item_1"
      />
    
      <input
        type="text"
        name="list_item"
        placeholder="List Item 2"
        formGroupName="list_item_2"
      />
    </form>
    

    Demo @ StackBlitz