Recursive form (tree view) using Reactive forms in angular 6

Viewed 5975

I have understand concept of recursive elements(tree view) from below.

Link 1

In my case, I want to use it with forms; let's say simple text input recursively. JSON structure of form is as below.

JOSN Structure

I have prepared below code.I am getting Maximum call stack size exceeded error by executing below code.

Below is my component.html file.

<form [formGroup]="testForm" (ngSubmit)="onSubmit()">
    <div formArrayName="element">
        <ng-template #recursiveList let-list>       
              <div *ngFor="let item of testForm.get('element').controls;let i=index;">     
                  <div [formGroupName]="i">
                    <input type="text" formControlName="type">
                  </div> 
                  <!-- {{item.get('element')?.controls?.length}} -->
                  <div *ngIf="item.get('element')?.controls?.length > 0">          
                    <ng-container *ngTemplateOutlet="recursiveList; context:{ $implicit: item.get('element').controls }"></ng-container>
                  </div>
              </div>          
        </ng-template>
        <ng-container *ngTemplateOutlet="recursiveList; context:{ $implicit: testForm.get('element').controls }"></ng-container>
    </div>     

And here is component.ts file.

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

@Component({
    selector: 'app-test',
    templateUrl: './test.component.html',
    styleUrls: [
      './test.component.scss'
    ]
  })
export class TestComponent implements OnInit{
    testForm:FormGroup;
    element:any;

constructor(private formBuilder: FormBuilder) { }
ngOnInit() {

this.testForm=this.formBuilder.group({
  element:this.formBuilder.array([
    this.formBuilder.group({
      type:'',
      element:this.formBuilder.array([
        this.formBuilder.group({
          type:'',
          element:this.formBuilder.array([                
          ])
        })
      ])        
    })
  ])
})
}

onSubmit() {       
    console.log(this.testForm.value);
}
}
2 Answers

In fact, Bhavik Patel's answer will solve the problem with the error:

Maximum call stack size exceeded

But your form bindings won't work properly due to how Angular reactive forms work. It relies on Dependency Injection tree which for this template looks like:

FormGroup(testForm)
  |__ FormArrayName('element')
           |___ FormGroupName('0')
           |     ....
           |___ FormGroupName(n)

You will always get updates only on your top level controls.

To fix this issue you can define some prefix which will be updated inside embedded view. And then use that prefix to define formGroup on each level of your tree:

<form class="tree" [formGroup]="testForm" (ngSubmit)="onSubmit()">
  <ng-template #recursiveList let-controls let-prefix="prefix">
    <ng-container *ngFor="let item of controls; let i = index">
      <div class="tree-item" [formGroup]="testForm.get(prefix + i)">
        <input type="text" formControlName="type">
      </div>
      <div class="sub-tree" *ngIf="item.get('element')?.controls?.length">
        <ng-container
          *ngTemplateOutlet="recursiveList; context:{ $implicit: item.get('element').controls, prefix: prefix + i + '.element.'  }"></ng-container>
      </div>
    </ng-container>
  </ng-template>
  <ng-container
    *ngTemplateOutlet="recursiveList; context:{ $implicit: testForm.get('element').controls, prefix: 'element.' }"></ng-container>
</form>

Ng-run Example

There are some changes in your HTML code as you are looking for the element list instead of list property itself.

Here is the updated code after resolving the issue.

component.html

 <form [formGroup]="testForm" (ngSubmit)="onSubmit()">
  <div formArrayName="element">
      <ng-template #recursiveList let-element>
            <div *ngFor="let item of element;let i=index;">
                <div [formGroupName]="i">
                  <input type="text" formControlName="type">
                </div>
                <!-- {{item.get('element')?.controls?.length}} -->
                <div *ngIf="item.get('element')?.controls?.length > 0">
                  <ng-container *ngTemplateOutlet="recursiveList; context:{ $implicit: item.get('element').controls }"></ng-container>
                </div>
            </div>
      </ng-template>
      <ng-container *ngTemplateOutlet="recursiveList; context:{ $implicit: testForm.get('element').controls }"></ng-container>
  </div>
 </form>

P.S.: And of course you should refactor your ts code as well. It should be dynamically generated.

Related