- UI组件
- 前端
【免费下载链接】ng-zorro-antd
Angular UI Component Library based on Ant Design
导读
在数据密集型后台界面中,“勾选多行 → 批量操作 → 清空选择”是最常见不过的交互闭环。ng-zorro-antd 的nz-table通过将第一列声明为联动选择列,即可获得与nz-checkbox完全一致的表头全选/半选、行单选能力。本篇以仓库中的官方示例 row-selection-and-operation.ts 为骨架,完整讲解从列声明、全选/半选状态推导、分页联动,到发起操作并清空选择的完整实现,并结合 th-selection.component.ts、td-addon.component.ts 等源码揭示其底层原理。读完你将能独立实现一个带批量操作、支持禁用行、分页联动与加载态的表格选择方案。
一、功能定位:第一列即选择列
ng-zorro-antd 表格的“行选择与操作”方案非常轻量——不需要额外的选择状态组件,只要在模板里把第一列声明为带[nzChecked]等属性的th/td即可。
- 表头
th增加[nzChecked]后,获得与nz-checkbox一样的功能(全选 / 取消全选),并配合[nzIndeterminate]呈现半选状态; - 表体每一行的
td同样通过[nzChecked]声明行内复选框; - 选择完毕后触发批量操作,操作完成后清空选择集合;
- 数据逻辑(选中集合的增删、全选判断、半选判断)需要开发者自行控制——这也是该方案的核心约定:
nz-table只提供 UI 联动与事件回调,不替你维护选中状态。
官方对该功能的描述与此完全一致(见 row-selection-and-operation.md):第一列是联动的选择框,增加[nzChecked]后,th获得和nz-checkbox一样的功能,选择后进行操作,完成后清空选择,数据逻辑需要自行控制。
二、完整实现示例:选择 + 操作 + 清空
官方演示组件NzDemoTableRowSelectionAndOperationComponent是这一交互的标准模板(源码见 row-selection-and-operation.ts)。以下为完整可运行实现:
import { Component, OnInit, signal } from '@angular/core'; import { NzButtonModule } from 'ng-zorro-antd/button'; import { NzTableModule } from 'ng-zorro-antd/table'; export interface Data { id: number; name: string; age: number; address: string; disabled: boolean; } @Component({ selector: 'nz-demo-table-row-selection-and-operation', imports: [NzButtonModule, NzTableModule], template: ` <div class="send-request"> <button nz-button nzType="primary" [disabled]="setOfCheckedId().size === 0" [nzLoading]="loading()" (click)="sendRequest()" > Send Request </button> <span>Selected {{ setOfCheckedId().size }} items</span> </div> <nz-table #rowSelectionTable nzShowPagination nzShowSizeChanger [nzData]="listOfData()" (nzCurrentPageDataChange)="onCurrentPageDataChange($event)" > <thead> <tr> <th [nzChecked]="checked()" [nzIndeterminate]="indeterminate()" nzLabel="Select all" (nzCheckedChange)="onAllChecked($event)" ></th> <th>Name</th> <th>Age</th> <th>Address</th> </tr> </thead> <tbody> @for (data of rowSelectionTable.data; track data.id) { <tr> <td [nzChecked]="setOfCheckedId().has(data.id)" [nzDisabled]="data.disabled" [nzLabel]="data.name" (nzCheckedChange)="onItemChecked(data.id, $event)" ></td> <td>{{ data.name }}</td> <td>{{ data.age }}</td> <td>{{ data.address }}</td> </tr> } </tbody> </nz-table> `, styles: ` .send-request { margin-bottom: 16px; } .send-request span { margin-inline-start: 8px; } ` }) export class NzDemoTableRowSelectionAndOperationComponent implements OnInit { readonly checked = signal(false); readonly loading = signal(false); readonly indeterminate = signal(false); readonly listOfData = signal<readonly Data[]>([]); readonly listOfCurrentPageData = signal<readonly Data[]>([]); readonly setOfCheckedId = signal(new Set<number>()); updateCheckedSet(id: number, checked: boolean): void { this.setOfCheckedId.update(setOfCheckedId => { const next = new Set(setOfCheckedId); if (checked) { next.add(id); } else { next.delete(id); } return next; }); } onCurrentPageDataChange(listOfCurrentPageData: readonly Data[]): void { this.listOfCurrentPageData.set(listOfCurrentPageData); this.refreshCheckedStatus(); } refreshCheckedStatus(): void { const listOfEnabledData = this.listOfCurrentPageData().filter(({ disabled }) => !disabled); const checked = listOfEnabledData.every(({ id }) => this.setOfCheckedId().has(id)); this.checked.set(checked); this.indeterminate.set(listOfEnabledData.some(({ id }) => this.setOfCheckedId().has(id)) && !checked); } onItemChecked(id: number, checked: boolean): void { this.updateCheckedSet(id, checked); this.refreshCheckedStatus(); } onAllChecked(checked: boolean): void { this.listOfCurrentPageData() .filter(({ disabled }) => !disabled) .forEach(({ id }) => this.updateCheckedSet(id, checked)); this.refreshCheckedStatus(); } sendRequest(): void { this.loading.set(true); const requestData = this.listOfData().filter(data => this.setOfCheckedId().has(data.id)); console.log(requestData); setTimeout(() => { this.setOfCheckedId.set(new Set<number>()); this.refreshCheckedStatus(); this.loading.set(false); }, 1000); } ngOnInit(): void { this.listOfData.set( new Array(100).fill(0).map((_, index) => ({ id: index, name: `Edward King ${index}`, age: 32, address: `London, Park Lane no. ${index}`, disabled: index % 2 === 0 })) ); } }该示例使用 Angular 最新的signal状态管理与@for控制流语法,并只依赖NzButtonModule与NzTableModule两个模块,适合作为功能的最小可运行起点。
三、数据状态自管理:五段逻辑拆解
正如文档强调的“数据逻辑需要自行控制”,示例中所有选中状态都由组件内部状态驱动。可拆解为五个关键部分:
1. 选中集合:Set<number>作为唯一数据源
setOfCheckedId是一个signal(new Set<number>()),只存选中行的id。之所以选用Set而非boolean[],是因为它天然支持跨页聚合(选中的行可能分布在多页),且查询has(id)是 O(1) 操作。updateCheckedSet采用不可变更新:复制出一个新Set再增删,避免直接修改原对象,保证signal变更可被侦测。
2. 当前页数据缓存:listOfCurrentPageData
通过表格的(nzCurrentPageDataChange)事件,把“当前页面展示的数据”缓存到listOfCurrentPageData。这是全选/半选判断的数据基础——分页场景下,表头“全选”语义是“选中当前页所有未禁用行”,而非全部 100 条数据。从源码可见,该事件由 table.component.ts 在数据变化时触发(@Output() readonly nzCurrentPageDataChange = new EventEmitter<readonly T[]>()),nzShowPagination(默认true)与nzShowSizeChanger(默认false,可在全局配置中开启)共同决定分页展示行为。
3. 状态刷新:refreshCheckedStatus推导全选与半选
const listOfEnabledData = this.listOfCurrentPageData().filter(({ disabled }) => !disabled); const checked = listOfEnabledData.every(({ id }) => this.setOfCheckedId().has(id)); this.checked.set(checked); this.indeterminate.set(listOfEnabledData.some(({ id }) => this.setOfCheckedId().has(id)) && !checked);全选条件:当前页所有未禁用行都在选中集合中;半选条件:至少有一个未禁用行被选中但并非全部选中。先过滤disabled行,保证禁用行既不会被every卡住,也不会触发半选误判。
4. 行级切换:onItemChecked
勾选/取消某一行时,更新Set后立即刷新表头状态。表头状态是从选中集合推导出来的派生值,而非独立维护的布尔量,这就是“联动”的本质——任何行变化都会自动反映到表头,反之表头操作也会批量写入集合。
5. 全选:onAllChecked
this.listOfCurrentPageData() .filter(({ disabled }) => !disabled) .forEach(({ id }) => this.updateCheckedSet(id, checked));表头勾选事件携带的checked布尔值,被应用到当前页所有未禁用行的id上,然后同样刷新状态。注意此处是在事件回调中逐条updateCheckedSet,虽然多次触发signal.update,但语义清晰、逻辑一致。
四、操作与清空:sendRequest 的完整闭环
批量操作通过sendRequest()实现,流程如下:
- 禁用与加载态:按钮的
[disabled]="setOfCheckedId().size === 0"保证无选中时不可点击;[nzLoading]="loading()"在请求期间展示加载转圈; - 收集选中数据:
this.listOfData().filter(data => this.setOfCheckedId().has(data.id))从全量数据(而非当前页)中提取被选中的行,因为选中集合可能跨页; - 模拟请求:示例用
setTimeout模拟 1 秒异步请求,console.log(requestData)输出待提交的数据,实际项目中此处替换为 HTTP 调用; - 清空选择:请求结束后
this.setOfCheckedId.set(new Set<number>())重置选中集合,再调用refreshCheckedStatus()让表头全选框、半选框同步复位,最后loading置回false。
这段“收集 → 请求 → 清空 → 复位”的流程正是文档所说“选择后进行操作,完成后清空选择”的标准落地方式。
五、源码级原理:th与td的选择能力从何而来
“给th/td加[nzChecked]就能得到 checkbox”这一魔法,本质是属性选择器组件。ng-zorro-antd 用两个独立组件分别接管了表头与单元格:
1. 表头选择组件NzThSelectionComponent
定义在 th-selection.component.ts,选择器为:
th[nzSelections], th[nzChecked], th[nzShowCheckbox], th[nzShowRowSelection]只要th上出现任一属性,该组件即生效。它内部渲染nz-table-selection,并对外暴露:
| 输入/输出 | 类型 | 说明 |
|---|---|---|
[nzChecked] | boolean | 全选框是否选中(默认false) |
[nzIndeterminate] | boolean | 全选框半选状态(默认false) |
[nzDisabled] | boolean | 全选框禁用(默认false) |
[nzLabel] | string \| null | 全选框的aria-label无障碍标签(默认null) |
[nzSelections] | Array<{ text, onSelect }> | 表头下拉菜单选项及回调(默认[]) |
[nzShowCheckbox] | boolean | 是否显示 checkbox(默认false) |
[nzShowRowSelection] | boolean | 是否显示下拉选择菜单(默认false) |
(nzCheckedChange) | EventEmitter<boolean> | 选中状态变化回调 |
源码中值得注意的两处自动行为(ngOnChanges):首次传入nzSelections且未显式设置过nzShowExpand时,自动开启nzShowRowSelection;首次传入nzChecked且未显式设置过nzShowCheckbox时,自动开启nzShowCheckbox。也就是说,只要写[nzChecked]="...",复选框就会被渲染出来,无需额外声明nzShowCheckbox,这正是“增加[nzChecked]后th获得 checkbox 功能”的机制来源。onCheckedChange回调会把内部状态同步回nzChecked并向上emit。
2. 单元格选择组件NzTdAddOnComponent
定义在 td-addon.component.ts,选择器:
td[nzChecked], td[nzDisabled], td[nzIndeterminate], td[nzIndentSize], td[nzExpand], td[nzShowExpand], td[nzShowCheckbox]当td带nzChecked等属性时,组件渲染一个nz-checkbox复选框,并同时处理展开图标与缩进(nzExpand/nzIndentSize),因此选择列与展开列可以共存于同一单元格。其对外 API 与th版基本对称(nzChecked、nzDisabled、nzIndeterminate、nzLabel、nzCheckedChange),且同样有“首次传入nzChecked即自动开启nzShowCheckbox”的默认行为。
3. 内部选择组件NzTableSelectionComponent
定义在 selection.component.ts,负责真正渲染复选框与“下拉全选菜单”(nzSelections配置的text/onSelect选项,可注入如“全部选中”“全部取消”“奇数行”等批量动作)。复选框通过[ngModel]="checked"与(ngModelChange)实现受控,半选态映射到nz-checkbox的[nzIndeterminate]。
4. 表格容器侧配合
nz-table 提供nzData数据输入、nzShowPagination(默认true)、nzShowSizeChanger(默认false)等分页开关,并通过nzCurrentPageDataChange通知当前页数据变化;#rowSelectionTable模板引用变量上的.data属性则是当前页渲染数据的直接来源,演示模板即用rowSelectionTable.data驱动@for渲染。测试用例 td.spec.ts 验证了ant-table-selection-column样式类与半选态(ant-checkbox-indeterminate)的实际生效,可作为底层行为的佐证。
六、API 速查与进阶指引
完整的th/td选择相关 API 说明可见官方文档 index.zh-CN.md,关键参数如下:
- 表格级:
[nzData]数据源、[nzShowPagination]是否显示分页(默认true)、[nzShowSizeChanger]是否可改每页条数(默认false,可经全局配置开启)、(nzCurrentPageDataChange)当前页数据变化回调; - 表头
th级:[nzShowCheckbox]是否添加 checkbox、[nzChecked]是否选中(可双向绑定)、(nzCheckedChange)选中回调、[nzSelections]下拉选择项Array<{ text, onSelect }>、[nzIndeterminate]半选态; - 单元格
td级:[nzShowCheckbox]、[nzChecked]、(nzCheckedChange),另支持[nzDisabled]禁用单行复选框。
更多进阶形态可参考同目录下的 row-selection-custom.ts(自定义全选下拉菜单),以及table组件的expand、edit等系列 demo(见 components/table/demo),便于将选择能力与展开行、行编辑组合使用。
七、小结与三个易错点
回顾整个实现,“自行控制数据逻辑”是贯穿始终的设计前提,落地时需特别注意三点:
- 全选只对当前页生效:表头全选基于
listOfCurrentPageData计算,跨页选中必须依赖Set的持久性——翻页后再回到原页,行仍保持选中; - 禁用行不参与全选/半选判断:
refreshCheckedStatus与onAllChecked都先filter(({ disabled }) => !disabled),否则禁用行会破坏every/some的语义; - 操作完成后务必清空并复位:
setOfCheckedId.set(new Set())之后还需调用refreshCheckedStatus(),否则表头全选框会残留勾选状态,破坏下一次选择的体验。
- UI组件
- 前端
【免费下载链接】ng-zorro-antd
Angular UI Component Library based on Ant Design
相关推荐
终极指南:Lightbug HTTP的Cookie管理与会话控制
终极指南:Lightbug HTTP的Cookie管理与会话控制 Lightbug HTTP是一个简单快速的Mojo HTTP框架,为开发者提供了高效的Cook
UI组件前端设计系统ng-zorro-antd TreeView 带选择框的树:checkbox 级联勾选完整实战指南
ng zorro antd TreeView 带选择框的树:checkbox 级联勾选完整实战指南 带选择框的树(Tree with checkboxes)是数
UI组件前端ng-zorro-antd 表单联动实战:基于 `setValue` 与 `valueChanges` 的控件动态赋值
ng zorro antd 表单联动实战:基于 setValue 与 valueChanges 的控件动态赋值 导读 本文以 ng zorro antd(Ang
UI组件前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考