如何将 FormGroups 和 FormArrays 传递到深层嵌套组件的递归机制中?

How do you pass FormGroups and FormArrays into a recursive mechanism of deep nested components?

我正在构建一个微型应用程序,用于在将 JSON 对象保存到我的数据库之前对其进行编辑。 JSON 对象来自我为解析 SVG 代码和创建形状类似于下面接口的数据对象而制作的另一个微应用程序。

export interface OvaadSvgStyleProperty{
    property: string;
    setting: string;
  }

  export interface OvaadGraphicAttribute{
    attribute: string;
    setting: string;
    bind? : string;
  }

  export interface OvaadGraphicObject{
    element: string;
    selfClosing: boolean;
    attributes: OvaadGraphicAttribute[];
    styles?: OvaadSvgStyleProperty[];
    subElements?: OvaadGraphicObject[];
  }

  export interface ViewBoxParameters{
    x: string;
    y: string;
    width: string;
    height: string;
  }

  export interface OvaadSvgDataObject extends TitleCore{
      graphicId: string;
      viewBox: ViewBoxParameters;
      coreAttributes: OvaadGraphicAttribute[];
      coreStyles: OvaadSvgStyleProperty[];
      elements: OvaadGraphicObject[];
  }

如果你注意 OvaadGraphicObject 接口,你会看到 subElements 属性 是 OvaadGraphicObject[] 类型,用于处理 <g> 标签和其他 svg 元素,如果出于某种原因需要,它们可能会嵌套数千层。

到目前为止,我已经通过与 Angular 表单的斗争了解到它喜欢表单是一个单一的整体事物,所以我决定创建一个函数族来生成表单并传入来自 JSON 对象的值,并使用 [formGroup]="myFormVar".

将完成的 FormGroup 传递到我的表单中

这提供了 FormGroup 相应的形状,所有值都按预期传递。如果我像对待任何其他数据一样对待数据并将其传递到 @Input()s 中,所有内容都会分解并转到需要去的地方,但不幸的是,它会破坏表单内的通信。到目前为止,您可以查看 stackblitz demo here to see the entire mechanism,但为了这个 post,我将重点关注表单的 elements 方面,这几乎可以破解如何处理所有内容的谜题否则。

首先,我用来创建元素 FormGroup 的函数如下所示

export function CreateGraphicElementForm(data?: OvaadGraphicObject): FormGroup{
    let elementForm: FormGroup = new FormGroup({
      element     : new FormControl((data ? data.element    : ''), Validators.required),
      selfClosing : new FormControl((data? data.selfClosing : false), Validators.required),
      attributes  : new FormArray([]),
      styles      : new FormArray([]),
      subElements : new FormArray([])
    });

    if(data && data.attributes.length > 0){
      data.attributes.forEach((a: OvaadGraphicAttribute)=>{
        let attrArray: FormArray = elementForm.get('attributes') as FormArray;
        attrArray.push(CreateAttributeForm(a));
      });
    }

    if(data && data.styles.length > 0){
      data.styles.forEach((a: OvaadSvgStyleProperty)=>{
        let styleArray: FormArray = elementForm.get('styles') as FormArray;
        styleArray.push(CreateStylePropertyForm(a));
      });
    }

    if(data && data.subElements){
      data.subElements.forEach((a:OvaadGraphicObject)=>{
        let subElementArray: FormArray = elementForm.get('subElements') as FormArray;
        subElementArray.push(CreateGraphicElementForm(a));
      });

    }

    return elementForm as FormGroup;
  }

所以在这个 FormGroup 中有 2 个 FormControl 和 3 个 FormArray 需要传递到它们各自的组件中。我处理这部分的方式是我正在调用的 Element List Component 接受这些 FormGroup 的数组并负责添加或删除它们。然后我有一个 ElementFormComponent,它从 ElementListComponent 接收一个 FormGroup。然后 ElementListComponent 当然会从创建表单的父组件获取数据,我称之为 SvgObjectFormComponent。这是 ElementListComponentElementFormComponent 的样子。

ElementList.component.ts

export class ElementListComponent implements OnInit {

  @Input() ElementListData: FormArray;

  constructor() { }

  ngOnInit() {
  }

  addElement(): void{
    const newElement: FormGroup = CreateGraphicElementForm();
    let elementList: FormArray = <FormArray>this.ElementListData as FormArray;
    console.log(newElement);
    elementList.push(newElement);
  }

  deleteElement(item: number): void{
    let elementList:FormArray = this.ElementListData as FormArray;

    console.log(item);

    elementList.removeAt(item);
  }

}

ElementList.component.html

<section [formGroup]="ElementListData">


    <article *ngIf="!ElementListData">
        <p>loading</p>
    </article>

    <article *ngIf="ElementListData">

        <h5 *ngIf="ElementListData.controls.length === 0" class="articleText">no elements</h5>

        <section *ngIf="ElementListData.controls.length > 0">

            <article *ngFor="let item of ElementListData.controls; let i = index" class="list-grid">

                <p class="index-area articleText">{{i}}</p>


       <!-- I removed attempts at passing in data to avoid discussing things we already know are wrong-->
                <element-form-component class="component-area"></element-form-component>



                <article class="delete-area">
                    <button (click)="deleteElement(i)">delete</button>
                </article>


            </article>

        </section>

    </article>

    <article>
        <button (click)="addElement()">add</button>
    </article>

</section>


ElementForm.component.ts

export class ElementFormComponent implements OnInit, ControlValueAccessor {

  @Input() ElementFormData: FormGroup;

  constructor() { }

  ngOnInit() {}

}

ElementForm.component.html

<section [formGroup]="ElementFormData">
    <article class="text-control-section">
        <label class="control-label">
            element:
            <input type="text" class="text-control" formControlName="element" />
        </label>

        <label class="control-label">
            self closing:
            <input type="text" class="text-control" formControlName="selfClosing" />
        </label>
    </article>



    <!-- again I eliminated failed attempts at passing objects into components to reduce confusion -->
    <article>
        <h3>Attributes</h3>
        <attribute-list-component></attribute-list-component>
    </article>

    <article>
        <h3>Styles</h3>
        <style-list-component></style-list-component>
    </article>



    <section>

        <h3>Sub Elements</h3>

        <p *ngIf="ElementFormData.controls.subElements.length === 0">no sub elements</p>

        <article *ngFor="let item of ElementFormData.controls.subElements; let i = index" class="list-grid">

            <p class="index-area">{{i}}</p>


           <!-- this is where the recursive behavior begins if the element has nested elements -->
            <element-form-component class="component-area"></element-form-component>

            <article class="delete-area">
                <button>delete element</button>
            </article>

        </article>

    </section>




</section>

我一直在尝试各种方法,从使用 [(ngModel)][FormControl] 对比 formControlName 对比 [formControlName] 到修补 ControlValueAccessor 只是为了发现它仅适用于单个 FormControl。在另一个问题上,我问的是基于 ControlValueAccessor 有人建议我应该将其添加到我的组件提供者

viewProviders: [
   { provide: ControlContainer, useExisting: FormGroupDirective }

这消除了 ControlValueAccesssor 允许访问所有控件的需要,但假设我只在表单同步到一个 FormGroup 时仅在一个地方使用我的组件正如您从这个示例中看到的那样,我需要弄清楚如何提供到组件树顶部的递归连接。 Angular 表单如何实现这种行为?

我的 stackblitz 包含用于演示的所有组件、函数和数据对象,以及我最近通过 ControlValueAcessor 尝试使此行为起作用的尝试。有人请帮我弄清楚如何完成此操作,因为 Angular 表单是我在使用该平台的整个过程中一直无法掌握的一件事,因为我不知道它是什么前面好像找不到路了。

我遇到了 this article,其中介绍了如何通过在我们的组件上使用 ChangeDetectionStrategy.push 来实现此行为。我们像这样将它添加到我们的组件中

@Component({
    selector       : 'element-list-component',
    templateUrl    : 'element-list.component.html',
    StyleUrls      : ['element-list.component.css'],
    // add change detection here
    changeDetection: ChangeDetectionStrategy.push
})

export class ElementListComponent {

    @Input() ElementListData : FormGroup;

    //.......
}

然后在我们的组件 class 中,我们可以使用常规 @Input() 来传递表单对象,就像我们处理任何其他数据一样。在父组件中我们需要像这样传入对象

<section [formGroup]="FormDataVar">

    <element-list-component [ElementListData]="FormDataVar.controls.elements"></element-list-component

</section>

从那里我们用 formControlName="yourControl"FormControls 连接到我们的输入,所有内容都保持同步到树的顶部 :)。