嵌套的自定义 FormArray 组件不与具有 FormArrayName 的子表单绑定

Posted

技术标签:

【中文标题】嵌套的自定义 FormArray 组件不与具有 FormArrayName 的子表单绑定【英文标题】:nested custom FormArray component doesn't bind with child form with FormArrayName 【发布时间】:2019-08-29 15:51:39 【问题描述】:

我尝试使用 CVA 创建 2 个嵌套表单。问题是当我将第二个 from 绑定到 formControl 时,它没有用数据初始化。

Stackblitz

我有主窗体

this.requestForm = this.fb.group(
  garageId: 0,
  routes: new FormArray([
    new FormGroup(
      addressPointId: new FormControl,
      municipalityId: new FormControl,
      regionId: new FormControl,
      rvId: new FormControl,
      sequenceNumber: new FormControl,
      settlementId: new FormControl,
      regionName: new FormControl,
      municipalityName: new FormControl,
      settlementName: new FormControl,
      description: new FormControl,
    )
  ]),
  endDateTime: 0,
);

在主表单 html 中,我将路由绑定到 formArrayName。

 <app-cva-form-array formArrayName="routes"></app-cva-form-array>

组件CVA-FORM-ARRAY有。

form = new FormArray([
new FormGroup(
  addressPointId: new FormControl,
  municipalityId: new FormControl,
  regionId: new FormControl,
  rvId: new FormControl,
  sequenceNumber: new FormControl,
  settlementId: new FormControl,
  regionName: new FormControl,
  municipalityName: new FormControl,
  settlementName: new FormControl,
  description: new FormControl,
)
]);

这里的一切都很好。我将数组中的每个 formGroup 绑定到子组件 CVA-FORM。

<app-cva-form [formControl]="route" (blur)="onTouched()"></app-cva-form>

CVA 格式 对于每个 formGroup,我创建了单独的组件,以防我想使用组件本身而不是整个数组。

  form: FormGroup = new FormGroup(
    regionName: new FormControl,
    regionId: new FormControl,
    municipalityName: new FormControl,
    municipalityId: new FormControl,
    sequenceNumber: new FormControl,
    settlementName: new FormControl,
    settlementId: new FormControl,
    addressPointId: new FormControl,
    description: new FormControl,
    rvId: new FormControl,
  );

main-form app-cva-form-array 绑定由于某种原因不起作用。

这些表格的想法来自kara's talk on angulaconnect. here are her slides.

请帮忙!

【问题讨论】:

不清楚你是在问背景表格吗? 是的。如果你看一下jsons。您会看到主窗体(背景窗体)与其他窗体不同步。 看看posetd的答案 【参考方案1】:

当您使用“自定义表单控件”时,您需要考虑使用表单控件(不是 FormArray,不是 FormGroup)来提供粗略的表单控件。 FormControl 有一个数组或一个对象作为值,但您不必对此感到困惑。(*)

你可以在stackblitz工作中看到

那是你的表格是这样的

//in main.form
this.requestForm = new FormGroup(
  garageId: new FormControl(0),
  routes: new FormControl(routes), //<--routes will be an array of object
  endDateTime: new FormControl(0)
)

//in cva-form-array
this.form=new FormArray([new FormControl(...)]); //<-this.form is a 
                             //formArray of FormControls NOT of formGroup

//finally in your cva-form
this.form=new FormGroup();
this.form=formGroup(
      addressPointId: new FormControl(),
      municipalityId: new FormControl(),
      ...
)

我创建了一个 const 来导出为简单的代码。我的 const 出口是

export const dataI = 
  addressPointId: "",
  municipalityId: "",
  regionId: "",
  rvId: "",
  sequenceNumber: "",
  settlementId: "",
  regionName: "",
  municipalityName: "",
  settlementName: "",
  description: "",

所以,在 mainForm 中我们有

  ngOnInit() 
    let routes:any[]=[];
    routes.push(...dataI);
    this.requestForm = new FormGroup(
      garageId: new FormControl(0),
      routes: new FormControl(routes),
      endDateTime: new FormControl(0)
    )
  
<mat-card [formGroup]="requestForm" style="background: #8E8D8A">
    <app-cva-form-array formControlName="routes"></app-cva-form-array>
</mat-card>

在 cvc-form 数组中,我们给出值时创建 formArray

  writeValue(v: any) 
    this.form=new FormArray([]);
    for (let value of v)
        this.form.push(new FormControl(value))

    this.form.valueChanges.subscribe(res=>
    
      if (this.onChange)
        this.onChange(this.form.value)
    )
  

    <form [formGroup]="form" >
        <mat-card *ngFor="let route of form.controls; 
            let routeIndex = index; let routeLast = last;">
           <button (click)="deleteRoute(routeIndex)">
             cancel
           </button>
           <app-cva-form [formControl]="route" (blur)="onTouched()"></app-cva-form>
      </form>

最后是 cva 形式

  writeValue(v: any) 
    this.form=new FormGroup();
    Object.keys(dataI).forEach(x=>
      this.form.addControl(x,new FormControl())
    )

    this.form.setValue(v,  emitEvent: false );
    this.form.valueChanges.subscribe(res=>
       if (this.onChanged)
        this.onChanged(this.form.value)
    )
  

<div [formGroup]="form">
  <mat-form-field class="locationDate">
    <input formControlName="regionName">
    <mat-autocomplete #region="matAutocomplete" 
      (optionSelected)="selectedLocation($event)">
      <mat-option *ngFor="let region of regions" 
      [value]="region">
        region.regionName
      </mat-option>
    </mat-autocomplete>
  </mat-form-field>
  <mat-form-field class="locationDate">
    <input formControlName="municipalityName" 
      [matAutocomplete]="municipality"
      (blur)="onTouched()"
      [readonly]="checked || this.form.value.regionId < 1">
   ....
   </form>

(*) 是的,我们习惯于看到 FormControl 的值是字符串或数字,但没有人禁止我们该值是对象或数组(例如,ng-bootstrap DatePicker 存储一个object year: .. month: .., day ..,mat-multiselect 存储一个数组,...)

更新当然,我们可以用来自服务或类似的数据来提供我们的控件。我们唯一必须考虑的是我们如何提供数据。通常我喜欢创建一个接收数据或 null 并返回 FormControl 的函数

  getForm(data: any): FormGroup 
    data = data ||  as IData;
    return new FormGroup(
      garageId: new FormControl(data.garageId),
      routes: new FormControl(data.routes),
      endDateTime: new FormControl(data.endDateTime)
    )
  

IData 是一个接口

export interface IData 
  garageId: number;
  routes: IDetail[];
  endDateTime: any

和IDetail另一个界面

export interface IDetail 
  addressPointId: string;
  ...
  description: string;

然后我们可以有一个复杂的数据,比如(对不起大对象)

let data = 
  garageId: 1,
  routes: [
    addressPointId: "adress",
    municipalityId: "municipallyty",
    regionId: "regionId",
    rvId: "rvId",
    sequenceNumber: "sequenceNumber",
    settlementId: "settlementId",
    regionName: "regionName",
    municipalityName: "municipalityName",
    settlementName: "settlementName",
    description: "description",
  ,
  
    addressPointId: "another adress",
    municipalityId: "another municipallyty",
    regionId: "another regionId",
    rvId: "another rvId",
    sequenceNumber: "another sequenceNumber",
    settlementId: "another settlementId",
    regionName: "another regionName",
    municipalityName: "another municipalityName",
    settlementName: "another settlementName",
    description: "another description",
  ],
  endDateTime: new Date()

那么只需要make

this.requestForm = this.getForm(data);

如果更新了堆栈闪电战

【讨论】:

感谢您的回答!我可以使用接口而不是常量吗?我不希望表单数组绑定到主表单。还是您认为我应该将 const 放在不同的文件中? 你好,我试图将解决方案放在我的项目中。我尝试加载已经存在的初始数据。我遵循与您相同的代码。但是我注意到,当我使用数据进行初始化时,它不会传输给子节点,当我尝试添加另一个路由时,它会将索引放在数组对象的前面。像这样“路线”:“0”:“regionId”:“”...“municipalityId”:“”。这也可能是初始化的问题。你知道有什么解决办法吗? @Vato,我更新了答案和stackblitz,希望对您有所帮助。关于使用接口和常量,我试过了,但没有结果:(,当然接口和常量可以在主窗体之外 @Eliseo,谢谢它很好用。但是一旦添加地址,它仍然会在数组前面添加索引。它使数组 0: 1: ... 等在添加数组之前看起来很好 [ , , ] 为了避免 ExpressionChanged... 它的 util 有时使用 setTimeOut(()=&gt;...your code..)【参考方案2】:

我认为这里的问题是formArrayName 不是NG_VALUE_ACCESSOR/DefaultValueAccessor 的输入。

另请注意:

她的例子是静态的parent-&gt;multiple children...意思是一对多而不是动态的。您正在尝试静态 parent 到从formArray 构建的许多动态child-&gt;grandChild 关系,然后尝试将grandChild 表单动态链接到通过child 传递到grandChild 的父formArrayIndex。您的 stackblitz 偏离了她所教的结构,并且肯定会引入一些讲座中未涉及的新挑战。

探索如何在parent 级别上迭代FormArray 并从该循环中实例化您的child-&gt;grandchild 关系可能是一个可能的解决方案,这样您就不会向下传递整个数组,只有@987654335 @ 那将适用。

<h1>MAIN FORM</h1>
     requestForm.value | json 
    <div *ngFor="let route of requestForm.get('routes').controls">
        <app-cva-form-array formControl="route" (onChildFormValueChange)="onFormChange($event)"></app-cva-form-array>
    </div>

选择器

输入:not([type=checkbox])[formControlName] textarea[formControlName] 输入:not([type=checkbox])[formControl] textarea[formControl] 输入:not([type=checkbox])[ngModel] textarea[ngModel] [ngDefaultControl]

https://angular.io/api/forms/DefaultValueAccessor#selectors


您唯一的输入选项是formControlNameformControlngModelngDefaultControl...

这就是formArrayNamemain-form <--> cva-form-array中工作的原因,但是formControl工作在 child-child to child level,因为您传递的是单数 formControl 进入你的app-cva-form,从你的app-cva-form-array 通过*ngFor 循环。

<mat-card *ngFor="let route of getForm.controls;
   <app-cva-form [formControl]="route" (blur)="onTouched()"></app-cva-form>

我相信这里要理解的关键是formArray 只是其子级的组织容器......在这种情况下,如果没有其他逻辑的帮助,它不会做你想要的事情。

目前似乎没有必要的功能 接受formArray 作为输入,迭代/动态管理数组, 并将链接更改回父 formArray

【讨论】:

【参考方案3】:

您需要将更新后的表单数据从子组件传递给父组件。我已经使用this.form.valueChanges() 方法来检测更改,然后将 Form 值发送到父组件。

父组件:

HTML 代码:

<app-cva-form-array formArrayName="routes" (onChildFormValueChange)="onFormChange($event)"></app-cva-form-array>

TS 代码:

public onFormChange(form): void 
    this.requestForm = form;

子组件:

HTML 代码:

No change:)

TS 代码:

@Output() onChildFormValueChange: EventEmitter<any> = new EventEmitter<any>();

registerEvent() 
    this.form.valueChanges.subscribe(() => 
      this.onFormValueChange()
    );


public onFormValueChange(): void 
    this.onChildFormValueChange.emit(this.form);

并在构造函数中调用registerEvent 方法,如:

constructor()
  this.registerEvent();

Working_Stackblitz

【讨论】:

这仅提供一种方式绑定。虽然我知道如何做两种方式,但它仍然不是我想要的。如果您检查 cva-form-array 如何绑定到 cva-form,我想为 main-form cva-form-array 实现类似的绑定。使用 cva 的目的是为了简化代码而不是使用额外的函数。 我在问题底部添加了关于代码来源的外部资源。 @Vato 不,这是双向绑定 如果您更改主表单,则 cva-form-array 不会更改。如果你写 regionName: new FormControl('asdfa'),在 main-form 中它会让子组件 regionnames 为空。 stackblitz.com/edit/angular-b6nm8c 在这里,如果您从父级添加数组,它不会只绑定到子级,反之亦然。但无论如何,正如我所说,使用 cva 不需要任何功能。

以上是关于嵌套的自定义 FormArray 组件不与具有 FormArrayName 的子表单绑定的主要内容,如果未能解决你的问题,请参考以下文章

在 Vue.js 中嵌套组件时出现未知的自定义元素

spark-xml 中具有嵌套父节点的自定义模式

为具有嵌套标签的自定义标签编写 jekyll 插件时出现问题

FormArray嵌套在另一个FormArray中时如何获取FormArrayName?

访问嵌套 Formarray 的控件

2个formarray内的嵌套复选框 - 角度材料