- UI组件
- 前端
【免费下载链接】ng-zorro-antd
Angular UI Component Library based on Ant Design
导读
表格编辑是后台管理系统中最高频的交互场景之一。NG-ZORRO(ng-zorro-antd)官方在 Table 组件的演示集中提供了"可编辑单元格(Editable Cells)"示例,它展示了如何在保持组件高性能的前提下,以极简代码实现"点击单元格进入编辑态、失焦保存、支持新增与删除行"的完整闭环。本文以该演示为骨架,结合nz-table组件的源码实现,讲清楚三个关键问题:如何在 OnPush 变更检测策略下安全地修改表格数据、如何用 signal 管理编辑状态、以及如何把该方案扩展为行编辑或自定义编辑形态。读完本文,你将能直接复制这套模式到自己的业务表格中。
一、示例总体设计:以最小代码实现单元格编辑闭环
官方演示(见 edit-cell.ts)的核心思路非常直白:不引入任何复杂的编辑框架,用"文本与输入框互斥显隐"来模拟编辑状态。整个示例只依赖 Angular 自身的表单绑定与 NG-ZORRO 的三个模块:
imports: [FormsModule, NzButtonModule, NzInputModule, NzPopconfirmModule, NzTableModule]FormsModule:提供ngModel双向绑定,让输入框直接修改数据行对象;NzButtonModule:渲染"Add"新增按钮;NzInputModule:提供nz-input指令的输入框样式;NzPopconfirmModule:为"Delete"操作提供二次确认气泡;NzTableModule:表格本体。
数据模型是一个普通的接口对象:
interface ItemData { id: string; name: string; age: string; address: string; }模板中的核心交互段落如下:
<td> <div class="editable-cell" [hidden]="editId() === data.id" (click)="startEdit(data.id)"> {{ data.name }} </div> <input [hidden]="editId() !== data.id" type="text" nz-input [(ngModel)]="data.name" (blur)="stopEdit()" /> </td>当editId()(当前正在编辑行的 id)不等于该行 id 时,显示文本 div;点击后调用startEdit(data.id),输入框解除隐藏、接管编辑;输入框失焦(blur)时调用stopEdit()关闭编辑态。由于输入框通过ngModel直接双向绑定到data.name,失焦时数据已经写回数据行,因此不需要额外的保存动作。
二、OnPush 模式下的数据操作铁律:immutable 更新
演示文档(edit-cell.md)特别强调了一条 NG-ZORRO 的全局约束:
为了获得更好的性能,NG-ZORRO 所有组件都运行在 OnPush 模式下,这意味着对
@Input()数据的 mutate 将不会生效,请使用 immutable 方式操作数组或者对象。
这是整个示例最容易踩坑的地方,值得拆开讲透。
2.1 为什么 mutate 不生效
nz-table组件在 table.component.ts 中声明了@Input() nzData: readonly T[] = [],并在ngOnChanges中把新数据推入内部数据服务:
if (nzData) { this.nzData = this.nzData || []; this.nzTableDataService.updateListOfData(this.nzData); }OnPush 策略下,Angular 只在@Input()引用发生变化、事件触发或显式markForCheck时才检查组件。如果你直接list.push(newItem)或item.name = 'xxx',nzData的引用没有变,ngOnChanges不会被触发,表格拿到的仍是旧数组引用,界面自然不刷新。这也就是演示代码里反复使用"展开运算符创建新数组/新对象"的根本原因。
2.2 示例中的三处 immutable 操作
演示组件用signal持有数据,全部通过update回调以纯函数方式生成新值:
新增行(addRow):
addRow(): void { this.listOfData.update(listOfData => [ ...listOfData, { id: `${this.i}`, name: `Edward King ${this.i}`, age: '32', address: `London, Park Lane no. ${this.i}` } ]); this.i++; }删除行(deleteRow):
deleteRow(id: string): void { this.listOfData.update(listOfData => listOfData.filter(d => d.id !== id)); }编辑时:ngModel写入的是data.name这个对象属性,虽然这看起来像 mutate,但编辑的是从nzData派生的当前页数据行,而刷新表格整体数据依赖的是数组引用的变化;编辑态显隐则完全由editId这个 signal 驱动,不依赖数据变更检测。这正是该方案能在 OnPush 下稳定工作的关键设计。
三、编辑状态管理:signal 替代组件字段
演示组件用两个 signal 管理全部状态(见 edit-cell.ts):
readonly editId = signal<string | null>(null); readonly listOfData = signal<ItemData[]>([]);editId:当前编辑行的 id,null表示未处于编辑态。点击单元格startEdit(id)将其设为 id,失焦stopEdit()将其重置为null;listOfData:表格数据源,模板中通过listOfData()读取。
在模板中读取 signal 时(如editId() === data.id、[nzData]="listOfData()"),Angular 会自动把 signal 的读取注册为依赖,signal 值变化时精准触发对应视图更新——这比手动维护editingId字段 +ChangeDetectorRef更加简洁,也天然符合 immutable 的更新风格。
四、与 nz-table 数据管线的衔接
理解了nzData的语义,就能明白示例中另一个关键写法——为什么模板遍历的是editRowTable.data而不是listOfData():
<nz-table #editRowTable nzBordered [nzData]="listOfData()"> ... @for (data of editRowTable.data; track data.id) {nz-table组件声明了exportAs: 'nzTable'(见 table.component.ts),模板引用变量#editRowTable拿到的是组件实例,其data属性是经过分页、排序、筛选处理后的当前页数据。遍历editRowTable.data而不是原始nzData,保证在开启分页时也只渲染当前页的行。
数据流的底层实现在 table-data.service.ts:updateListOfData()把新数组推入listOfData$(BehaviorSubject),随后经过listOfDataAfterCalc$(筛选/排序)与listOfFrontEndCurrentPageData$(分页切片)两级变换,最终通过listOfCurrentPageData$暴露给表格渲染。整个链路都是响应式流,任何一步替换新数组都会触发下游重算——这从源码层面再次印证了"必须传新引用"的必要性。
五、交互细节与样式增强
5.1 删除确认
删除操作通过nz-popconfirm包裹,避免误删:
<a nz-popconfirm nzPopconfirmTitle="Sure to delete?" (nzOnConfirm)="deleteRow(data.id)">Delete</a>nzOnConfirm是确认后的回调,只有用户点击确认才执行deleteRow。
5.2 编辑态的视觉反馈
示例自带的样式让"可编辑"这一状态变得可见(edit-cell.ts 内联 styles):
.editable-cell { position: relative; padding: 5px 12px; cursor: pointer; } .editable-row:hover .editable-cell { border: 1px solid #d9d9d9; border-radius: 4px; padding: 4px 11px; }默认状态下文本单元格带手型光标;鼠标悬停行时,可编辑单元格出现 1px 边框,模拟"点击进入编辑"的暗示,与 antd 设计语言保持一致。
5.3 表格边框与 trackBy
<nz-table nzBordered>开启边框线便于单元格区分;@for ... track data.id指定唯一跟踪键,避免增删行时 Angular 复用错误的 DOM 节点。
六、进阶扩展:从单元格编辑到行编辑
同一演示集还提供了"可编辑行(Editable Rows)"方案(见 edit-row.ts 与 edit-row.md),与单元格编辑形成互补:
- 单元格编辑:聚焦单个字段,用
editId标记当前编辑行,适合字段少、改动小的场景; - 行编辑:用一个
editCache(signal 存储{ [key]: { edit: boolean; data: ItemData } })为每行缓存一份编辑副本,点击 Edit 后整行切换为输入框,Save 时listOfData.update(list => list.map(item => item.id === id ? { ...item, ...edited } : item))合并回数据源,Cancel 时从缓存恢复原始副本。
两者的共同点正是本文反复强调的 immutable 原则:编辑副本独立于数据源、保存时生成新数组、取消时丢弃副本。开发者可以参照这两个例子,把nz-input换成nz-select、nz-date-picker、nz-input-number等任意 NG-ZORRO 表单组件,自由定制自己的编辑形态(演示文档原文即注明"开发者可以参照该例子根据自己需求自由定制表格的编辑功能")。
七、完整可运行代码
以下是单元格编辑方案的完整组件代码(来自 edit-cell.ts,可直接复制到独立*.ts文件并在imports中引入对应模块后运行):
import { Component, OnInit, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { NzButtonModule } from 'ng-zorro-antd/button'; import { NzInputModule } from 'ng-zorro-antd/input'; import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm'; import { NzTableModule } from 'ng-zorro-antd/table'; interface ItemData { id: string; name: string; age: string; address: string; } @Component({ selector: 'nz-demo-table-edit-cell', imports: [FormsModule, NzButtonModule, NzInputModule, NzPopconfirmModule, NzTableModule], template: ` <button nz-button (click)="addRow()" nzType="primary">Add</button> <br /> <br /> <nz-table #editRowTable nzBordered [nzData]="listOfData()"> <thead> <tr> <th nzWidth="30%">Name</th> <th>Age</th> <th>Address</th> <th>Action</th> </tr> </thead> <tbody> @for (data of editRowTable.data; track data.id) { <tr class="editable-row"> <td> <div class="editable-cell" [hidden]="editId() === data.id" (click)="startEdit(data.id)"> {{ data.name }} </div> <input [hidden]="editId() !== data.id" type="text" nz-input [(ngModel)]="data.name" (blur)="stopEdit()" /> </td> <td>{{ data.age }}</td> <td>{{ data.address }}</td> <td> <a nz-popconfirm nzPopconfirmTitle="Sure to delete?" (nzOnConfirm)="deleteRow(data.id)">Delete</a> </td> </tr> } </tbody> </nz-table> `, styles: ` .editable-cell { position: relative; padding: 5px 12px; cursor: pointer; } .editable-row:hover .editable-cell { border: 1px solid #d9d9d9; border-radius: 4px; padding: 4px 11px; } ` }) export class NzDemoTableEditCellComponent implements OnInit { i = 0; readonly editId = signal<string | null>(null); readonly listOfData = signal<ItemData[]>([]); startEdit(id: string): void { this.editId.set(id); } stopEdit(): void { this.editId.set(null); } addRow(): void { this.listOfData.update(listOfData => [ ...listOfData, { id: `${this.i}`, name: `Edward King ${this.i}`, age: '32', address: `London, Park Lane no. ${this.i}` } ]); this.i++; } deleteRow(id: string): void { this.listOfData.update(listOfData => listOfData.filter(d => d.id !== id)); } ngOnInit(): void { this.addRow(); this.addRow(); } }八、实践要点总结
- 数据更新必须走 immutable:新增用
[...list, item],删除用filter,修改行用map生成新对象,任何push、splice、item.x = y式的 mutate 在 OnPush 下都不会刷新表格; - 编辑态与数据分离:用
editId(或editCache)这类 signal 单独驱动编辑 UI,避免把 UI 状态混进业务数据; - 遍历当前页数据:通过模板引用变量
#editRowTable访问data,获取分页/排序后的当前页数据,而不是直接遍历nzData; - 失焦即保存:
ngModel绑定 +blur关闭编辑态的组合,天然实现"所见即所得"的保存体验; - 按场景选形态:单字段小改动用单元格编辑,整行多字段用行编辑(参照 edit-row.ts),再进一步可用
nz-select、nz-date-picker等替换输入框,扩展为任意自定义编辑控件。
相关源码路径索引
- 演示说明文档:edit-cell.md、edit-row.md
- 演示实现:edit-cell.ts、edit-row.ts
- 表格组件实现:table.component.ts(
nzData输入、exportAs: 'nzTable'、ngOnChanges数据同步) - 数据管线服务:table-data.service.ts(
updateListOfData与分页/排序/筛选响应式流)
- UI组件
- 前端
【免费下载链接】ng-zorro-antd
Angular UI Component Library based on Ant Design
相关推荐
Ant Design Table 单元格编辑实战:基于 Form 与 components 定制实现可编辑表格
Ant Design Table 单元格编辑实战:基于 Form 与 components 定制实现可编辑表格 本指南围绕 ant design 仓库中 edi
前端UI组件设计系统Ant Design表格编辑功能:单元格编辑与行内编辑实现
Ant Design表格编辑功能:单元格编辑与行内编辑实现 在日常数据管理工作中,你是否经常遇到需要快速修改表格数据的场景?比如编辑订单信息、更新用户资料或调整
前端UI组件设计系统ng-zorro-antd Drawer 对象编辑实战:用遮罩禁闭、自定义页脚构建可编辑表单抽屉
ng zorro antd Drawer 对象编辑实战:用遮罩禁闭、自定义页脚构建可编辑表单抽屉 本文围绕 ng zorro antd(NG ZORRO)官方示
UI组件前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考