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

