Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get a nested FormArray control in Angular 9 ReactiveForm


EDIT

The first part of the problem was solved thanks to the answer of Chellappan above, but i am encountering other problems, that made me do a stackblitz that can be found here : https://stackblitz.com/edit/angular-ivy-bzh3dh

The problems are that i can't retrieve correctly the list of products selected from the form cause they are all outputed :

  "courses": [
    {
      "courseName": "Dessert",
      "products": [
        {
          "product": true
        },
        {
          "product": false
        },
        {
          "product": null
        }
      ]

I don't know how to be able to retrieve something like:

  "courses": [
    {
      "courseName": "Dessert",
      "products": [
        {
          "Crumble aux pommes": true
        },
        {
          "Mousse au chocolat maison": false
        },
        {
          "Riz au lait": false
        }
      ]

And the second problem is that the labels of my checkboxes are not independent. If i add a new 'course' and that i select some products, the labels of all checkboxes in other courses are changed too.

enter image description here

I hope that my post is not too messy, i don't edit messages so often on Stack Overflow

END OF EDIT


I have a reactive form with nested form Array to describe a menu that contains several courses and each course contans several products, but i can't figure out how to make it works. My actual problem is that i don't manage to retrieve my second level of controls.

Here is the json structure of the menu:

{
  "name": "",
  "description": "",
  "price": "",
  "courses": [
    {
      "courseName": "",
      "products": [
        {
          "product": ""
        }
      ]
    }
  ]
}

I followed this project as an example :

Deep nested reactive form

Here is my simplified form for readability purpose :

    <form [formGroup]="menuForm" (ngSubmit)="onSubmit(menuForm)">

        <div class="form-group">
          <input
            class="form-control"
            formControlName="name"
            type="text">
        </div>
        <div class="form-group">
          <textarea
            class="form-control"
            formControlName="description"></textarea>
        </div>
        <div class="col-md-3 form-group">
          <input
            class="form-control"
            formControlName="price"
            type="number">
        </div>
      </div>
      <div class="row col-md-12">
        <button class="btn btn-outline-dark" type="button" (click)="onAddCourse()">
          Add course
        </button>
      </div>

      <div class="row" formArrayName="courses">
        <div *ngFor="let course of getCourses(menuForm); let i = index"
            [formGroupName]="i">

          <div class="...">
              <select
                class="form-control"
                formControlName="courseName">
                <option *ngFor="let course of courses">{{course.name}}</option>
              </select>

              <button type="button" class="mt-2 btn btn-outline-dark" (click)="onLoadProducts(i)">
                Load Products // <- will load products that have course Name selected form the input 'courseName'above
              </button>

              <div class="row" formArrayName="products">
                <div class="col-md-12"
                     *ngFor="let product of getProducts(menuForm); let j = index"
                      [formGroupName]="j">
                  <input class="form-check-input" type="checkbox" id="product{{i}}-{{j}}" 
                   formControlName="product"/>
                  <label class="form-check-label" for="product{{i}}-{{j}}">product</label>
                </div>
              </div>
          </div>
        </div>
      </div>

      <br>
      <div class="row col-md-12">
        <button class="btn btn-success mr-1" type="submit" [disabled]="!menuForm.valid">{{buttonSubmitLabel}}</button>
      </div>
    </form>

And here is my form initialisation in TS :

export class MenuEditComponent implements OnInit {

  menuForm: FormGroup;
  menu: MenuModel;
  categories: CategoryModel[];
...

  constructor(private router: Router,
              private route: ActivatedRoute,
              private productService: ProductService,
              private menuService: MenuService) { }

  ngOnInit(): void {
    this.productService.getCategories().subscribe(
      result => {
        this.categories = result;
      },
      error => {
        console.log(error);
        this.categories = [];
      });
    this.initForm();
  }

  initForm() {
    this.menuForm = new FormGroup({
      name: new FormControl('', [Validators.required, Validators.maxLength(100)]),
      description: new FormControl('', Validators.maxLength(255)),
      price: new FormControl('', [Validators.required, Validators.min(0)]),
      courses: new FormArray([
        this.initCourse(),
      ])
    });
  }

  initCourse() {
    return new FormGroup({
      courseName: new FormControl(''),
      products: new FormArray([
        this.initProduct()
      ])
    });
  }

  initProduct() {
    return new FormGroup({
      product: new FormControl('')
    });
  }

  getCourses(form) {
    return form.controls.courses.controls;
  }

  getProducts(form) {
    return form.controls.products.controls;
  }

  onAddCourse() {
    const control = this.menuForm.get('courses') as FormArray;
    control.push(this.initCourse());
  }

  onLoadProducts(i: number) {
    console.log(this.menuForm.controls.courses);
    const courseControl = this.menuForm.get('courses') as FormArray;
    const course = courseControl.at(i).get('courseName').value;
    console.log(course);
    const products: ProductModel[] = this.mapProducts.get(course);
    console.log(products);
    const productControl = this.menuForm.get('courses').get[i].get('products') as FormArray;

  onSubmit(menuForm: FormGroup) {
    ...
  }

...
}

If i leave the backend like that, i have an error :

Cannot read property 'controls' of undefined at MenuEditComponent.getProducts

So my 'products' FormArray is not initialized. If i do :

  getProducts(form) {
    return form.controls.products?.controls;
  }

i don't have the error anymore, but i can't access the products formControls. If i try :

  onLoadProducts(i: number) {
    const courseControl = this.menuForm.get('courses') as FormArray;
    const course = courseControl.at(i).get('courseName').value;
    const products: ProductModel[] = this.mapProducts.get(course);
    const productControl = this.menuForm.get('courses').get[i].get('products') as FormArray;
    //On the last line i get a 'Cannot read property 'get' of undefined'
...
}

What am i doing wrong ? Why my 'products' FormArray is not initialized ?

Thanks a lot for your help !

like image 552
Alain Duguine Avatar asked Oct 27 '25 07:10

Alain Duguine


2 Answers

I have taken a look at your stackblitz and I have modified it and made it working. You don't need to bind the formControlName with your products because you are not setting them from the frontend UI rather you are just selecting them. I have added a field named added in the class ProductModel and then I have just implemented the change event for the checkboxes to mark it true or false.

Also you can see how I am setting the value for the product. When you hit the Load Products button you are adding empty FormGroup but you should also set the product control inside it with product data. You can see that in the code how i am doing it inside the LoadProducts method

After that you can see in the loop for the products how i am getting the product field. The loop is actually giving us FormGroup we have to get product field value from it

Here is the stackblitz that is doing the job.

https://stackblitz.com/edit/angular-ivy-ohwv1v

Particular you can note this part

if (this.productsByCategory) {
  this.productsByCategory.forEach(
  (product) => {
    const productForm: FormGroup = this.initProduct();
    productForm.controls.product.setValue(product);
      control.push(productForm);
    });
}

After creating the productFormGroup you should also set the product in it. like productForm.controls.product.setValue(product);

And then in the frontend you can see

<div *ngFor="let p of getProducts(course); let j = index" [formGroupName]="j">
  <input type="checkbox" id="product{{i}}-{{j}}" [value]="p.value.product.added" (change)="p.value.product.added = !p.value.product.added"/>
  <label  for="product{{i}}-{{j}}">{{p.value.product.name}}</label>
</div>

you should have the value bind to the added attribute of the product and the name should be shown inside label from the product object too.

Hopefully this will help you proceed further.

And the second problem is that the labels of my checkboxes are not independent. If i add a new 'course' and that i select some products, the labels of all checkboxes in other courses are changed too.

The above issue is resolved

I don't know how to be able to retrieve something like:

Rather than productName: true/false you have whole product object available and the added field will mark whether its checked/unchecked

like image 167
muasif80 Avatar answered Oct 28 '25 22:10

muasif80


courses is array of formGroup and inside that formGroup products is another formArray so you have to pass single course formGroup to getProducts method.

component.html

<div class="row" formArrayName="courses">
  <div *ngFor="let course of getCourses(menuForm); let i = index" [formGroupName]="i">
    <div class="...">
      <select class="form-control" formControlName="courseName">
        [value]="course"
        <option *ngFor="let course of courses">{{ course.name }}</option>
      </select>
      <button type="button" class="mt-2 btn btn-outline-dark" (click)="onLoadProducts(i)">
        Load Products
      </button>
      <div class="row" formArrayName="products">
        <div class="col-md-12" *ngFor="let product of getProducts(course); let j = index" [formGroupName]="j">
          <input class="form-check-input" type="checkbox" id="product{{ i }}-{{ j }}" formControlName="product" />
          <label class="form-check-label" for="product{{ i }}-{{ j }}">product</label>
        </div>
      </div>
    </div>
  </div>
</div>

Then use get method in FormGroup to get a child control.

component.ts

getCourses(form) {
    return form.get('courses').controls;
}

getProducts(form) {
    return form.get('products').controls;
}

Example

like image 29
Chellappan வ Avatar answered Oct 28 '25 21:10

Chellappan வ



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!