使用可重复输入从 json 构建表单:无法使用路径 formArray 找到控件

Building form from json with repeatable inputs: Cannot find control with path formArray

我正在尝试基于 json 构建表单。我有一个使用 Spring 引导的后端 returns 以下对象到 angular 前端应用程序:

{
  "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": {}
    }
  ]
}

在这个 JSON 中,我尝试使用尽可能多的输入类型。大多数代码实际上都有效。在我的组件中,我只需要调用我的 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");
    }
  )
}

从那里我尝试检查我的 Json 以便动态生成我的表单。为此,我将响应发送到 buildForm 方法。

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}
  }
}

我还有一些要复制的输入。为此,我尝试将它们添加到名为 repeatedInputFormGroup 的 formGroup 中。最后,我使用传播运算符来最终构建我的表单。在这个阶段我不检查添加了多少项目(maxRepeat 值)。

我实例化了这些方法以允许用户添加新输入或删除它。

// 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);
}

我的提交方法除了一些 console.log

没有做任何特别的事情
onSubmit(): void {
  console.log("Form is valid: " + this.form.valid);
  console.log("Form values: " + this.form.value);
}

在我的 HTML 内容中,我检查收到的表格并构建输入

<!-- creating the form and loop -->
<span *ngIf="form != null">
  <form [formGroup]="form" (ngSubmit)="onSubmit()">
    <div *ngFor="let control of jsonResponse.controls">
      <div class="mb-3">
        <span *ngIf="control.label != '' && control.type !== 'toggle' && control.type !== 'checkbox'">
          <label class="form-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"
            class="form-control"
          />
        </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"
              class="input-group mb-3"
              [formGroupName]="id">
              <input
                class="form-control"
                formControlName="{{ control.name }}"/>
              <button type="button" class="btn btn-outline-secondary"
                      (click)="deleteInputItem(id)">Remove</button>
            </div>
            <button type="button" class="btn btn-primary" (click)="addInputItem()">Add entry</button>
          </div>
        </span>
        <!-- text area -->
        <span *ngIf="control.type === 'textarea'">
          <textarea
            [formControlName]="control.name"
            [value]="control.value"
            class="form-control"
          ></textarea>
        </span>
        <!-- select -->
        <span *ngIf="control.type === 'select'">
          <select
            [formControlName]="control.name"
            class="form-select form-select-lg mb-3">
            <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}}" class="form-label">{{control.label}}</label>
          <input
            *ngIf="control.type === 'range'"
            type="range"
            [min]="control.options?.min"
            [max]="control.options?.max"
            [formControlName]="control.name"
            class="form-range"
            id="{{control.name}}"
          />
        </span>

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

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

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

      </div>
    </div>
    <div>
      <button
        class="btn btn-primary"
        type="submit">
        Submit
      </button>
    </div>
  </form>
</span>

我似乎对可以使用“添加”按钮添加的输入有疑问。首先在加载表单时,我没有检索整个表单并收到错误消息。

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)

我还注意到在提交表单时我的单选按钮没有设置。如果它能工作,这是一个很酷的东西。

对于重复控件,我们可以使用 FormArray 来表示它们,而不是使用另一个 FormControl repeatedInputFormGroup。下面是可以在 buildForm() 中的 switch 块之后添加的代码:

  // 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);
  }

我们可以修改addInputItemdeleteInputItem为:

  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为重复控制:

<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"
      class="input-group mb-3">
      <input class="form-control" formControlName="{{ id }}" />
      <button
        type="button"
        class="btn btn-outline-secondary"
        (click)="deleteInputItem(control.name, id)">
        Remove
      </button>
    </div>
    <button
      type="button"
      class="btn btn-primary"
      (click)="addInputItem(control.name)">
      Add entry
    </button>
  </div>
</span>

关于未设置单选按钮,您需要添加 formControlName 指令作为:

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