Search code examples
angularangular-reactive-forms

How Do I Add and Delete an Array in a Nested Reactive Form?


I have a Nested Reactive Form Array that I need to duplicate. For some reason, I'm only. able to duplicate one part of the array. Here is a link to the code. I've updated the code to include a cascading dropdown I did with ngModel. It seems when I add another array my selection changes with my second one. I know it's something to do with my model I just can't figure it out. I've updated the StackBlitz link below. thanks.

    import 'zone.js/dist/zone';
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { bootstrapApplication } from '@angular/platform-browser';
import {
  FormArray,
  FormBuilder,
  FormControl,
  FormGroup,
  FormsModule,
  ReactiveFormsModule,
  Validators,
} from '@angular/forms';

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CommonModule, FormsModule, ReactiveFormsModule],
  templateUrl: 'main.html',
})
export class App {
  selectedCategoryList: String = "--Choose Category--";

    categoryList: Array<any> = [
    {
        name: "Test One",
        subCategoryList: [
            {
                typeList: [
                    ""
                ],
                name: "Test 1"
            },
            {
                typeList: [
                    ""
                ],
                name: "Test 1.1"
            },
            {
                typeList: [
                    ""
                ],
                name: "Test 2.2"
            }
        ]
    },
    {
        name: "Test Two",
        subCategoryList: [
            {
                typeList: [
                    ""
                ],
                name: "Test 2"
            },
            {
                typeList: [
                    ""
                ],
                name: "Test 2.1"
            },
            {
                typeList: [
                    ""
                ],
                name: "Test 2.2"
            }
        ]
    },
    {
        name: "Test Three",
        subCategoryList: [
            {
                typeList: [
                    ""
                ],
                name: "Test 3"
            }
        ]
    }
];

selectedCountry: String = "--Choose Document Category--";

subCategoryList: Array<any> = [];
typeList: Array<any> = [];

  form!: FormGroup;

  constructor(private fb: FormBuilder) {}

  documentForm = this.fb.group({
    appCode: ['123'],
    accountType: [''],
    emailId: ['test@aol.com'],
    IDV: [''],
    envelopeRequest: this.fb.group({
      documents: this.fb.array([]),
    }),
  });

  ngOnInit() {
    // this.form = this.fb.group({
    //   documents: this.fb.array([]),
    // });
    this.initDocument();
  }

  changeCategoryList(category: any) {
    this.subCategoryList = this.categoryList.find((cat: any) => cat.name == category.target.value).subCategoryList;
  }
  
  changeSubCategoryList(subList: any) {
    this.typeList = this.categoryList.find((cat: any) => cat.name == this.selectedCountry).subCategoryList.find((stat: any) => stat.name == subList.target.value).typeList;
  }

  initDocument() {
    this.documents.push(this.newDocument());
    let documentIndex = this.documents.length - 1;
    this.newDocumentFieldGroup(documentIndex);

    //console.log(this.documentFields(0).controls.length);
  }

  newDocument() {
    return this.fb.group({
      name: new FormControl(''),
      documentId: new FormControl(''),
      documentFields: this.fb.array([]),
    });
  }

  newDocumentFieldGroup(comIndex: number) {
    this.documentFields(comIndex).push(this.newDocumentFields());

    this.documentFields(comIndex).push(
      this.newDocumentFields('documentSubCategory')
    );
  }

  newDocumentFields(name = 'documentCategory') {
    return this.fb.group({
      name: new FormControl(name),
      value: new FormControl(''),
    });
  }

  get documents() {
    // Problem was from here, should use `documentForm` --> envelopeRequest --> documents
    return this.documentForm.controls.envelopeRequest.controls
      .documents as FormArray;
  }

  documentFields(comIndex: number) {
    return this.documents.controls[`${comIndex}`].get(
      'documentFields'
    ) as FormArray;
  }

  documentFieldsIndexes(comIndex: number) {
    return Array(this.documentFields(comIndex).controls.length / 2)
      .fill(0)
      .map((x, i) => i * 2);
  }

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

bootstrapApplication(App);

Here is the HTML

<form [formGroup]="documentForm" (ngSubmit)="submit()">
  
  <ng-container formGroupName="envelopeRequest">
    <button (click)='initDocument()'>Add Document</button>
    <ng-container formArrayName="documents">
      <ng-container
        *ngFor="let control of documents.controls; let comIndex=index"
      >
        <ng-container [formGroupName]="comIndex">
          <div class="row justify-content-start mb-3">
            <div class="col-md-1">
              {{comIndex}}
              <input
                type="hidden"
                id="fname"
                name="fname"
                formControlName="documentId"
              />
              <label for="formFile" class="col-form-label">File:</label>
            </div>
            <div class="col-md-6">
              <input
                class="form-control"
                type="file"
                id="file"
                name="file"
                formControlName="name"
              />
            </div>
          </div>
          <ng-container formArrayName="documentFields">
            <ng-container
              *ngFor="let skillIndex of documentFieldsIndexes(comIndex);"
            >
              <div class="row mb-3">
                <label class="col-sm-1 col-form-label">Category:</label>
                <div class="col-md-2">
                  <ng-container [formGroupName]="skillIndex">
                    <select id="" class="form-select" formControlName="value" [(ngModel)]= "selectedCategoryList" (change)="changeCategoryList($event)">
                      <option selected>Select</option>
                      <option *ngFor="let c of categoryList">
                        {{c.name}}
                      </option>
                    </select>
                  </ng-container>
                </div>

                <label class="col-auto col-form-label">Sub-Category:</label>
                <div class="col-md-2">
                  <ng-container [formGroupName]="skillIndex + 1">
                    <select id="" class="form-select" formControlName="value" (change)="changeSubCategoryList($event)">
                      <option selected>Select</option>
                      <option *ngFor="let s of subCategoryList">{{s.name}}</option>
                    </select>
                  </ng-container>
                </div>
              </div>
            </ng-container>
          </ng-container>
        </ng-container>
      </ng-container>
    </ng-container>
  </ng-container>

  <button type="submit">Submit</button>
</form>
<pre>{{this.documentForm.value | json }}

Nested Form Array

You will see only the input file works but my other fields only repeat in the wrong spot. Any help will do. thanks


Solution

  • The issue was the below code in the initDocument method:

    this.newDocumentFieldGroup(0);
    

    which will add the new nested FormGroups into the documentFields FormArray for the first element of documents FormArray.

    You have to provide the latest index for the FormGroup in the documents FormArray. Do remember that the array index starts from 0.

    initDocument() {
      this.documents.push(this.newDocument());
      let documentIndex = this.documents.length - 1;
      this.newDocumentFieldGroup(documentIndex);
    
      //console.log(this.documentFields(0).controls.length);
    }
    

    Demo @ StackBlitz


    Updated:

    The reason why all the categories were updated to the same value as you are using [(ngModel)]:

    <select id="" class="form-select" formControlName="value" [(ngModel)]= "selectedCategoryList" (change)="changeCategoryList($event)">
    

    This results that all the form controls will share the same value. Remove the [(ngModel)] as it is not needed and you are using the Reactive Form.

    <select id="" class="form-select" formControlName="value"  (change)="changeCategoryList($event)">
      ...
    </select>
    

    If you are keen to set the default value at the beginning, would suggest using the patchValue method.

    newDocumentFields(name = 'documentCategory') {
      return this.fb.group({
        name: new FormControl(name),
        value: new FormControl(
          name == 'documentCategory' ? '--Choose Document Category--' : ''
        ),
      });
    }
    

    Demo (for updated question) @ StackBlitz