11

I'm building dynamic form and want to add form groups 'on the fly'.

Here is my code which almost works:

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

export class CombinedComponent implements OnInit {

    ltsForm: FormGroup;

    constructor(private formBuilder: FormBuilder) {
    }

    ngOnInit() {
        this.ltsForm = this.initFormGroup();

        // init products
        for (let i = 0; i < 3; i++) { // add dynamically products
            this.addProduct();
        }
        
        console.log(this.ltsForm); // Array 'prods' is empty
    }

    // initialize form group, but don't add products yet because they will be added dynamically later
    initFormGroup() {
        let group = this.formBuilder.group({
            products: this.initProductGroup()
        });

        return group;
    }

    initProductGroup() {
        let group = this.formBuilder.group(
            {
                //initialize empty formbuilder array
                prods: this.formBuilder.array([])
            }
        );

        return group;
    }

    initProducts() {
        return this.formBuilder.group({
            id: [''],
            value: false, // checkbox value
        });
    }

    addProduct() {
        <FormArray>this.ltsForm.controls['products'].value.prods.push(this.initProducts());

        console.log(this.ltsForm); // Array 'prods' contains 3 FormGroup elements
    }
} 

Template:

<form [formGroup]="ltsForm"
      novalidate
      (ngSubmit)="save(ltsForm)">

    <div formGroupName="products">
        <div formArrayName="prods">

            <div *ngFor="let product of ltsForm.controls.products.value.prods.controls; let i = index">
                <div [formGroupName]="i">
                    <input type="checkbox"
                           formControlName="value"
                           id="product_{{ i }}"
                           name="product_{{ i }}">
                </div>
            </div>

        </div>
    </div>

    <button type="submit"
            [disabled]="!ltsForm.valid">
        Submit
    </button>
</form>

In method addProduct() I push the whole FormGroup element to the 'prods' array. So at the end the output from console in ngOnInit() contains just an empty 'prods' array, while the array from console output in addProduct() method has 3 elements. It looks like this.ltsForm looses its reference and isn't updating. Any ideas?

UPD: Just found out that if I remove the whole content from template, I get the 'prods' filled with data.

Malik M
  • 101
  • 1
  • 9
rvaliev
  • 1,051
  • 2
  • 12
  • 31

1 Answers1

27

There were a number of small mistakes and complexities, so I pared down your example and built it back up. The Angular team had examples of a nested form array and nested form group that were very helpful. Here was the process (and plnkr):

  1. Got a simple group working: { projects: '' }.
  2. Got a group with an array of controls working: { projects: ['a', 'b', 'c'] }. I skipped prods, it seemed unnecessary.

    <form [formGroup]="ltsForm" novalidate (ngSubmit)="save()">
      <div formArrayName="products">
        <div *ngFor="let p of products.controls; let i=index">
          <input [formControlName]="i">
        </div>
      </div>
      <button type="submit" [disabled]="!ltsForm.valid">
        Submit
      </button>
    </form>
    
    ...
    
    export class CombinedComponent implements OnInit {
    
      ltsForm: FormGroup;
    
      get products() { return this.ltsForm.get('products'); }
    
      constructor(private formBuilder: FormBuilder) {}
    
      ngOnInit() {
          this.ltsForm = this.formBuilder.group({
            products: this.formBuilder.array([])
          });
    
          for (let i = 0; i < 3; ++i) {
            this.addProduct();
          }
      }
    
      addProduct() {
        this.products.push(this.formBuilder.control(''));
      }
    
      save() {
        console.log(this.ltsForm.value);
      }
    } 
    
  3. Final step replace controls in the array with groups:

    @Component({
      selector: 'combined-component',
      template: `
        <form [formGroup]="ltsForm" novalidate (ngSubmit)="save()">
          <div formArrayName="products">
            <div *ngFor="let p of products.controls; let i=index">
              <div [formGroupName]="i">
                <input formControlName="id">
                <input type="checkbox" formControlName="value">
              </div>
            </div>
          </div>
          <button type="submit" [disabled]="!ltsForm.valid">
              Submit
          </button>
        </form>
      `
    })
    export class CombinedComponent implements OnInit {
    
      ltsForm: FormGroup;
    
      get products() { return this.ltsForm.get('products'); }
    
      constructor(private formBuilder: FormBuilder) {}
    
      ngOnInit() {
          this.ltsForm = this.formBuilder.group({
            products: this.formBuilder.array([])
          });
    
          for (let i = 0; i < 3; ++i) {
            this.addProduct();
          }
      }
    
      addProduct() {
        this.products.push(this.formBuilder.group({
          id: '',
          value: false
        }));
      }
    
      save() {
        console.log(this.ltsForm.value);
      }
    } 
    
stealththeninja
  • 3,576
  • 1
  • 26
  • 44
  • Thank you, however I can't validate the whole form array. For example I have 3 products (checkboxes), and I need that at least 1 is checked. Then I will have another form array with materials checkboxes, where also at least 1 material should be selected. Doing products: this.formBuilder.array([], Validators.required) doesn't work, so that was the reason I wanted to create at first form group, then validate it, and within that form group add form array. – rvaliev Jan 07 '17 at 13:12
  • 1
    I think I figured it out. Using this.formBuilder.array([], Validators.required) is wrong, because 'Validators.required' validates if the value is empty or not. In case of Array it will be always validated, because array already exists. So therefore I must write custom validation function 'checkboxReqyured', and use it like this: products: this.formBuilder.array([], Validators.compose([checkboxRequired])) – rvaliev Jan 07 '17 at 14:09
  • If form consist of nestead array(ex in above case -> value[{"":""}] then how to build the form – Soumya Gangamwar Apr 07 '17 at 09:33
  • @stealththeninja I followed your approach in my Angular application, I am getting an issue in binding controls using formControlName. Please have look into my posted question: https://stackoverflow.com/questions/47754073/getting-issue-in-registering-contorl-with-form-in-angular-reactive-forms – Waleed Shahzaib Dec 11 '17 at 13:50