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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 20:21:05