Home > Software design >  Building form from json with repeatable inputs: Cannot find control with path formArray
Building form from json with repeatable inputs: Cannot find control with path formArray

Time:12-28

I'm trying to build a form based on a json. I have a backend using Spring boot that returns the following object to an angular front app:

{
  "controls": [
    {
      "name": "genre",
      "type": "radio",
      "radioOptions": [
        { "key": "1", "value": "Mr." },
        { "key": "2", "value": "Ms." }
      ],
      "validators": {}
    },
    {
      "name": "firstName",
      "label": "First name:",
      "value": "",
      "type": "text",
      "repeat": false,
      "validators": {
        "required": true,
        "minLength": 10
      }
    },
    {
      "name": "lastName",
      "label": "Last name:",
      "value": "",
      "type": "text",
      "repeat": false,
      "validators": {}
    },
    {
      "name": "softwareTopics",
      "label": "Softwares:",
      "value": "",
      "type": "text",
      "repeat": true,
      "maxRepeat": 5,
      "validators": {
        "required": true,
        "minLength": 10
      }
    },
    {
      "name": "hobbies",
      "label": "Hobbies",
      "value": "",
      "type": "select",
      "selected": "Select your hobbies",
      "multi": false,
      "selectOptions": [
        { "key": "1", "value": "Tennis" },
        { "key": "2", "value": "Golf" },
        { "key": "3", "value": "Bike"}
      ],
      "validators": {}
    },
    {
      "name": "time",
      "label": "Time",
      "value": "",
      "type": "time",
      "validators": {}
    },
    {
      "name": "date",
      "label": "Date",
      "value": "",
      "type": "date",
      "validators": {}
    },
    {
      "name": "comments",
      "label": "Comments",
      "value": "",
      "type": "textarea",
      "validators": {}
    },
    {
      "name": "agreeTerms",
      "label": "This is a checkbox?",
      "value": "false",
      "type": "checkbox",
      "validators": {}
    },
    {
      "name": "size",
      "label": "Size",
      "value": "",
      "type": "range",
      "options": {
        "min": "0",
        "max": "100",
        "step": "1"
      },
      "validators": {}
    },
    {
      "name": "toggle",
      "label": "Do you like toggles?",
      "value": "false",
      "type": "toggle",
      "validators": {}
    }
  ]
}

In this JSON, I try to use as many type of input possibles. Most of the code works actually. In my component, I just have to call my rest API that returns me my JSON object.

ngOnInit(): void {
  // loading json response from back
  this.apiService.getForm().subscribe(
      (response: any) => {
        this.jsonResponse = response
        this.buildForm((this.jsonResponse.controls))
    },
      (error: any) => {
        console.log(error)
    },
    () => {
        console.log("Done");
    }
  )
}

From there I try to check my Json in order to generate dynamically my form. For this, I send the response to the buildForm method.

buildForm(controls: JsonFormControls[]): void {
  // we will loop all entries of JsonFormControls objects from the controls array
  console.log("controls", controls);
  let repeatedInputFormGroup = this.fb.group({});
  for (const control of controls) {
    // some inputs have one or more validators: input can be required, have a min length, max length...
    const controlValidators = [];
    // a control has a key and a value.
    // example: "validators": { "required": true, "minLength": 10 }
    // this snippet is reusable: can be optimized if used in many forms
    for (const [key, value] of Object.entries(control.validators)) {
      switch (key) {
        case 'min':
          controlValidators.push(Validators.min(value));
          break;
        case 'max':
          controlValidators.push(Validators.max(value));
          break;
        case 'required':
          if (value) {
            controlValidators.push(Validators.required);
          }
          break;
        case 'requiredTrue':
          if (value) {
            controlValidators.push(Validators.requiredTrue);
          }
          break;
        case 'email':
          if (value) {
            controlValidators.push(Validators.email);
          }
          break;
        case 'minLength':
          controlValidators.push(Validators.minLength(value));
          break;
        case 'maxLength':
          controlValidators.push(Validators.maxLength(value));
          break;
        case 'pattern':
          controlValidators.push(Validators.pattern(value));
          break;
        case 'nullValidator':
          if (value) {
            controlValidators.push(Validators.nullValidator);
          }
          break;
        default:
          break;
      }
    }
    // we must handle repeated inputs
    repeatedInputFormGroup = this.fb.group({});
    if (control.repeat) {
      repeatedInputFormGroup = this.fb.group({
        responses: this.fb.array([this.fb.group({response:''})])
      })
    }
    // we add a new control and pass an array of validators
    this.form.addControl(
      control.name,
      this.fb.control(control.value, controlValidators)
    );

    this.form.controls = { ...this.form.controls, ...repeatedInputFormGroup.controls}
  }
}

I also have some inputs that i want to duplicate. For this I tried to add them onto a formGroup named repeatedInputFormGroup. Finally, I use spread operator to finally build my form. At this stage I do not check how many items are added (maxRepeat value).

I instanciated those methods to allow user to add a new input or remove it.

// getter
get items(): FormArray {
  return this.form.get('responses') as FormArray;
}
addInputItem(): void {
  this.items.push(this.fb.group({response:''}));
}

deleteInputItem(index: number): void {
  this.items.removeAt(index);
}

My submit method do nothing special except some console.log

onSubmit(): void {
  console.log("Form is valid: "   this.form.valid);
  console.log("Form values: "   this.form.value);
}

In my HTML content, I'm chekking the form I receive and build the inputs

<!-- creating the form and loop -->
<span *ngIf="form != null">
  <form [formGroup]="form" (ngSubmit)="onSubmit()">
    <div *ngFor="let control of jsonResponse.controls">
      <div >
        <span *ngIf="control.label != '' && control.type !== 'toggle' && control.type !== 'checkbox'">
          <label >{{ control.label }}</label>
        </span>
        <!-- for inputs that are not repeatable -->
        <span *ngIf="inputTypes.includes(control.type) && control.repeat === false">
          <input
            [type]="control.type"
            [formControlName]="control.name"
            [value]="control.value"
            
          />
        </span>
        <!-- for inputs that are not repeatable -->
        <span *ngIf="inputTypes.includes(control.type) && control.repeat === true">
          <div formArrayName="{{ control.name }}">
            <div
              *ngFor="let item of items.controls; let id = index"
              
              [formGroupName]="id">
              <input
                
                formControlName="{{ control.name }}"/>
              <button type="button" 
                      (click)="deleteInputItem(id)">Remove</button>
            </div>
            <button type="button"  (click)="addInputItem()">Add entry</button>
          </div>
        </span>
        <!-- text area -->
        <span *ngIf="control.type === 'textarea'">
          <textarea
            [formControlName]="control.name"
            [value]="control.value"
            
          ></textarea>
        </span>
        <!-- select -->
        <span *ngIf="control.type === 'select'">
          <select
            [formControlName]="control.name"
            >
            <option selected>{{ control.selected }}</option>
            <option *ngFor="let option of control.selectOptions"
                    value="{{ option.key }}"> {{ option.value }}</option>
          </select>
        </span>

        <!-- range -->
        <span *ngIf="control.type === 'range'">
          <label for="{{control.name}}" >{{control.label}}</label>
          <input
            *ngIf="control.type === 'range'"
            type="range"
            [min]="control.options?.min"
            [max]="control.options?.max"
            [formControlName]="control.name"
            
            id="{{control.name}}"
          />
        </span>

        <!-- handling checkboxes -->
        <span *ngIf="control.type === 'checkbox'">
          <input  type="checkbox" [id]="control.name"/>
          <label >{{ control.label }}</label>
        </span>

        <!-- toggle -->
        <span *ngIf="control.type === 'toggle'">
          <div >
            <input  type="checkbox" id="{{ control.name }}">
            <label  for="{{ control.name }}">Default switch checkbox input</label>
          </div>
        </span>

        <!-- radio buttons -->
        <span *ngIf="control.type === 'radio'">
          <div  *ngFor="let option of control.radioOptions">
            <input  type="radio" name="{{control.name }}"
                   id="{{option.key}}-{{option.value}}" value="{{option.value}}">
            <label  for="{{option.key}}-{{option.value}}">{{option.value}}</label>
          </div>
        </span>

      </div>
    </div>
    <div>
      <button
        
        type="submit">
        Submit
      </button>
    </div>
  </form>
</span>

It seems that I have an issue with the inputs I can add with an "Add" button. First on loading the form, I do not retrieve the whole form and get an error message.

core.mjs:6469 ERROR Error: Cannot find control with path: 'softwareTopic -> 0'
    at _throwError (forms.mjs:1779)
    at setUpFormContainer (forms.mjs:1752)
    at FormGroupDirective._setUpFormContainer (forms.mjs:5437)
    at FormGroupDirective.addFormGroup (forms.mjs:5327)
    at FormGroupName.ngOnInit (forms.mjs:4189)
    at callHook (core.mjs:2526)
    at callHooks (core.mjs:2495)
    at executeInitAndCheckHooks (core.mjs:2446)
    at selectIndexInternal (core.mjs:8390)

I also noticed that my radio buttons are not set when I submit form. It's a cool stuff if it works.

CodePudding user response:

For repeated controls, instead of using another FormControl repeatedInputFormGroup, we can represent them using FormArray. Below is the code that can be added after switch block within buildForm():

  // we must handle repeated inputs
  const formControl = this.fb.control(control.value, controlValidators);
  if (control.repeat) {
    this.form.addControl(control.name, this.fb.array([formControl]));
  } else {
    this.form.addControl(control.name, formControl);
  }

We can modify addInputItem and deleteInputItem as:

  addInputItem(name: string): void {
    // Haven't taken care of setting validators
    (<FormArray>this.form.get(name)).push(this.fb.control(''));
  }

  deleteInputItem(name: string, index: number): void {
    (<FormArray>this.form.get(name)).removeAt(index);
  }

HTML for repeated control:

<span *ngIf="inputTypes.includes(control.type) && control.repeat === true">
  <div formArrayName="{{ control.name }}">
    <div *ngFor="let item of form.get(control.name)['controls']; let id = index"
      >
      <input  formControlName="{{ id }}" />
      <button
        type="button"
        
        (click)="deleteInputItem(control.name, id)">
        Remove
      </button>
    </div>
    <button
      type="button"
      
      (click)="addInputItem(control.name)">
      Add entry
    </button>
  </div>
</span>

Regarding radio buttons not getting set, you would need to add formControlName directive as:

<input  type="radio" name="{{ control.name }}"
value="{{ option.value }}" id="{{ option.key }}-{{ option.value }}"
[formControlName]="control.name"/>
  • Related