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

Angular 14:如何解除Mat-dialog中FormArray内FormGroup的双向绑定

问题描述

场景

使用Angular的mat-dialog编辑Deliverable类型的数据,数据包含name字段和steps数组,steps的每个元素是带step_num、step_name的对象。当前编辑时,修改steps内的字段会实时同步到原组件数据,但name的修改仅在保存时更新的表现符合预期,需要实现:取消/关闭弹窗时不修改原数据,仅点击保存时更新。

相关代码如下:

数据结构伪代码

// TOTALLY PSEUDO
Deliverable = FormGroup({
  name: FormControl,
  steps: FormArray[FormGroup({
    step_num: FormControl,
    step_name: FormControl
  }), FormGroup({
    step_num: FormControl,
    step_name: FormControl
  })]
})

原组件编辑函数

export interface Deliverable {
  name: string,
  steps: any[]
}

edit(deliverable: Deliverable) {
  const tempSteps = Object.assign([], deliverable.steps)
  const dialogRef = this.dialog.open(DeliverableDialogComponent, {
    data: { name: deliverable.name, steps: tempSteps }
  })

  dialogRef.afterClosed().subscribe(result => {
    console.log(result)
  })
}

弹窗组件HTML

<mat-dialog-content>
    <mat-form-field class="form-field" appearance="outline">
        <mat-label>Deliverable Name</mat-label>
        <input matInput type="text" autocomplete="off" [(ngModel)]="data.name" />
    </mat-form-field>
    <div *ngFor="let step of data.steps; let stepIndex=index">
        <mat-form-field class="form-field" appearance="outline">
            <mat-label>Step Number</mat-label>
            <input matInput type="text" autocomplete="off" [(ngModel)]="step.step_num">
        </mat-form-field>
        <mat-form-field class="form-field" appearance="outline">
            <mat-label>Step Name</mat-label>
            <input matInput type="text" autocomplete="off" [(ngModel)]="step.step_name">
        </mat-form-field>
    </div>
</mat-dialog-content>

弹窗组件TS

import { Component, Inject, OnInit } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';

export interface Deliverable {
  name: string;
  steps: any[];
}

@Component({
  selector: 'app-deliverable-dialog',
  templateUrl: './deliverable-dialog.component.html',
  styleUrls: ['./deliverable-dialog.component.scss']
})
export class DeliverableDialogComponent implements OnInit {

  constructor(public dialogRef: MatDialogRef<DeliverableDialogComponent>,
              @Inject(MAT_DIALOG_DATA) public data: Deliverable) { }

  onNoClick(): void {
    this.dialogRef.close()
  }

  ngOnInit(): void {
  }
}

核心问题

steps中的step_num和step_name修改时会实时更新原组件数据,期望仅在点击保存时更新原数据,取消或关闭弹窗时不修改原数据,name的表现已符合预期。


解决方案

问题根源是浅拷贝:Object.assign([], deliverable.steps)仅复制了数组外壳,数组内的每个step对象仍引用原数据的内存地址,导致弹窗修改直接同步原数据;而name是值类型,传递的是副本,所以不会实时修改原数据。

下面提供几种解决方案:

方案1:JSON序列化实现深拷贝

修改原组件edit函数,对steps进行深拷贝,断开与原数据的引用:

edit(deliverable: Deliverable) {
  // 深拷贝steps数组及内部所有对象
  const tempSteps = JSON.parse(JSON.stringify(deliverable.steps));
  const dialogRef = this.dialog.open(DeliverableDialogComponent, {
    data: { name: deliverable.name, steps: tempSteps }
  })

  dialogRef.afterClosed().subscribe(result => {
    if (result) {
      // 确认保存时,将修改后的数据同步到原对象
      deliverable.name = result.name;
      deliverable.steps = result.steps;
    }
  })
}

同时在弹窗HTML添加保存按钮,点击时返回修改后的数据:

<mat-dialog-actions align="end">
  <button mat-button (click)="onNoClick()">取消</button>
  <button mat-button [mat-dialog-close]="data" cdkFocusInitial>保存</button>
</mat-dialog-actions>

只有点击保存时,修改后的data才会返回给原组件,此时再更新原数据;取消或关闭弹窗时无有效结果返回,原数据保持不变。

方案2:手动遍历复制step对象

如果数据中存在JSON无法序列化的类型(如函数、undefined),可手动遍历复制每个step对象:

edit(deliverable: Deliverable) {
  const tempSteps = deliverable.steps.map(step => ({
    ...step // 展开运算符复制每个step的所有属性
  }));
  const dialogRef = this.dialog.open(DeliverableDialogComponent, {
    data: { name: deliverable.name, steps: tempSteps }
  })

  dialogRef.afterClosed().subscribe(result => {
    if (result) {
      deliverable.name = result.name;
      deliverable.steps = result.steps;
    }
  })
}

弹窗按钮逻辑同方案1。

方案3:使用Reactive Forms隔离数据(可选)

如果项目已使用响应式表单,可在弹窗中创建独立的FormGroup,基于传入数据初始化,天然隔离原数据:

// 弹窗组件TS
import { FormBuilder, FormGroup, FormArray } from '@angular/forms';

export class DeliverableDialogComponent implements OnInit {
  deliverableForm: FormGroup;

  constructor(
    public dialogRef: MatDialogRef<DeliverableDialogComponent>,
    @Inject(MAT_DIALOG_DATA) public data: Deliverable,
    private fb: FormBuilder
  ) { }

  ngOnInit(): void {
    // 基于传入数据初始化表单
    this.deliverableForm = this.fb.group({
      name: [this.data.name],
      steps: this.fb.array(
        this.data.steps.map(step => this.fb.group({
          step_num: [step.step_num],
          step_name: [step.step_name]
        }))
      )
    });
  }

  // 快捷获取steps表单数组
  get steps(): FormArray {
    return this.deliverableForm.get('steps') as FormArray;
  }

  onSave(): void {
    this.dialogRef.close(this.deliverableForm.value);
  }
}

弹窗HTML改用表单绑定:

<mat-dialog-content [formGroup]="deliverableForm">
    <mat-form-field class="form-field" appearance="outline">
        <mat-label>Deliverable Name</mat-label>
        <input matInput type="text" autocomplete="off" formControlName="name" />
    </mat-form-field>
    <div formArrayName="steps">
        <div *ngFor="let step of steps.controls; let stepIndex=index" [formGroupName]="stepIndex">
            <mat-form-field class="form-field" appearance="outline">
                <mat-label>Step Number</mat-label>
                <input matInput type="text" autocomplete="off" formControlName="step_num">
            </mat-form-field>
            <mat-form-field class="form-field" appearance="outline">
                <mat-label>Step Name</mat-label>
                <input matInput type="text" autocomplete="off" formControlName="step_name">
            </mat-form-field>
        </div>
    </div>
</mat-dialog-content>
<mat-dialog-actions align="end">
  <button mat-button (click)="onNoClick()">取消</button>
  <button mat-button (click)="onSave()" cdkFocusInitial>保存</button>
</mat-dialog-actions>

原组件afterClosed()订阅逻辑同前,收到结果后更新原数据即可。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 22:05:18