FormArray in dynamic form angular
Asked Answered
G

2

6

I have implemented a stackblitz where you can create a dynamic form using some configuration. Everything works fine till you use a FormArray. What's the idea? Basically you can have many fields in your configuration file with different type. For example checkbox, text, number select etc. In some case you should have a more complex structure and so you should have a FormArray. In this stackblitz I tried to represent what's going on

https://stackblitz.com/edit/angular-dynamic-form-builder-gycshd?file=app/dynamic-form-builder/dynamic-form-builder.component.ts

So, when I found an array type I'm going to create a FormArray instead of a simple FormControl that's my list of fields

 public fields: any[] = [
    {
      type: "text",
      name: "firstName",
      label: "First Name",
      required: true
    },
    {
      type: "text",
      name: "lastName",
      label: "Last Name",
      required: true
    },
    {
      type: "text",
      name: "email",
      label: "Email",
      required: true
    },
    {
      type: "text",
      name: "description",
      label: "Description",
      required: true,
      multiLine: true
    },
    {
      type: "dropdown",
      name: "country",
      label: "Country",
      value: "in",
      required: true,
      options: [{ value: "in", label: "India" }, { value: "us", label: "USA" }]
    },
    {
      type: "array",
      name: "users",
      label: "Users",
      structure: "users.json"
    }
  ];

And here's my dynamic form builder component

export class DynamicFormBuilderComponent implements OnInit {
  @Output() onSubmit = new EventEmitter();
  @Input() fields: any[] = [];
  form: FormGroup;
  allDatas: any;
  constructor() {}

  ngOnInit() {
    let fieldsCtrls = {};
    this.allDatas = getData();
    for (let f of this.fields) {
      if (f.type != "array") {
        fieldsCtrls[f.name] = new FormControl(
          this.allDatas[f.name] || "",
          Validators.required
        );
      } else {
        fieldsCtrls[f.name] = new FormArray([
          new FormControl(this.allDatas[f.name] || "", Validators.required)
        ]);
      }
    }
    this.form = new FormGroup(fieldsCtrls);
  }
}

As you can see, I set a value from allDatas, if exists, and I create the FormGroup with FormControls and FormArray. But when I generate the arraybox I have errors. This is the arraybox component that displays a FormArray control:

import { HttpClient } from "@angular/common/http";
import { Component, Input, OnInit } from "@angular/core";
import { FormGroup } from "@angular/forms";
import { ConfigService } from "../../config.service";

@Component({
  selector: "arraybox",
  template: `
    <div [formGroup]="form">
      <div formArrayName="{{ field.name }}">
        <div
          *ngFor="let obj of arrayFileds; index as idx"
          [formGroupName]="idx"
        >
          <input
            attr.type="{{ obj.type }}"
            class="form-control"
            placeholder="{{ obj.name }}"
            id="{{ obj.name }}"
            name="{{ obj.name }}"
            formControlName="{{ idx }}"
          />
        </div>
      </div>
    </div>
  `
})
export class ArrayBoxComponent implements OnInit {
  @Input() field: any = {};
  @Input() form: FormGroup;
  arrayFileds: any = [];

  get isValid() {
    return this.form.controls[this.field.name].valid;
  }
  get isDirty() {
    return this.form.controls[this.field.name].dirty;
  }

  constructor(private config: ConfigService) {}

  ngOnInit(): void {
    this.arrayFileds = this.config.getData(this.field.structure);
    console.log(this.arrayFileds);
  }
}

getData() returns the structure of that FormArray. In this case

[{
    "label": "Username",
    "name": "userName",
    "type": "text"
}, {
    "label": "User email",
    "name": "userEmail",
    "type": "text"
}]

The errors in console are:

ERROR
Error: Cannot find control with path: 'users -> 0 -> 0'
ERROR
Error: Cannot find control with path: 'users -> 1'

I really don't know what's wrong here. How can I use FormArray to show the fields with their values in this form?

NB. stackblitz gets me an error if I use http to parse json so for now I'm using directly an import of the json. Basically, this.field.structure in this example is not used (but it should be).

Goldston answered 3/11, 2020 at 10:18 Comment(0)
C
3

Well, nice work but you need to change a few things to achieve what you want.

  1. You need to change how you initialize the form array. You have named controls in your FormArray so you need to push FormGroup into your FormArray :
 for (let f of this.fields) {
      if (f.type != "array") {
        fieldsCtrls[f.name] = new FormControl(
          this.allDatas[f.name] || "",
          Validators.required
        );
      } else { // changed part
        fieldsCtrls[f.name] = new FormArray([
          ...this.allDatas[f.name].map(
            r =>
              new FormGroup(
                Object.entries(r).reduce((acc, [k, v]) => {
                  acc[k] = new FormControl(v || "", Validators.required);
                  return acc;
                }, {})
              )
          )
        ]);
      }

The above code basically generates FormGroup that has the object keys as FormControls

  1. You need to change your ArrayBox components template like below :
 <input
            attr.type="{{ obj.type }}"
            class="form-control"
            placeholder="{{ obj.name }}"
            id="{{ obj.name }}"
            name="{{ obj.name }}"
            formControlName="{{ obj.name }}"
          />

You had formControlName="{{ idx }}" which will cause Angular to seek a FormControl named 0 1 that's one of the reasons why you get Error: Cannot find control with path: 'users -> 0 -> 0' error. The other reason is you were adding directly FormControls instead FormGroups. As your ArrayBox template states FormArray has an array of FormGroup

Working Stackblitz

As @End.Game noticed the array part still had some parts to fix. The root cause of the problem lies in the ArrayBox template. You were iterating over your configuration objects to render your form. But since it's a FormArray you also need to repeat that template for the count of the FormArray. So you will need another *ngFor to achieve that :

   <div [formGroup]="form">
      <div formArrayName="{{ field.name }}">
        <div *ngFor="let frm of formArray.controls; index as idx">
          <div formGroupName="{{ idx }}">
            <div *ngFor="let obj of arrayFileds;">
              <input
                attr.type="{{ obj.type }}"
                class="form-control"
                placeholder="{{ obj.name }}"
                id="{{ obj.name }}"
                name="{{ obj.name }}"
                formControlName="{{ obj.name }}"
              />
            </div>
            <hr />
          </div>
        </div>
      </div>
    </div>

Please also note that moved idx variable to the parent loop.

Corncob answered 3/11, 2020 at 11:15 Comment(2)
Thank you for your answer. Your example it's working, in part. I mean, It's ok and it goes as I want, but it shows only one result. I can only see { userName: "Second test", userEmail: "[email protected]" } and not the first one. Can you confirm that?Goldston
@End.Game I noticed that now. Will update the question and stackblitz.Corncob
K
0

Check this stackblitz example:-

https://stackblitz.com/edit/angular-dynamic-form-creation

hope it will be useful for you !

Kirovabad answered 3/11, 2020 at 10:25 Comment(1)
It's not exactly what I was looking for. This example creates FormControl only. Not FormArrayGoldston

© 2022 - 2024 — McMap. All rights reserved.