Search code examples
angularangular11

Dynamic form Builder in angular with formarray


I had implemented the dynamic form builder in my project and it is working fine. But, now I need to add a form inside the dynamic form builder. Which will be like form inside a form. So, I had created a formarray inside my ts and it is coming correctly.

I am getting an error like

ERROR Error: Cannot find control with name: '891713'

This is what I had implemented https://angular.io/guide/dynamic-form

Code sample I used is https://stackblitz.com/edit/angular-dynamic-form-builder

So I had created a component named formControl inside the folder 'atoms' and if the control is fromControl, I am again calling field control.

FormControl component

            <td *ngFor="let itm of items; let i = index" formArrayName="childs" >
              <field-builder [field]="itm" [form]="form" ></field-builder>
            </td>

FormControl component Ts


  @Input() field: any = {};
  @Input() form: FormGroup;
  @Output() sendDataToParent = new EventEmitter<string>();
  // form: FormGroup;
  itemLength: any;
  items: any;
  elemData = {}
  row = [];
  isDisabled : boolean = false;
  childs: FormArray;
  formNewStatus : boolean = true;
  // childs: any;
  
  ngOnInit() {
    this.items = this.field.items;
    this.itemLength = this.items.length
    this.items.forEach(element => {
      const elemId =element.id;
      this.elemData[elemId] = ''
    });
    console.log("from control form=====>",this.form)
    this.row.push(this.elemData);
  }

  addTable() {
    const frmArr = this.form.get('childs') as FormArray;
    this.sendDataToParent.emit("add");
    this.row.push(this.elemData)
    console.log("this.row===>",this.row)
  }
  
  deleteRow(x){
    this.row.splice(x, 1 );
  } 
  deleteAllRow(){
    console.log("came here")
    this.row = [];
  }

In the parent component, I had added the code for formarray to add more

  onFormControlValueChange(parentField:any){
    if(parentField === 'add'){
      this.childs = this.form.get('childs') as FormArray;
      this.childs.push(this.fb.group(this.createItem()));
    }
    console.log("this form====>", this.form)
  }

Can anyone help me to solve this issue? Thanks in advance! :)


Solution

  • puff, it's a large and complex improve the code to allow fromGroups and formArray, but we are going to try.

    First we are going to change a few the DynamicFormBuilderComponent

    We are going to create a function getForm to can make recursive create formGroup

      getForm(group: FormGroup, fields: any[]) {
        for (let f of fields) {
          switch (f.type) {
            case "group":
              group.addControl(f.name, new FormGroup({}));
              this.getForm(group.get(f.name) as FormGroup, f.children);
    
              break;
            case "checkbox":
              group.addControl(f.name, new FormGroup({}));
              const groupOption = group.get(f.name) as FormGroup;
              for (let opt of f.options) {
                groupOption.addControl(opt.key, new FormControl(opt.value));
              }
              break;
            case "array":
              group.addControl(f.name, new FormArray([]));
              const array = group.get(f.name) as FormArray;
              if (f.value)
              {
                  f.value.forEach(x=>array.push(this.addGroupArray(f.children)))
                  array.patchValue(f.value)
              }
    
            break;
            default:
              group.addControl(
                f.name,
                new FormControl(f.value || "", Validators.required)
              );
              break;
          }
        }
    

    See that before we create an object fields and finally add the object fields to the formGroup. Using this function we directly create an empty formGroup and we are adding FormControls or FormGroup or a FormArray using group.addControl. This allow us call recursive the function if it's necesary.

    So, in ngOnInit we make

      ngOnInit() {
        this.getForm(this.form, this.fields);
      }
    

    See that we can access to the form from parent using a template variable

    <dynamic-form-builder #dynamic ..>
    {{dynamic.form?.value|json}}
    

    And how I decide that the form has the "exact" model -before we has an object with {"fields:"exactModel"}-

    And we need an auxiliar function to create the formGroup of a FormArray

      addGroupArray(fields:any[])
      {
         const group:FormGroup=new FormGroup({})
         fields.forEach(x=>{
           group.addControl(x.name,new FormControl(null,x.required?Validators.required:null))
         })
         return group
      }
    

    Our FieldBuilderComponent sould take account the new two types of fields: "array" and "group". I go to put it inside a fieldset

      <!--in case group we repeat the field-builder using
         as form the "getFormGroup" and as field "field.children"
      -->
      <fieldset *ngSwitchCase="'group'">
         <legend>{{field.name}}</legend>
         <field-builder *ngFor="let item of field.children"
           [form]="getFormGroup(field.name)" [field]="item">
         </field-builder>
      </fieldset>
    
      <!--in case array we create a table and use the function
          "getFormArray"
      -->
    
      <fieldset *ngSwitchCase="'array'">
         <legend>{{field.name}}</legend>
          <table [formArrayName]="field.name">
            <tr>
              <th *ngFor="let item of field.children">{{item.label}}</th>
              <th>
              <button class="btn btn-primary" (click)="getFormArray(field.name).push(this.addGroupArray(field.children))">Add</button>
              </th>
            </tr>
            <tr *ngFor="let group of getFormArray(field.name).controls;let i=index" [formGroupName]="i">
               <td *ngFor="let item of field.children">
                  <field-builder noLabel="true" [form]="getFormArray(field.name).at(i)"
                    [field]="item">
                  </field-builder>
                </td>
                <td><button class="btn btn-primary" (click)="getFormArray(field.name).removeAt(i)">Delete</button></td>
            </tr>
          </table>
        </fieldset>
    

    I use two auxiliar function that only return "casted" the formGroup and the formArray

      getFormGroup(field:string)
      {
        return this.form.get(field) as FormGroup
      }
      getFormArray(field:string)
      {
        return this.form.get(field) as FormArray
      }
    

    See how inside we call to the own component. I need add an "attribute" noLabel to not show the label in case we are mannage a FormArrayLabel

      //in constructor
      constructor(@Attribute('noLabel') noLabel) { 
        this.noLabel=noLabel || false;
      }
    

    And use

     <label *ngIf="!noLabel" ....></label>
    

    I need to repeat the function addGroupArray again in this component (I can't imagine another way)

    Well, the only to take account is how dive value to the formArray, see that the field of an array is like:

    {
      type: "array",
      name: "arrayName",
      value:[{firstName:"array",lastName:"array lastName"}],
      children: [
        {
          type: "text",
          name: "firstName",
          label: "First Name",
          required: true
        },
        {
          type: "text",
          name: "lastName",
          label: "Last Name",
          required: true
        }
      ]
    },
    

    As usually this is the stackblitz without warranty

    NOTE: the atom for options should be

      <div [formGroup]="form">
        <div class="form-check" *ngFor="let opt of field.options">
          <label class="form-check-label">
          <input [formControlName]="field.name"  class="form-check-input" type="radio" [value]="opt.key" >
            {{opt.label}}
          </label>
        </div>
      </div> 
    

    Update Really I never like "split" a series of checkbox in an array with true/false values. It's more natural that the value was, e.g. "c,f" and is checked the check box "cooking" and "fishing".

    Well, the first is change the "atom checkbox" using [ngModel] and (ngModelChange). First we are going to create two auxiliar functions:

      //a simple "getter" to get the value
      get value() {
        return this.form ? this.form.get(this.field.name).value : null;
      }
    
      //we pass "checked" and the "key" of the option
      change(checked: boolean, key: any) {
        const oldvalue = this.form.get(this.field.name).value || null;
    
         //if has no value
        if (!oldvalue) {
          //use setValue with the "key" (if checked) or null
          this.form.get(this.field.name).setValue(checked ? "" + key : null);
          return;
        } else {
    
          //in value store all the options that fullfilled with the condition
          const value = checked
            ? this.field.options.filter( //is in the old value or is the key
    
                x => oldvalue.indexOf(x.key) >= 0 || x.key == key
              )
            : this.field.options.filter(  //is in the old value and is not the key
                x => oldvalue.indexOf(x.key) >= 0 && x.key != key
              );
    
          //we give the value null if there're no options that fullfilled
          //or a join of the keys
          this.form
            .get(this.field.name)
            .setValue(value.length > 0 ? value.map(x => x.key).join(",") : null);
        }
      }
    

    Well, now we can use our [ngModel] and (ngModelChange). We need say to Angular that is a "standalone"

    <div [formGroup]="form">
          <div *ngFor="let opt of field.options" class="form-check form-check">
            <label class="form-check-label">
              <input
                [ngModel]="value && value.indexOf(opt.key) >= 0"
                (ngModelChange)="change($event, opt.key)"
                [ngModelOptions]="{ standalone: true }"
                class="form-check-input"
                type="checkbox"
                id="inlineCheckbox1"
                value="option1"
              />
              {{ opt.label }}</label>
          </div>
        </div>
    

    So, we remove the case of "checkbox" in out function getForm

      getForm(group: FormGroup, fields: any[]) {
        for (let f of fields) {
          switch (f.type) {
            case "group":
              ...
              break;
            case "array":
               ....
            break;
            default:
              ...
              break;
          }
        }
      }