Nested form array in angular
Asked Answered
V

3

1

I am making angular application with reactive form, where i have made a nested form array which will get nested on button click.

A clean working example https://stackblitz.com/edit/angular-thhczx , it has static inputs and hence on click over Add new template, it will add a another nested part and for Add new property, it will generate another property array..

So you had got the above working example concept right??

I would like to have the same json but not with add button and with dropdown.

Html of dropdown:

<select multiple [(ngModel)]="selectItems" (change)="changeEvent($event)">
      <option *ngFor="let template of templates" [value]="template.key">{{template.value}}</option>
</select>

{{selectItems|json}}

<form [formGroup]="form">
    <div *ngFor="let item of array">
            {{item.value}} is the parent
      <div *ngFor="let child of item.templateChild">
        {{child.property_name}}
        <input type="text" [value]="child.property_name">
        </div>
        <br><br><br>
      </div>
      </form>
 <br><br><br>
{{form.value|json}}

Templates array: which gives value for dropdown

    templates = [
    {
      key: 1, value: "Template one",
      templateOneChild: [
        { property_name: "Property one" },
        { property_name: "Property two" }
      ]
    },
    {
      key: 2, value: "Template two",
      templateTwoChild: [
        { property_name: "Property three" },
        { property_name: "Property four" },
        { property_name: "Property five" }
      ]
    },
    {
      key: 3, value: "Template three",
      templateThreeChild: [
        { property_name: "Property six" },
        { property_name: "Property seven" }
      ]
    }
  ]

Also made a stackblitz link for the above https://stackblitz.com/edit/angular-1sg5cv

Here if i select the option template one and template two (as the selectbox is multi select) from the dropdown then i am expecting the output as,

"template_details" : [
    { "template_name": "Template One",
      "template_data" : [{"property_one": "", "property_two":""}]
    },
    { "template_name": "Template Two",
      "template_data" : [{"property_three": "", "property_four":"", 
                            "property_five":""}]
    }
    ]

If you click over the two options of template one and two you can see that you will get two and three input boxes respectively...

I wish to generate the input boxes with property names automatically under each template on selection of dropdown values..

So in simple need to convert dropdown demo as like the static inputs with add button with the same nested json structure.

I kindly request angular experts to help me in generation of input boxes based on property names for the selected template's..

I did my level best in it unable to get the solution please help me to form nested array json on based on selection of dropdown..

Vermont answered 29/11, 2018 at 12:15 Comment(2)
I wrote an answers. It's important that you understand the two "jobs" (make a formGroup and displayed). I leave a stackblitz but try make step by step the answer (at first your .html can be only a < pre>{{form?.value|json}}</ pre>) CheersEnunciate
@Eliseo, You were amazing thank you dear..Vermont
E
3

@Undefined, you need two different jobs

  1. Create a formGroup
  2. Display inputs that manage the formGroup

the first part is the easer. Go step by step, if you select template one, you need some like

this.fb.group({
    template_name:"template one",
    template_data:this.fb.array([
          this.fb.group({
             property_one:'',
             property_two:''
          })
    ])
})

but you want to do the things dinamically, so, make a function that receive an object and return a FormGroup. As you only need the "value" of the template and the childs, your function can be like

createFormGroup(value:string,children:any[]):FormGroup
{
/*e.g. for template one, you send 
      value: "Template one",
      children: [
        { property_name: "Property one" },
        { property_name: "Property two" }
      ]
*/

     let controls:FormGroup[]=children.map(
          (x:any)=>this.fb.group({
              [x.property_name]:''
            })
     )
     return this.fb.group({
         template_name:value,
         template_data:this.fb.array(controls)
     })
}

So yet we can create a formGroup for the differents templates and join in a FormArray

changeEvent(e) {
    let arrayControls:FormGroup[] = [];
    //in this.selectItems we have, e.g. [1,3]
    for (let select of this.selectItems) {
      //search the template, select will be e.g. 1,3
      let template:any=this.templates.find(x=>x.key==select);
      switch (+select) {
        case 1:
          arrayControls.push(this.createFormGroup(template.value,template.templateOneChild));
          break;
        case 2:
          arrayControls.push(this.createFormGroup(template.value,template.templateTwoChild));
          break;
        case 3:
          arrayControls.push(this.createFormGroup(template.value,template.templateThreeChild));
          break;
       }
    }
    this.form=this.fb.group({
       template_details:this.fb.array(arrayControls);
    })
}

See that if all ours children of templates was under a property "children" (not templateOneChild for the first, templateTwoChild for the seconds...) our function becomes in

changeEvent(e) {
    let arrayControls:FormGroup[] = [];
    //in this.selectItems we have, e.g. [1,3]
    for (let select of this.selectItems) {
      //search the template, select will be e.g. 1,3
      let template:any=this.templates.find(x=>x.key==select);
      arrayControls.push(this.createFormGroup(template.value,template.children));
    }
    this.form=this.fb.group({
       template_details:this.array(arrayControls);
    })
}

Well you have the "form" created, now is time to show it. The form is like

<div *ngIf="form">
  <form [formGroup]="form">
    <div formArrayName="template_details">
      <div *ngFor="let item of details.controls;let i=index" [formGroupName]="i">
        <input formControlName="template_name">

        <div formArrayName="template_data">
          <div *ngFor="let child of item.get('template_data').controls;let j=index" [formGroupName]="j">
         <input formControlName="??????????">
          </div>
        </div>
      </div>
    </div>
  </form>
</div>

Yes, we have a problem, we don't know the "formControlName" of the inner formArray. One solution is have a variable "controlsName" that will be an array of array, so, if e.g. we choose 1 and 3 template our controlsName was like

controlsName=[
   ["property_one","property_two"],
   ["property_six",property_seven"]
]

Well, again make a function that return an array of strings with the names of the properties. it's a simply version of our createFormGroup, receive "children" and return an array of strings.

getControlNames(children:any[]):string[]
{
     let controlNames:string[]=children.map(x=>x.property_name);
     return controlNames;
}

Well, in changeEvent we call to this function after call to createFormGroup

changeEvent(e) {
    let arrayControls:FormGroup[] = [];
    let controlsName:string[] = []; //<--add this line
    for (let select of this.selectItems) {
      let template:any=this.templates.find(x=>x.key==select);
      switch (+select) {
        case 1:
          arrayControls.push(this.createFormGroup(template.value,template.templateOneChild));
           controlsName.push(this.getControlNames(template.templateOneChild)); //<--and this
          break;
        ... idem with case 2 and case 3...
      }
    }
    this.controlsName=controlsName; //<--give value to name first
    //then create the form
    this.form=this.fb.group({
       template_details:this.fb.array(arrayControls);
    })

After this, replace the < input formControlName="??????????" > by

<input [formControlName]="controlsName[i][j]"> 

See that we use [formControlName] (not formControlName) because is an evaluated expression.

See the stackblitz here

Enunciate answered 29/11, 2018 at 21:24 Comment(2)
Eliseo, No more words to express thanks for you.. Even though you was late, you helped at right time.. Thanks a lot dear because of you i am learning complex things handling in angular.. I am really lucky to have you in this forum..Vermont
Eliseo, Is there any possibility for it?? #53608177Vermont
P
0

I am not sure about your question.You want to dynamically add controls using json.

Reference link : https://angular.io/guide/dynamic-form

Working example : https://stackblitz.com/edit/angular-srpk3w

Replace your files with the below code :

app.component.html

<select multiple [(ngModel)]="selectItems" (change)="changeEvent($event)">
      <option *ngFor="let template of templates" [value]="template.key">{{template.value}}</option>
</select>

{{selectItems|json}}
<div *ngIf="form">
<form [formGroup]="form"> 
    <div *ngFor="let item of array">
            {{item.value}} is the parent
      <div *ngFor="let child of item.templateChild; index as i">
        {{child.property_name}}
        <input type="text" formControlName="{{child.property_name.split(' ').join('_')}}"  [value]="child.property_name" >
        </div> 
        <br><br><br>
      </div>
      </form>
      </div>
 <br><br><br>
{{form.value|json}}

app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {

    array: any[] = [];
    selectItems: any;
    form: FormGroup;

    templates = [
    {
      key: 1, value: "Template one",
      templateChild: [
        { property_name: "Property one" },
        { property_name: "Property two" }
      ]
    },
    {
      key: 2, value: "Template two",
      templateChild: [
        { property_name: "Property three" },
        { property_name: "Property four" },
        { property_name: "Property five" }
      ]
    },
    {
      key: 3, value: "Template three",
      templateChild: [
        { property_name: "Property six" },
        { property_name: "Property seven" }
      ]
    }
  ]


  changeEvent(e) {
    this.array = [];
    for (let select of this.selectItems) {
     this.array.push(this.templates[select-1])
     this.form=this.getFormValue(this.array);
      }     
  }
getFormValue(array){
  let group: any = {};
  array.forEach(r=>{
    r.templateChild.forEach((t,index)=>{
    group[t.property_name.replace(/ /g, "_")]= new FormControl(t.property_name);
    })
  })
return new FormGroup(group);;
}
}
Pneumonoultramicroscopicsilicovolcanoconiosis answered 29/11, 2018 at 14:22 Comment(2)
Hope this helpsPneumonoultramicroscopicsilicovolcanoconiosis
No the output is like expected output mentioned in question.. If i choose any parent template and their properties needs to list under the parent template.. In yours its getting appended every selection not separating parent and child..Vermont
R
0

try this in ts

            import { FormBuilder, FormGroup, Validators } from '@angular/forms';

constructor(private formBuilder: FormBuilder) {
  this.myForm = this.formBuilder.group({
    selectedItems: [[]],
    itemQuestions: this.formBuilder.array([])
  });
}itemQuestions: any[] = [
  { question: 'Question 1', options: ['Yes', 'No'] },
  { question: 'Question 2', options: ['Yes', 'No'] },
  { question: 'Question 3', options: ['Yes', 'No'] }
];

      generateItemQuestions(): FormGroup[] {
        const selectedItems = this.myForm.get('selectedItems').value;
        return selectedItems.map((item: string) => {
          return this.formBuilder.group({
            item: [item],
            answers: ['', Validators.required]
          });
        });
}

      onSelectionChange() {
        const itemQuestions = this.generateItemQuestions();
        this.myForm.setControl('itemQuestions',               
        this.formBuilder.array(itemQuestions));
        }

        onSubmit() {
          if (this.myForm.valid) {
            const formValue = this.myForm.value;
            console.log(formValue);
            // Perform necessary actions with the form data
          } else {
            // Form is invalid, display error messages or handle accordingly
          }
        }
       

in html

<form [formGroup]="myForm" (ngSubmit)="onSubmit()">
  <ion-item>
    <ion-label>Selected Items</ion-label>
    <ion-select formControlName="selectedItems" multiple="true">
      <ion-select-option value="Item 1">Item 1</ion-select-option>
      <ion-select-option value="Item 2">Item 2</ion-select-option>
      <!-- Add other options -->
    </ion-select>
  </ion-item>
  
  <div formArrayName="itemQuestions">
    <div *ngFor="let questionGroup of myForm.get('itemQuestions').controls; let i=index" [formGroupName]="i">
      <div>
        {{ questionGroup.get('item').value }}
      </div>
      
      <div *ngFor="let question of itemQuestions">
        <div>{{ question.question }}</div>
        <ion-radio-group formControlName="answers">
          <ion-item>
            <ion-label>Yes</ion-label>
            <ion-radio value="Yes"></ion-radio>
          </ion-item>
          <ion-item>
            <ion-label>No</ion-label>
            <ion-radio value="No"></ion-radio>
          </ion-item>
        </ion-radio-group>
      </div>
    </div>
  </div>
  
  <ion-button type="submit">Submit</ion-button>
</form>
Rushy answered 3/6, 2023 at 19:44 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Susi

© 2022 - 2024 — McMap. All rights reserved.