Построитель динамических форм в angular с formarray

Я реализовал конструктор динамических форм в своем проекте, и он отлично работает. Но теперь мне нужно добавить форму в построитель динамических форм. Что будет похоже на форму внутри формы. Итак, я создал формат внутри своего ts, и он работает правильно.

Я получаю сообщение об ошибке

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

Это то, что я реализовал https://angular.io/guide/dynamic-form.

Пример кода, который я использовал: https://stackblitz.com/edit/angular-dynamic-form-builder

Итак, я создал компонент с именем formControl внутри папки «атомы», и если элемент управления fromControl, я снова вызываю управление полем.

Компонент FormControl

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

Компонент FormControl 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 = [];
  }

В родительский компонент я добавил код formarray, чтобы добавить больше

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

Может ли кто-нибудь помочь мне решить эту проблему? Заранее спасибо! :)


person Raru    schedule 15.04.2021    source источник
comment
Пожалуйста, поделитесь соответствующим кодом TS как для формы, так и для построителя полей здесь, на SO.   -  person Will Alexander    schedule 15.04.2021
comment
Итак, вы рекурсивно вызываете управление полем. в том же компоненте? Где логика для этого и почему вы это делаете?   -  person Vimal Patel    schedule 15.04.2021
comment
@WillAlexander Я также добавил код TS. Все остальные коды одинаковы в упомянутой мной ссылке stackblitz.   -  person Raru    schedule 19.04.2021
comment
@VimalPatel На самом деле это мое требование.   -  person Raru    schedule 19.04.2021


Ответы (1)


puff, это большое и сложное улучшение кода, позволяющее использовать fromGroups и formArray, но мы собираемся попробовать.

Сначала мы собираемся изменить несколько компонентов DynamicFormBuilderComponent.

Мы собираемся создать функцию getForm, чтобы можно было рекурсивно создавать 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;
      }
    }

Обратите внимание на это, прежде чем мы создадим объект fields и, наконец, добавим поля объекта в formGroup. Используя эту функцию, мы напрямую создаем пустую группу форм и добавляем элементы управления формами, или группу форм, или массив форм, используя group.addControl. Это позволяет нам вызывать рекурсивную функцию, если это необходимо.

Итак, в ngOnInit делаем

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

Посмотрите, что мы можем получить доступ к форме из родителя, используя переменную шаблона.

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

И как я решаю, что форма имеет точную модель, прежде чем у нас будет объект с {fields:exactModel}-

И нам нужна вспомогательная функция для создания formGroup массива форм.

  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
  }

Наш FieldBuilderComponent должен учитывать два новых типа полей: массив и группа. Я собираюсь поместить его в набор полей

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

Я использую две вспомогательные функции, которые возвращают только formGroup и formArray

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

Посмотрите, как внутри мы вызываем собственный компонент. Мне нужно добавить атрибут noLabel, чтобы не отображать метку, если мы управляем FormArrayLabel

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

И использовать

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

Мне нужно еще раз повторить функцию addGroupArray в этом компоненте (другого способа я не представляю)

Ну, единственное, что нужно учитывать, это то, как погрузить значение в formArray, увидеть, что поле массива похоже на:

{
  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
    }
  ]
},

Как обычно, это stackblitz без гарантии

ПРИМЕЧАНИЕ. атом для параметров должен быть

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

Обновить На самом деле мне никогда не нравилось разбивать серию флажков в массиве со значениями true/false. Более естественно, что значение было, например. c,f и установлен флажок кулинария и рыбалка.

Ну, во-первых, измените флажок атома, используя [ngModel] и (ngModelChange). Сначала мы собираемся создать две вспомогательные функции:

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

Что ж, теперь мы можем использовать наши [ngModel] и (ngModelChange). Нам нужно сказать Angular, что это автономный

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

Итак, убираем случай чекбокса в out функции getForm

  getForm(group: FormGroup, fields: any[]) {
    for (let f of fields) {
      switch (f.type) {
        case "group":
          ...
          break;
        case "array":
           ....
        break;
        default:
          ...
          break;
      }
    }
  }
person Eliseo    schedule 15.04.2021
comment
Большое спасибо @Элизео. Я мог бы исправить свою проблему, изучив то, как вы дали здесь - person Raru; 19.04.2021