Angular Material - 在扩展面板的 FormArray 上使用 *ngFor 后应用崩溃

Angular Material - App crashes after using *ngFor on a FormArray for expansion panels

首先感谢@AJT_82的建议!

我一直在研究一个更简单的代码示例,可以在 stackblitz.com/edit/angular-42gobh 找到重现错误的代码 已对有问题的行进行注释,以便您查看正确的结果。 只需取消对 <div [formGroup]="i"></div> 的注释即可使一切崩溃

基本上,我有一个为我的组件构建表单的服务,HTML 文件使用 Angular Material。当手风琴用于 formArray 时,应用程序完全崩溃并且无法正确分配 formGroup:

客户-edit.service.ts:

import { Injectable } from '@angular/core';
import { FormGroup, FormBuilder, FormArray, Validators } from '@angular/forms';

@Injectable({
  providedIn: 'root'
})
export class CustomerEditService {
  private cusForm: FormGroup = this.fb.group({
    thirdParty: this.fb.group({
      name: this.fb.control(null),
      vat: this.fb.control(null),
      corpoPhone: this.fb.control(null),
      corpoMail: this.fb.control(null),
      corpoWeb: this.fb.control(null),
      activityNumber: this.fb.control(null),
      addresses: this.fb.array([]),
      contacts: this.fb.array([])
    }),
    docRefs: this.fb.group({}),
    commentsArr: this.fb.group({})
  });

  constructor(
    private fb: FormBuilder
    ) {    }

    // **** EMPTY FORMS GETTERS ****

    getAddressForm(address?: any) {
      const addressForm: FormGroup = this.fb.group({
        street: this.fb.control(null),
        streetcomp: this.fb.control(null),
        streetcomp2: this.fb.control(null),
        city: this.fb.control(null),
        cp: this.fb.control(null),
        state: this.fb.control(null),
        country: this.fb.control(null),
        main: this.fb.control(null)
      });
      if (address) {
        addressForm.setValue(address);
      }
      return addressForm;
    }

    getFilledThirdPartyForm(thirdParty?: any) {
      const thirdPartyForm: FormGroup = this.fb.group({
        name: this.fb.control(null, Validators.required),
        vat: this.fb.control(null, Validators.required),
        corpoPhone: this.fb.control(null, Validators.required),
        corpoMail: this.fb.control(null, Validators.required),
        corpoWeb: this.fb.control(null, Validators.required),
        activityNumber: this.fb.control(null),
      });
      if (thirdParty) {
        Object.keys(thirdParty).map(
          el => {
            if (Object.keys(thirdPartyForm.controls).indexOf(el) !== -1 && el !== 'addresses') {
              thirdPartyForm.get(el).setValue(thirdParty[el]);
            }
        });
      }
      return thirdPartyForm;
    }

}

这是构建表单的组件的 TS 文件。该表单基于 "real life" 中的 JSON 对象(第三方)构建,此对象通过 HTTP 请求来自数据库:

import { Component, OnInit, Input } from '@angular/core';
import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms';
import { CustomerEditService } from '../customer-edit.service';

@Component({
  selector: 'app-basic-edit',
  templateUrl: './test.component.html'
})

export class testComponent implements OnInit {
  thirdParty: any = 
    {
      "addresses": [
        {
          "street": "AVENIDA ESTADOS UNIDOS, 141",
          "streetcomp": "",
          "streetcomp2": "",
          "city": "SAN BARTOLOME DE TIRAJANA ",
          "cp": "35290",
          "state": "PALMAS (LAS)",
          "country": "spain",
          "main": true
        },
        {
          "street": "OTRA DIRECCION DUMMY",
          "streetcomp": "",
          "streetcomp2": "",
          "city": "MADRID",
          "cp": "280007",
          "state": "MADRID",
          "country": "spain",
          "main": false
        }
      ],
      "contacts": [
        {
          "_id": "5cf0f6f2a3e9cf847c5861af",
          "title": "Mrs.",
          "role": "CFO",
          "firstName": "John",
          "lastName": "Doe",
          "phone": "912345654",
          "mobile": "673369900",
          "thirdParty_id": "5cf0f6d0a3e9cf847c5861aa",
          "addresses": [
            {
              "street": "AVENIDA ESTADOS UNIDOS , 141",
              "streetcomp1": "TUNTE",
              "streetcomp2": "",
              "cp": "35290",
              "city": "SAN BARTOLOME DE TIRAJANA ",
              "state": "PALMAS (LAS)"
            }
          ],
          "email": "jdoe@ketchup.com",
          "auditTrail": {
            "creation": {
              "user_id": "1",
              "creationDate": "1559213796974"
            },
            "modification": [
              {
                "user_id": "1",
                "modifDate": "1559213833358"
              }
            ]
          }
        }
      ] 
    };

  thirdPartyForm: FormGroup;

  constructor(
    private fb: FormBuilder,
    private cusEditService: CustomerEditService
  ) {

  }

  ngOnInit() {
    this.thirdPartyForm = this.cusEditService.getFilledThirdPartyForm(this.thirdParty);
    const addresses: any[] = this.thirdParty.addresses;
    const addressesFormArr: FormArray = new FormArray([]);
    addresses.forEach(
      address => {
        const currAddressForm: FormGroup = this.cusEditService.getAddressForm(address);
        addressesFormArr.push(currAddressForm);
      });
    this.thirdPartyForm.setControl(
      'addresses',
      addressesFormArr
    );
    console.log(this.thirdPartyForm.get('addresses'));
  }

  onSubmit() {
    console.log('Submitted');
  }
}

这是 HTML:

<h1>Addresses Test</h1>
<form [formGroup]="thirdPartyForm" (ngSubmit)="onSubmit()">
  <div formArrayName="addresses">
    <mat-accordion>
      <mat-expansion-panel *ngFor="let address of thirdPartyForm.get('addresses').value; index as i">
        <mat-expansion-panel-header>
          {{ address.city }}
        </mat-expansion-panel-header>
          <div [formGroupName]="i"> <!-- UNCOMMENTING THIS LINE MAKES EVERYTHING CRASH -->
          </div>
      </mat-expansion-panel>
    </mat-accordion>
  </div>
</form>

希望我能告诉您确切的原因,但解决方案是将您的迭代更改为控制而不是值以使其工作:

<mat-expansion-panel *ngFor="let address of thirdPartyForm.get('addresses').controls; index as i" >
    <mat-expansion-panel-header>
        {{ address.get('city').value }}
    </mat-expansion-panel-header>
    <div [formGroupName]="i">
        <input formControlName="city">
    </div> 
</mat-expansion-panel>

我最好的猜测是,通过访问值进行迭代并尝试在其中嵌套表单组名称,您已经创建了某种循环,其中内容不断重新呈现,因为访问 formGroupName 指令导致值重新评估,而控件则不需要,因为它是静态的 属性。表单控件中的值实际上是引擎盖下的 getter,因此它可以重新计算。函数迭代是不纯的,因此它们不断地被重新评估,结果,在 ngFor 中,内容不断被重新呈现(缺少 trackBy 子句)。

我注意到迭代值似乎在没有手风琴的情况下也能工作,所以我猜这也与这里的子组件方面有关,不断重新渲染导致它 运行 内存不足和崩溃。可能值得在他们的 github 上注册,因为他们至少应该记录你不应该尝试这样做。

我继续添加了一个测试指令,该指令将其构造函数记录到一些普通的 div 中,我在其中按值迭代,内部有和没有表单组名称指令。我发现,当 n = 表单数组的长度时,该指令将在内部没有 formGroupName 指令的情况下实例化 n*2 次。但是对于 formGroupName,它实例化了 n^2 + 2n 次。所以可以肯定的是,迭代值会导致它求值两次,然后由于某种原因添加该指令会导致它对顶部数组中的每个组再次求值,从而以指数方式呈现内容。相比之下,使用控件进行迭代会导致指令仅按预期实例化 n 次。

虽然仍然不能完全确定这一切的原因,但可以确定这会发生并且是您崩溃的原因。

演示闪电战:https://stackblitz.com/edit/angular-fgqytp?file=src/app/test-component/test.component.html

不相关的旁注,您可以像这样使用 formbuilder:

  const thirdPartyForm: FormGroup = this.fb.group({
    name: [null, Validators.required],
    vat: [null, Validators.required],
    corpoPhone: [null, Validators.required],
    corpoMail: [null, Validators.required],
    corpoWeb: [null, Validators.required],
    activityNumber: null
  })

"leaf" 假定表单生成器中的值是控件,并且它接受该数组语法以根据需要添加验证器。这是使用 formbuilder 与只做 new FormGroup({key: new FormControl(null)})

的主要好处