Search code examples
angularangular-reactive-formsformarrayformgroups

Dynamically control the number of FormGroups with a FormArray


I'm trying to dynamically control the number of FormGroups with a FormArray such that I set the FormArray length.

myForm = this.fb.group({
  formGroupsToAdd: ['5'],
  formList: this.fb.array(this.getFormControls(5))
});

getFormControls(numberOfFormGroups: number) {
  const myArray = new Array(numberOfFormGroups);
  myArray.fill(this.fb.group({
    length: [''],
    width: [''],
    height: ['']
  }));
  return myArray;
}

I also have a way of getting this formList: FormArray with

get unitList() {
  return this.myForm.get('formList') as FormArray;
}

However, when I go to add a value to a single control in this FormArray, it updates all FormGroups in the FormArray.

<div formArrayName="formList">
  <div *ngFor="let unitForm of formList.controls; let i = index">
    <div [formGroup]="formList.controls[i]">
      <pre>{{formList.controls[i].value | json}}</pre>
      <pre>i = {{i}}</pre>
      <mat-form-field>
        <input matInput formControlName="length" />
      </mat-form-field>
      <mat-form-field>
        <input matInput formControlName="width" />
      </mat-form-field>
      <mat-form-field>
        <input matInput formControlName="height" />
      </mat-form-field>
    </div>
  </div>
</div>

So my formList.value looks like this...

[
  {
    "length": "",
    "width": "asdf",
    "height": ""
  },
  {
    "length": "",
    "width": "asdf",
    "height": ""
  },
  {
    "length": "",
    "width": "asdf",
    "height": ""
  },
  {
    "length": "",
    "width": "asdf",
    "height": ""
  },
  {
    "length": "",
    "width": "asdf",
    "height": ""
  }
]

Where instead I need a single FormGroup to contain of course it's own unique FormControl values.

What am I doing wrong? Why doesn't the [formGroup]="formList.controls[i]" satisfy my need here by providing a unique identifier for each formGroup?

EDIT: I've added a stackblitz with my problem.
https://stackblitz.com/edit/angular-ivy-sa7pbb?file=src/app/app.component.html


Solution

  • You should use [formGroupName]="idx" like this.

    <div formArrayName="formList">
      <div *ngFor="let unitForm of formList.controls; let i = index" [formGroupName]="i">
          <mat-form-field>
            <input matInput formControlName="length" />
          </mat-form-field>
          <mat-form-field>
            <input matInput formControlName="width" />
          </mat-form-field>
          <mat-form-field>
            <input matInput formControlName="height" />
          </mat-form-field>
      </div>
    </div>
    

    And some useful functions:

    // Call init form on init
    ngOnInit(): void {
        this.initForm();
    }
    
    // init form here
    initForm() {
        this.myForm = this.fb.group({
          formList: this.fb.array([])
        });
    
        // Call this 5 times or any times you want to init rows.
        this.addItem();
    }
    
    // Add new item to make an addition button on UI
    addItem() {
        // You can enter input into newItem function to load data from API to make update/details screen.
        this.unitList.push(this.newItem(0, 0, 0));
    }
    
    // Your function to get fromList
    unitList() {
        return this.myForm.get('formList') as FormArray;
    }
    
    // new Item as a FormGroup to add into main form and it will auto render on UI
    newItem(length: number, width: number, height: number): FormGroup {
        const newGroup = this.fb.group({
          length: [length],
          width: [width],
          height: [height]
        })
        return newGroup;
    }
    
    // Clear all items to make a refresh/clean UI
    clearAllList() {
        this.unitList.clear();
    }
    
    // Remove item at index to make a delete button
    removeItem(index: number) {
        this.unitList.removeAt(idx);
    }
    

    Updated: Fix issue on stackblitz: https://stackblitz.com/edit/angular-ivy-rtmpwc?file=src/app/app.component.ts

    The issue is myArray.fill, and when you use it, it will use 1 reference variable to set for all 5 items in your array. So when you change one item, it will ref into 5 items

    getFormControls(numberOfFormGroups) {
        const myArray = new Array(numberOfFormGroups);
        myArray.fill(this.fb.group({
          length: [''],
          width: [''],
          height: ['']
        }));
        return myArray;
      }
    

    So the fixed function should be like this. It will create 5 reference for 5 items.

    getFormControls(numberOfFormGroups) {
        const myArray = new Array();
        for (let i = 1; i <= numberOfFormGroups; i++) {
          let newItem = this.fb.group({ length: [''], width: [''], height: [''] });
          myArray.push(newItem);
        }
        return myArray;
      }