You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular深层嵌套响应式表单报错:TypeError: Cannot read properties of null (reading 'controls')

Angular深层嵌套响应式表单报错:Cannot read properties of null (reading 'controls')

问题概述

开发了一个深层嵌套的Angular响应式表单,提交时调用REST接口。表单前两层运行正常,但第三层(recipients相关部分)出现问题:曾遇到文本框一次只能输入一个字符的情况,当前报错:

ERROR TypeError: Cannot read properties of null (reading 'controls')

请求结构

{
  "appCode": "",
  "accountType": "",
  "emailId": "",
  "IDV": "",
  "emailSubject": null,
  "emailBlurb": null,
  "envelopeRequest": {
    "compositeTemplates": [
      {
        "compositeTemplateId": 13,
        "serverTemplates": [
          {
            "sequence": 93,
            "templateId": ""
          }
        ],
        "inlineTemplates": [
          {
            "sequence": 10,
            "recipients": {
              "signers": [
                {
                  "name": null,
                  "email": "",
                  "recipientId": 41
                }
              ],
              "carbonCopies": [],
              "customFields": {
                "listCustomFields": [],
                "textCustomFields": []
              }
            }
          }
        ]
      }
    ]
  }
}

现有代码

TypeScript代码

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

export class YourComponent {
  templateForm = this.fb.group({
    appCode: ['esg'],
    accountType: [''],
    emailId: [''],
    IDV: [''],
    emailSubject: [null, Validators.required],
    emailBlurb: [null, Validators.required],
    envelopeRequest: this.fb.group({
      compositeTemplates: this.fb.array([this.newCompositeTemplate()]),
    }),
  });

  constructor(private fb: FormBuilder) {}

  randomNumber(max: number) {
    return Math.floor(Math.random() * max) + 1;
  }

  compositeTemplates(): FormArray {
    return this.templateForm.get('envelopeRequest.compositeTemplates') as FormArray;
  }

  newCompositeTemplate(): FormGroup {
    return this.fb.group({
      compositeTemplateId: this.randomNumber(100),
      serverTemplates: this.fb.array([this.newTemplate()]),
      inlineTemplates: this.fb.array([this.newSequence()])
    });
  }

  addCompositeTemplate() {
    this.compositeTemplates().push(this.newCompositeTemplate());
  }

  removeCompositeTemplate(comIndex: number) {
    this.compositeTemplates().removeAt(comIndex);
  }

  serverTemplates(comIndex: number): FormArray {
    return this.compositeTemplates().at(comIndex).get('serverTemplates') as FormArray;
  }

  inlineTemplates(comIndex: number): FormArray {
    return this.compositeTemplates().at(comIndex).get('inlineTemplates') as FormArray;
  }

  signers(comIndex: number): FormArray  {
    return this.compositeTemplates().at(comIndex).get('recipients.signers') as FormArray;
  }

  addSigner(k: number){
    this.signers(k).push(this.envelopeSigners());
  }

  removeSigner(k: number, i: number) {
    this.signers(k).removeAt(i);
  }

  newTemplate(): FormGroup {
    return this.fb.group({
      sequence: this.randomNumber(100),
      templateId: '',
    });
  }

  newSequence(): FormGroup {
    return this.fb.group({
      sequence: this.randomNumber(10),
      recipients: this.fb.group({
        signers: this.fb.array([this.envelopeSigners()]),
        carbonCopies: this.fb.array([]),
        customFields: this.fb.group({
          listCustomFields: this.fb.array([]),
          textCustomFields: this.fb.array([])
        })
      })
    })
  }

  addServerTemplate(serverIndex: number) {
    this.serverTemplates(serverIndex).push(this.newTemplate());
  }

  removeServerTemplate(serverIndex: number, skillIndex: number) {
    this.serverTemplates(serverIndex).removeAt(skillIndex);
  }

  envelopeSigners(): FormGroup {
    return this.fb.group({
      name: [null, Validators.required],
      email: ['',[Validators.required, Validators.pattern("^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$")] ],
      recipientId: this.randomNumber(50)
    })
  }
}

HTML代码

<div formArrayName="compositeTemplates">
  <div *ngFor="let c of compositeTemplates().controls; let comIndex=index">
    <div [formGroupName]="comIndex">
      <input type="hidden" formControlName="compositeTemplateId" />
      <div formArrayName="serverTemplates">
        <div *ngFor="let skill of serverTemplates(comIndex).controls; let skillIndex=index">
          <div [formGroupName]="skillIndex">
            <input type="hidden" formControlName="sequence" />
            <input type="hidden" formControlName="templateId" />
          </div>
        </div>
      </div>
      <div formArrayName="inlineTemplates">
        <div *ngFor="let skill of inlineTemplates(comIndex).controls; let skillIndex=index">
          <div [formGroupName]="skillIndex">
            <input type="hidden" formControlName="sequence" />
            <ng-container formGroupName="recipients">
              <div style="margin-top: 0.3vh; position: absolute; right: 3vw;">
                <fa-icon [icon]="faPlus" (click)='addSigner(i)'></fa-icon>
              </div>
              <ng-container formArrayName="signers">
                  <ng-container *ngFor="let s of signers(comIndex).controls let i = index" [formGroupName] = "i">
                    <div style="position: absolute;right: 3vw;margin-top: 3vh;color: red;"><fa-icon [icon]="faMinus" *ngIf="i" (click)="removeSigner(i)"></fa-icon></div>
                    <div class="row mt-4">
                      <div class="col-auto">
                        <label class="col-form-label">Full Name:</label>
                      </div>
                      <div class="col-md-5">
                        <input type="text" formControlName="name" id="{{'name'+i}}" placeholder="Enter name" class="form-control">
                      </div>
                      <div class="col-auto">
                        <label class="col-form-label">To:</label>
                      </div>
                      <div class="col-md-5">
                        <input type="text" formControlName="email" id="{{'email'+i}}" placeholder="Enter email address" class="form-control">
                      </div>
                    </div>
                  </ng-container>
              </ng-container>
            </ng-container>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

错误分析与修复方案

1. 核心错误:signers方法路径错误

当前signers方法直接从compositeTemplates的对象中获取recipients.signers,但recipients实际存在于inlineTemplates数组的表单组中,导致get('recipients.signers')返回null,读取controls时报错。

修复TS中的signers、addSigner、removeSigner方法:

// 新增inlineIndex参数,支持定位到具体的inlineTemplate
signers(comIndex: number, inlineIndex: number = 0): FormArray {
  return this.inlineTemplates(comIndex).at(inlineIndex).get('recipients.signers') as FormArray;
}

addSigner(comIndex: number, inlineIndex: number = 0){
  this.signers(comIndex, inlineIndex).push(this.envelopeSigners());
}

removeSigner(comIndex: number, inlineIndex: number = 0, signerIndex: number) {
  this.signers(comIndex, inlineIndex).removeAt(signerIndex);
}

2. 模板中方法调用参数错误

模板中addSigner(i)和removeSigner(i)的参数不正确,i的作用域错误,且缺少必要的索引参数。

修复HTML代码:

<div formArrayName="compositeTemplates">
  <div *ngFor="let c of compositeTemplates().controls; let comIndex=index">
    <div [formGroupName]="comIndex">
      <input type="hidden" formControlName="compositeTemplateId" />
      <div formArrayName="serverTemplates">
        <div *ngFor="let skill of serverTemplates(comIndex).controls; let skillIndex=index">
          <div [formGroupName]="skillIndex">
            <input type="hidden" formControlName="sequence" />
            <input type="hidden" formControlName="templateId" />
          </div>
        </div>
      </div>
      <div formArrayName="inlineTemplates">
        <div *ngFor="let skill of inlineTemplates(comIndex).controls; let skillIndex=index">
          <div [formGroupName]="skillIndex">
            <input type="hidden" formControlName="sequence" />
            <ng-container formGroupName="recipients">
              <div style="margin-top: 0.3vh; position: absolute; right: 3vw;">
                <!-- 传入正确的comIndex和skillIndex -->
                <fa-icon [icon]="faPlus" (click)='addSigner(comIndex, skillIndex)'></fa-icon>
              </div>
              <ng-container formArrayName="signers">
                <!-- 调用修复后的signers方法,添加trackBy优化变更检测 -->
                <ng-container *ngFor="let s of signers(comIndex, skillIndex).controls; let i = index; trackBy: trackByIndex" [formGroupName]="i">
                  <div style="position: absolute;right: 3vw;margin-top: 3vh;color: red;">
                    <!-- 传入完整的索引参数 -->
                    <fa-icon [icon]="faMinus" *ngIf="i" (click)="removeSigner(comIndex, skillIndex, i)"></fa-icon>
                  </div>
                  <div class="row mt-4">
                    <div class="col-auto">
                      <label class="col-form-label">Full Name:</label>
                    </div>
                    <div class="col-md-5">
                      <input type="text" formControlName="name" id="{{'name'+i}}" placeholder="Enter name" class="form-control">
                    </div>
                    <div class="col-auto">
                      <label class="col-form-label">To:</label>
                    </div>
                    <div class="col-md-5">
                      <input type="text" formControlName="email" id="{{'email'+i}}" placeholder="Enter email address" class="form-control">
                    </div>
                  </div>
                </ng-container>
              </ng-container>
            </ng-container>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

3. 解决文本框只能输入一个字符的问题

模板中频繁调用signers(comIndex, skillIndex).controls会导致每次变更检测都生成新的数组引用,重置表单状态。添加trackBy函数优化变更检测:

在组件中添加trackBy函数:

trackByIndex(index: number): number {
  return index;
}

总结

  • 修正表单控件的路径定位,确保正确获取到recipients.signers表单数组
  • 同步修正模板中方法调用的参数,传递正确的索引值
  • 使用trackBy函数优化变更检测,解决输入框状态重置问题

内容的提问来源于stack exchange,提问作者fearless924

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 17:24:48