构建含两层嵌套FormArray的Angular表单遇无限循环问题求助
问题:构建嵌套FormArray的Angular表单适配API结构
我正在构建一个包含多层嵌套FormArray的Angular表单,需要适配API预期的JSON结构,该结构由三个自定义TypeScript类型定义。
TypeScript类型定义
type Response = { label: string; questions?: Question[]; } type Question = { label: string; responses?: Response[]; } type Scenario = { title: string; description: Question[]; }
示例JSON结构
{ "title": "Cool Services", "description": [ { "label": "Assistance ménagère. Nos produits ?", "responses": [ { "label": "Gaz domestique", "questions": [ { "label": "Quantité ?", "responses": [ { "label": "7Kg", "questions": [ { "label": "Livraison à domicile ?", "responses": [ { "label": "Oui", "questions": [ { "label": "Nom du quartier ?" }, { "label": "Numéro de téléphone ?" } ] }, { "label": "Non", "questions": [ { "label": "Numéro de téléphone ?" } ] } ] } ] }, { "label": "13Kg", "questions": [ { "label": "Livraison à domicile ?", "responses": [ { "label": "Oui", "questions": [ { "label": "Nom du quartier ?" }, { "label": "Numéro de téléphone ?" } ] }, { "label": "Non", "questions": [ { "label": "Votre numéro de téléphone ?" } ] } ] } ] }, { "label": "18 Kg", "questions": [ { "label": "Livraison à domicile ?", "responses": [ { "label": "Oui", "questions": [ { "label": "Nom du quartier ?" }, { "label": "Votre numéro de téléphone ?" } ] }, { "label": "Non", "questions": [ { "label": "Votre numéro de téléphone ?" } ] } ] } ] } ] } ] }, { "label": "Marmites", "questions": [ { "label": "Catégorie ?", "responses": [ { "label": "Marmite chauffante", "questions": [ { "label": "Contenance ?", "responses": [ { "label": "3L", "questions": [ { "label": "Nom du quartier ?" }, { "label": "Votre numéro de téléphone ?" } ] }, { "label": "5L", "questions": [ { "label": "Nom du quartier ?" }, { "label": "Votre numéro de téléphone ?" } ] } ] } ] }, { "label": "Marmite ordinaire", "questions": [ { "label": "Contenance ?", "responses": [ { "label": "5L", "questions": [ { "label": "Nom du quartier ?" }, { "label": "Votre numéro de téléphone ?" } ] }, { "label": "10L", "questions": [ { "label": "Nom du quartier ?" }, { "label": "Votre numéro de téléphone ?" } ] } ] } ] } ] } ] } ] } ] }
当前组件代码(存在无限循环问题)
export class ScenarioFormComponent implements OnInit { scenarioForm!: FormGroup; questionsForm!: FormGroup; responsesForm!: FormGroup; constructor(private fb: FormBuilder) {} ngOnInit(): void { this.initScenarioForm(); } initScenarioForm() { this.scenarioForm = this.fb.group({ title: '', description: this.initQuestionsForm() }); } // INIT QUESTIONS & RESPONSES initQuestionsForm() { this.questionsForm = this.fb.group({ questions: this.fb.array([this.questionForm()]) }); return this.questionsForm } initResponsesForm() { this.responsesForm = this.fb.group({ responses: this.fb.array([this.responseForm()]) }); return this.responsesForm; } // QUESTIONS & RESPONSES FORM questionForm() { return this.fb.group({ label: '', responses: this.fb.array([this.initResponsesForm()]) }); } responseForm() { return this.fb.group({ label: '', questions: this.fb.array([this.initQuestionsForm()]) }); } // GET QUESTIONS AND RESPONSES getQuestions() { return this.questionsForm.controls['questions'] as FormArray; } getResponses() { return this.responsesForm.controls['responses'] as FormArray; } // ADD QUESTIONS & RESPONSES onAddQuestions() { return this.getQuestions().push(this.initQuestionsForm()); } onAddResponses() { return this.getResponses().push(this.initResponsesForm()); } // DELETE QUESTIONS & RESPONSES onDeleteQuestions(index: number) { this.getQuestions().removeAt(index); } onDeleteResponses(index: number) { this.getResponses().removeAt(index); } // SUBMIT SCENARIO FORM onSubmit() { } }
当前模板代码
<form [formGroup]="scenarioForm" (ngSubmit)="onSubmit()" > <div> <input type="text" placeholder="Title" formControlName="title" > </div> <div formGroupName="description"> <div formArrayName="questions" *ngFor="let creds of getQuestions().controls; let i = index"> <ng-container [formGroupName]="i"> <div> <input type="text" placeholder="Label question" formControlName="label" > </div> <div formArrayName="responses" *ngFor="let cred of getResponses().controls; let j = index"> <ng-container [formGroupName]="j"> <div> <input type="text" placeholder="Label response" formControlName="label" > </div> <button type="button" (click)="onDeleteResponses(j)"> Delete response </button> </ng-container> </div> <span (click)="onAddResponses(j)">Add response</span> <button type="button" (click)="onDeleteQuestions(i)"> Delete Question </button> </ng-container> </div> <span (click)="onAddQuestions(i)">Add question</span> </div> <div> <button type="submit" [disabled]="scenarioForm.invalid"> Submit </button> </div> </form>
问题分析与解决方案
无限循环原因
当前代码中,questionForm()调用initResponsesForm(),而initResponsesForm()创建的数组里又调用responseForm(),responseForm()再调用initQuestionsForm(),形成循环依赖,导致初始化时无限递归,引发无限循环。同时模板中获取FormArray的方式错误,全局的getQuestions()和getResponses()只能访问顶层数组,无法处理嵌套结构;循环变量作用域混乱,导致添加/删除按钮无法精准定位控件。
修正后的组件代码
import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, FormArray } from '@angular/forms'; type Response = { label: string; questions?: Question[]; } type Question = { label: string; responses?: Response[]; } type Scenario = { title: string; description: Question[]; } @Component({ selector: 'app-scenario-form', templateUrl: './scenario-form.component.html', styleUrls: ['./scenario-form.component.css'] }) export class ScenarioFormComponent implements OnInit { scenarioForm!: FormGroup; constructor(private fb: FormBuilder) {} ngOnInit(): void { this.initScenarioForm(); } // 初始化顶层表单 initScenarioForm() { this.scenarioForm = this.fb.group({ title: '', description: this.fb.array([this.createQuestionForm()]) // description直接对应Question数组 }); } // 创建单个Question表单组 createQuestionForm(question?: Question): FormGroup { return this.fb.group({ label: question?.label || '', responses: this.fb.array( question?.responses?.map(res => this.createResponseForm(res)) || [this.createResponseForm()] ) }); } // 创建单个Response表单组 createResponseForm(response?: Response): FormGroup { return this.fb.group({ label: response?.label || '', questions: this.fb.array( response?.questions?.map(q => this.createQuestionForm(q)) || [] // 初始不自动创建子Question,避免循环 ) }); } // 获取顶层的description数组(Question数组) get descriptionArray(): FormArray { return this.scenarioForm.get('description') as FormArray; } // 获取某个Question下的responses数组 getResponsesArray(questionIndex: number): FormArray { return this.descriptionArray.at(questionIndex).get('responses') as FormArray; } // 获取某个Response下的questions数组 getQuestionsArray(responseIndex: number, questionParentIndex: number): FormArray { return this.getResponsesArray(questionParentIndex).at(responseIndex).get('questions') as FormArray; } // 添加顶层Question addQuestion() { this.descriptionArray.push(this.createQuestionForm()); } // 添加某个Question下的Response addResponse(questionIndex: number) { this.getResponsesArray(questionIndex).push(this.createResponseForm()); } // 添加某个Response下的Question addSubQuestion(responseIndex: number, questionParentIndex: number) { this.getQuestionsArray(responseIndex, questionParentIndex).push(this.createQuestionForm()); } // 删除顶层Question deleteQuestion(index: number) { this.descriptionArray.removeAt(index); } // 删除某个Question下的Response deleteResponse(questionIndex: number, responseIndex: number) { this.getResponsesArray(questionIndex).removeAt(responseIndex); } // 删除某个Response下的Question deleteSubQuestion(responseIndex: number, questionParentIndex: number, subQuestionIndex: number) { this.getQuestionsArray(responseIndex, questionParentIndex).removeAt(subQuestionIndex); } // 提交表单 onSubmit() { console.log(this.scenarioForm.value); // 此处可将表单值提交至API } }
修正后的模板代码
<form [formGroup]="scenarioForm" (ngSubmit)="onSubmit()"> <!-- Title输入框 --> <div> <input type="text" placeholder="Title" formControlName="title"> </div> <!-- 顶层Question列表 --> <div formArrayName="description"> <div *ngFor="let question of descriptionArray.controls; let i = index" [formGroupName]="i"> <div> <input type="text" placeholder="Label question" formControlName="label"> <button type="button" (click)="deleteQuestion(i)">删除问题</button> </div> <!-- 当前Question下的Response列表 --> <div formArrayName="responses" style="margin-left: 20px;"> <div *ngFor="let response of getResponsesArray(i).controls; let j = index" [formGroupName]="j"> <div> <input type="text" placeholder="Label response" formControlName="label"> <button type="button" (click)="deleteResponse(i, j)">删除回复</button> </div> <!-- 当前Response下的子Question列表 --> <div formArrayName="questions" style="margin-left: 20px;"> <div *ngFor="let subQuestion of getQuestionsArray(j, i).controls; let k = index" [formGroupName]="k"> <div> <input type="text" placeholder="Label sub question" formControlName="label"> <button type="button" (click)="deleteSubQuestion(j, i, k)">删除子问题</button> </div> </div> <button type="button" (click)="addSubQuestion(j, i)">添加子问题</button> </div> </div> <button type="button" (click)="addResponse(i)">添加回复</button> </div> </div> <button type="button" (click)="addQuestion()">添加问题</button> </div> <!-- 提交按钮 --> <div> <button type="submit" [disabled]="scenarioForm.invalid">提交</button> </div> </form>
关键修正点
- 打破循环依赖:创建
createResponseForm时,初始仅生成空的子Question数组,只有用户手动点击"添加子问题"时才创建新控件,避免递归无限循环。 - 层级化访问FormArray:通过索引参数精准获取对应层级的嵌套数组,不再依赖全局变量存储单个FormGroup,适配多层嵌套结构。
- 修正模板作用域:每个循环的索引变量(i、j、k)作用域独立,添加/删除按钮能精准定位到目标控件。
- 完全匹配API结构:顶层
description直接对应FormArray,每个Question/Response的结构与TypeScript类型定义完全对齐,表单值可直接提交给API。
内容的提问来源于stack exchange,提问作者DONGMO BERNARD GERAUD
相关产品推荐
相关产品推荐

