Angular 17+新@for语法结合表单removeAt(index)行为异常
Angular 17+ @for循环删除FormArray控件异常问题
迁移至Angular 17+并使用新模板语法时,发现@for循环存在异常行为:旧*ngFor循环中调用removeAt(index)可正常删除对应表单控件,但在@for循环中,即便传入的索引经控制台验证正确,调用removeAt(index)却始终删除数组最后一项。
问题复现代码
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { bootstrapApplication } from '@angular/platform-browser'; import { FormArray, ReactiveFormsModule, UntypedFormBuilder, } from '@angular/forms'; import 'zone.js'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-root', imports: [ReactiveFormsModule, CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: ` <form [formGroup]="baseSearchForm"> <div> <input type="text" formControlName="search" placeholder="Search for ..."> </div> <div style="margin-top: 1rem; background: lightgray; padding: 1rem; " formArrayName="searchParams"> <!-- removeFilter here will always successfully delete the right control --> <!-- <div *ngFor="let control of searchParams.controls; let i = index;"> <div style="display: flex; flex-direction: row;" [formGroupName]="i"> <input type="text" formControlName="field" placeholder="... where column ..."> <input type="text" formControlName="operator" placeholder="... is ..."> <button *ngIf="i > 0" (click)="removeFilter(i)">Remove</button> </div> </div> --> <!-- removeFilter will only delete the last element of the array, regardless of the index --> @for(control of searchParams.controls; track $index;) { <div style="display: flex; flex-direction: row;" [formGroupName]="$index"> <input type="text" formControlName="field" placeholder="... where column ..."> <input type="text" formControlName="operator" placeholder="... is ..."> <button *ngIf="$index > 0" (click)="removeFilter($index)">Remove</button> </div> } </div> <button style="margin-top: 1rem" (click)="addFilter()">Add Filter</button> </form> `, }) export class App { fb = inject(UntypedFormBuilder); baseSearchForm = this.fb.group({ search: [''], searchParams: this.fb.array([ this.fb.group({ field: [0], logicalOperator: [''], operator: [''], type: [''], values: [], }), ]), }); get searchParams() { return this.baseSearchForm.get('searchParams') as FormArray; } removeFilter(index: number) { this.searchParams.removeAt(index); } addFilter() { this.searchParams.push( this.fb.group({ field: [this.searchParams.length], logicalOperator: [''], operator: [''], type: [''], values: [], }) ); } } bootstrapApplication(App);
问题原因
核心问题在于**track $index的使用方式**。Angular新@for循环中,track用于标记列表项的唯一性,当用$index作为跟踪标识时,删除某索引的元素后,后续元素的索引会自动前移,Angular会认为这些元素的“标识”(索引)未发生变化,从而复用之前的DOM元素及表单关联。但FormArray的控件索引与原始数组绑定,这就导致DOM显示的索引和实际FormArray的控件索引出现错位,最终表现为删除了最后一项。
而旧*ngFor默认(未指定trackBy时)基于对象引用跟踪,FormArray的controls数组中每个控件都是独立对象,删除后Angular能正确识别元素变化,因此不会出现该问题。
解决方案
方案1:使用控件的唯一标识作为track依据
给每个表单组添加唯一ID属性,避免依赖索引:
// 初始化表单时添加id this.fb.group({ id: Date.now(), // 用时间戳生成唯一ID field: [0], logicalOperator: [''], operator: [''], type: [''], values: [], }) // addFilter方法中同样添加id addFilter() { this.searchParams.push( this.fb.group({ id: Date.now(), field: [this.searchParams.length], logicalOperator: [''], operator: [''], type: [''], values: [], }) ); }
修改@for循环:
@for(control of searchParams.controls; track control.get('id')?.value;) { <div style="display: flex; flex-direction: row;" [formGroupName]="searchParams.controls.indexOf(control)"> <input type="text" formControlName="field" placeholder="... where column ..."> <input type="text" formControlName="operator" placeholder="... is ..."> <button *ngIf="searchParams.controls.indexOf(control) > 0" (click)="removeFilter(searchParams.controls.indexOf(control))">Remove</button> </div> }
方案2:依赖对象引用跟踪(与*ngFor默认行为一致)
不指定track表达式,让Angular默认基于控件对象的引用识别唯一性:
@for(control of searchParams.controls; track control;) { <div style="display: flex; flex-direction: row;" [formGroupName]="searchParams.controls.indexOf(control)"> <input type="text" formControlName="field" placeholder="... where column ..."> <input type="text" formControlName="operator" placeholder="... is ..."> <button *ngIf="searchParams.controls.indexOf(control) > 0" (click)="removeFilter(searchParams.controls.indexOf(control))">Remove</button> </div> }
内容的提问来源于stack exchange,提问作者ak.leimrey
相关产品推荐
相关产品推荐

