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

nested custom FormArray component doesn't bind with child form with FormArrayName

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

Stackblitz

我有 MAIN-FORM:

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-FORM 我为每个 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,
  });

主窗体 <--to--> app-cva-form-array 绑定由于某种原因不起作用。

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

请帮忙!

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

Parent Component:

HTML代码:

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

TS代码:

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

Child Component:

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

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

另请注意:

Her examples are static parent->multiple children... meaning 1 to many and not dynamic. You are attempting static parent to many dynamic child->grandChild relationships built from formArray, then trying to dynamically link grandChild form to parent formArrayIndex that was passed through the child to the grandChild. Your stackblitz deviates from the structure she is teaching, and certainly introduces some new challenges not covered in the lecture.

探索如何在 parent 级别迭代 FormArray 并从该循环内实例化您的 child->grandchild 关系可能是一个可能的解决方案,这样您就不会通过整个数组向下,只有 formGroup 会适用。

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

This is the reason formArrayName will not work in main-form <--> cva-form-array however, formControl will work for child-child to child level, as you are passing a singular formControl into your app-cva-form, from your app-cva-form-array via the *ngFor loop.

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

我相信这里要理解的关键是 formArray 只是其 children 的一个组织容器...如果没有来自额外的逻辑。

There currently doesn't appear to be the necessary functionality to accept formArray as an input, iterate/dynamically manage the array, and link changes back to the parent formArray.

当您使用 "custom Form Control" 时,您需要考虑将表单控件(不是 FormArray,不是 FormGroup)提供给 cursom 表单控件。 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 是

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

  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-bootstrapDatePicker存储一个对象{年:..月:..,日..},mat-multiselect存储一个数组,...)

Update 当然,我们可以将来自服务或类似服务的数据提供给我们的控件。我们唯一必须考虑的是我们如何提供数据。通常我喜欢制作一个函数来接收数据或 null 和 return 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);

stackblitz 更新