 行选择(Row Selection)完整指南:从状态管理到 Shift 范围选择)
前端UI组件【免费下载链接】table Headless UI for building powerful tables datagrids for TS/JS - React-Table, Vue-Table, Solid-Table, Svelte-Table项目地址https://gitcode.com/gh_mirrors/ta/table点击查看免费下载导读行选择Row Selection是数据表格最常用的交互能力之一。本文以 TanStack Table v9 的 Octane 适配版tanstack/octane-table为核心系统讲解如何通过rowSelectionFeature启用行选择、读写选择状态、用外部 Atom 管理状态、配置条件选择与单选/子行选择并深入剖析 Shift 范围选择Shift Range Selection的交互细节。读完本文你将掌握在 Octane 应用中实现带复选框、全选、单选与范围选择的完整方案并理解其背后的源码实现与测试验证。快速上手启用行选择行选择功能由rowSelectionFeature提供。将它传入tableFeatures()组合进 feature 集合再交给useTable行选择相关的全部 API 即自动生效。import { useTable, tableFeatures, rowSelectionFeature, } from tanstack/octane-table const features tableFeatures({ rowSelectionFeature }) const table useTable({ features, columns, data, })从源码看rowSelectionFeature是一个标准的TableFeature对象它负责四件事见 rowSelectionFeature.ts初始化状态getInitialState为表格注册rowSelection状态切片默认值为{}空对象提供默认选项getDefaultTableOptions一次性注册enableRowSelection、enableMultiRowSelection、enableSubRowSelection、enableRowRangeSelection以及isRowRangeSelectionEvent五个选项的默认值扩展 Row 原型向row实例挂载getIsSelected、getCanSelect、getToggleSelectedHandler、toggleSelected等 API扩展 Table 实例向table实例挂载getSelectedRowModel、toggleAllRowsSelected、getIsAllRowsSelected等 API。这也解释了文档中Adding the row selection feature enables the related APIs的含义所有行选择 API 都是该 feature 在实例构造阶段统一挂载的。需要说明的是octane-table包本身对table-core是逐字转出verbatim re-export行选择的全部实现位于核心包 rowSelectionFeature.ts 及其工具函数 rowSelectionFeature.utils.ts。仓库提供了完整的可运行示例examples/octane/row-selection该示例同时叠加了分页、列过滤与全局过滤并在 main.tsrx 中演示了多 feature 的组合方式。读取行选择状态表格实例会自动托管rowSelection状态。Octane 版v9提供了四类读取 APIAPI作用table.state.rowSelection响应式读取行选择状态由useTable的 selector 订阅getSelectedRowModel()返回所有被选中的行getFilteredSelectedRowModel()过滤后仍被选中的行getGroupedSelectedRowModel()分组、排序后仍被选中的行console.log(table.state.rowSelection) // 行选择状态 - { 1: true, 2: false, ... } console.log(table.getSelectedRowModel().rows) // 全部客户端选中行 console.log(table.getFilteredSelectedRowModel().rows) // 过滤后的选中行 console.log(table.getGroupedSelectedRowModel().rows) // 分组后的选中行在事件回调等非渲染代码中还可以用table.atoms.rowSelection.get()读取当前快照。这种读取不会订阅组件到未来的变更因此在渲染位置请优先使用table.state.rowSelection或table.Subscribe。在源码层面这三个 RowModel API 都带有memoDeps缓存依赖见 rowSelectionFeature.tsgetSelectedRowModel依赖table.atoms.rowSelection与getCoreRowModel()getFilteredSelectedRowModel依赖选择状态与getFilteredRowModel()getGroupedSelectedRowModel依赖选择状态与getSortedRowModel()。也就是说选中状态变化或对应 RowModel 变化时缓存会自动失效并重算这正是过滤后选中行等语义能保持准确的底层机制。[!NOTE] 若使用了manualPagination请留意getSelectedRowModel只能基于传入的data生成行因此只会返回当前页的选中行但rowSelection状态本身可以安全地保存不在data中的行 id。管理行选择状态推荐外部 Atomv9 方式如果需要在应用其他部分方便地读取选中行 id例如用于发起 API 调用推荐通过atoms表选项把rowSelection状态切片交给外部 Atom 持有。Atom 能保留细粒度订阅应用任意位置都能读取选择值而不会迫使拥有表格的组件整体重渲染。import { useCreateAtom, useSelector } from tanstack/octane-store import { useTable, tableFeatures, rowSelectionFeature, type RowSelectionState, } from tanstack/octane-table const features tableFeatures({ rowSelectionFeature }) const rowSelectionAtom useCreateAtomRowSelectionState({}) // 在任意需要的地方订阅该 Atom const rowSelection useSelector(rowSelectionAtom) const table useTable({ features, // ... atoms: { rowSelection: rowSelectionAtom, // 选择 API 将更新该 Atom }, })关于atoms的优先级与行为tanstack/octane-table的 table-state 技能说明 明确外部 Atom 是直接同步所有者优先级高于options.state表格 API 的写入会直接到达该 Atom。同时建议外部 Atom 与on[State]Change回调不要成对使用二者是两种互斥的所有权模式。更完整的对比见 Table State Guide。兼容受控 state onRowSelectionChangev8 风格v8 风格的state.rowSelectiononRowSelectionChange模式仍然受支持。它对简单集成或迁移 v8 代码较为方便但细粒度不如外部 Atom。const [rowSelection, setRowSelection] useStateRowSelectionState({}) const table useTable({ features, // ... onRowSelectionChange: setRowSelection, state: { rowSelection, }, })示例项目 main.tsrx 同时注释展示了两种可切换的写法默认使用rowSelectionAtom并保留了state.rowSelectiononRowSelectionChange以及initialState.rowSelection首屏预选的备选方案方便对照。使用更有意义的行 id行选择状态以row id 为键。默认情况下每个行的 row id 就是row.index即 0、1、2…这在数据排序、过滤或刷新后会变得不可靠。使用行选择功能时几乎总是应该通过getRowId指定一个稳定的唯一标识const table useTable({ features, // ... getRowId: (row) row.uuid, // 用数据库中的 uuid 作为 row id })设置后选择状态形如{ 13e79140-62a8-4f9c-b087-5da737903b76: true, f3e2a5c0-5b7a-4d8a-9a5c-9c9b8a8e5f7e: false }而不是这样{ 0: true, 1: false }示例中即采用getRowId: (row) row.id数据由 makeData.ts 生成id 为 uuid。e2e 测试 smoke.spec.ts 也专门注释Row ids are uuids, so only the number of selected keys is assertable佐证了行 id 是 uuid 这一事实。条件启用与禁用行选择行选择默认对所有行启用。enableRowSelection选项接受布尔值或行级函数可精确控制const table useTable({ // ... enableRowSelection: (row) row.original.age 18, // 仅允许成年人行被选中 })源码实现见 rowSelectionFeature.utils.ts非常直接export function row_getCanSelect(row) { const options row.table.options if (typeof options.enableRowSelection function) { return options.enableRowSelection(row) } return options.enableRowSelection ?? true }即传入函数时逐行求值否则返回布尔默认值true。UI 侧请用row.getCanSelect()决定复选框是否禁用disabled属性与源码中的判断保持一致。单选模式默认允许多选。设置enableMultiRowSelection为false可强制单选适合用单选按钮代替复选框的场景也可以传函数对某行的子行条件禁用多选const table useTable({ // ... enableMultiRowSelection: false, // 同一时刻仅允许一行被选中 // enableMultiRowSelection: row row.original.age 18, // 对成年人行启用单选 })子行Sub-Row选择默认情况下选中父行会连带选中其全部子行。enableSubRowSelection可禁用该行为const table useTable({ // ... enableSubRowSelection: false, // 禁用子行联动选择 // enableSubRowSelection: row row.original.age 18, // 对成年人行禁用子行选择 })子行选择对全选 API 同样生效当父行屏蔽子行选择时table.toggleAllRowsSelected()与table.toggleAllPageRowsSelected()会跳过该父行的后代getIsAllRowsSelected()与getIsAllPageRowsSelected()在判定是否全选时也会忽略这些后代。选中父行会把父行 id 与其所有可选的子行 id一起写入选择状态。默认情况下之后取消子行不会移除父行 id——因为部分表格把状态中的 id 视为字面选中项。如果需要可向 toggle API 传入deselectParents选项在取消选中时清理祖先 idrow.getToggleSelectedHandler({ deselectParents: true }) // 或 row.toggleSelected(false, { deselectParents: true })源码中row_toggleSelected的处理逻辑见 rowSelectionFeature.utils.ts印证了这一行为写入时按(opts?.selectChildren ?? true) row_getCanMultiSelect(row)决定是否递归选中后代当!value opts?.deselectParents为真时调用pruneAncestorRowIds清除祖先 id。Shift 范围选择row.getToggleSelectedHandler()默认支持 Shift 范围选择一次普通的可选中行交互会建立锚点anchor随后按住 Shift 点击另一行会选中/取消选中两者之间的闭区间区间内所有行的选中状态由被点击复选框的结果值决定被点击的端点成为下一次 Shift 交互的锚点。事件识别逻辑在 feature 默认选项中见 rowSelectionFeature.ts只要事件暴露event.shiftKey或event.nativeEvent.shiftKey即判定为范围选择。这也是官方建议Octane 复选框处理器用onClick而不是onChange的原因——onClick才能把带shiftKey修饰键的点击事件完整传给处理器。可以整体禁用范围选择或替换事件判定例如改用平台修饰键 Meta/Ctrlconst table useTable({ // ... enableRowRangeSelection: false, // 例如用平台修饰键代替 Shift // isRowRangeSelectionEvent: event // Boolean((event as { metaKey?: boolean }).metaKey), })范围选择遵循表格当前的逻辑显示顺序包括过滤、分组、排序与展开。客户端分页下范围可以跨页因为使用分页前的完整顺序手动/服务端分页下只有当前data中已加载的行能参与范围。当子行选择启用时范围内遇到的父行默认会递归切换其可选择的子行。若只想让显示顺序区间中明确存在的行变化传入selectChildren: falseconst handler row.getToggleSelectedHandler({ selectChildren: false, })交互锚点的生命周期规则值得注意只要锚点行 id 仍在显示顺序中它会跨排序、过滤、分组、展开、分页与数据更新保持若过滤或数据替换移除了锚点下一次 Shift 交互退化为普通行切换并建立新锚点resetRowSelection、任一全选 API、table.reset()都会清除锚点直接调用row.toggleSelected()、table.setRowSelection()或外部受控状态变更不会建立或移动锚点。源码中锚点由表格实例数据_lastSelectedRowId保存feature 在initTableInstanceData与resetTableInstanceData中将其初始化为null见 rowSelectionFeature.ts与reset 会清除锚点的文档描述完全对应。e2e 测试 smoke.spec.ts 验证了范围选择的闭区间语义选中第 1 行后Shift 点击第 5 行期望选择数为 5两端点都包含且区间外的第 0、6 行不被选中。渲染行选择 UITanStack Table 不规定 UI 形态——复选框、单选按钮或整行点击均可。表格实例提供了一系列 API 辅助渲染。连接复选框输入以下 handler 可直接绑定到复选框输入它们会自动调用内部 API 更新状态并触发重渲染row.getToggleSelectedHandler()切换单行选中table.getToggleAllRowsSelectedHandler()切换全部行跨页table.getToggleAllPageRowsSelectedHandler()切换当前页全部行。若需要更细粒度的控制也可以直接使用row.toggleSelected()、table.toggleAllRowsSelected()或像操作任何状态更新器一样调用table.setRowSelection()。这些 handler 本质上只是便捷封装。const columns [ { id: select-col, header: ({ table }) ( Checkbox checked{table.getIsAllRowsSelected()} indeterminate{table.getIsSomeRowsSelected()} onChange{table.getToggleAllRowsSelectedHandler()} // 或 getToggleAllPageRowsSelectedHandler / ), cell: ({ row }) ( Checkbox checked{ row.getIsSelected() || (row.getCanSelectSubRows() row.getIsAllSubRowsSelected()) } disabled{!row.getCanSelect()} indeterminate{row.getIsSomeSelected()} onClick{row.getToggleSelectedHandler()} / ), }, // ...更多列定义... ][!NOTE]getCanSelectSubRows()与getIsAllSubRowsSelected()子句只对含子行的表格有意义扁平数据下仅row.getIsSelected()就足够。完整的子行模式包括用deselectParents清理废弃父 id可参考 expanding 示例。连接整行点击若想要更简洁的交互把点击事件直接绑到tr上即可row.getToggleSelectedHandler()同样适用tbody {table.getRowModel().rows.map((row) { return ( tr key{row.id} className{row.getIsSelected() ? selected : null} onClick{row.getToggleSelectedHandler()} {row.getVisibleCells().map((cell) { return td key{cell.id}{/* */}/td })} /tr ) })} /tbody半选indeterminate状态行选择的部分选中通过getIsSomeRowsSelected/getIsSomePageRowsSelected/row.getIsSomeSelected表达。示例项目 main.tsrx 中的IndeterminateCheckbox组件演示了完整做法在useEffect中根据indeterminate布尔值设置原生ref.current.indeterminate。e2e 测试也对半选状态做了断言见 smoke.spec.ts选中 1000 行中的 1 行时表头与表尾全选框均为indeterminatetrue且未勾选。与分页、过滤的联动语义示例与测试共同验证了行选择与分页、过滤联动时的重要语义表头全选框绑定getIsAllRowsSelected()跨所有页测试点击后期望选中 1000 行翻页后新页面行依然处于选中状态因为选择以行 id 为键见 smoke.spec.ts表尾全选框绑定getIsAllPageRowsSelected()仅当前页测试中期望选中 10 行且表头保持半选选择汇总Object.keys(table.state.rowSelection).length统计所有被选中行 id 的数量其分母使用getPreFilteredRowModel()因此全局过滤不会改变分母见 smoke.spec.ts。小结在 TanStack Table v9Octane 版中行选择由rowSelectionFeature一个开关式启用随后即可获得状态读取、条件选择、单选、子行联动、Shift 范围选择与全套 UI handler。选择状态的托管推荐使用外部 Atom 以获得细粒度订阅行 id 务必通过getRowId指定为业务唯一键子行场景注意selectChildren与deselectParents两个选项的配合。上述所有行为均有核心源码rowSelectionFeature.ts、rowSelectionFeature.utils.ts、可运行示例examples/octane/row-selection与 Playwright e2e 测试smoke.spec.ts三重印证可直接作为落地实现的参照。赞分享前端UI组件【免费下载链接】table Headless UI for building powerful tables datagrids for TS/JS - React-Table, Vue-Table, Solid-Table, Svelte-Table项目地址https://gitcode.com/gh_mirrors/ta/table点击查看免费下载相关推荐TanStack Table Preact 行选择Row Selection完整指南从状态管理到 Shift 区间多选TanStack Table Preact 行选择Row Selection完整指南从状态管理到 Shift 区间多选 本文以 TanStack Tabl前端UI组件Puppeteer ElementHandle.type() 输入方法详解聚焦元素的模拟键盘输入机制与延迟控制Puppeteer ElementHandle.type 输入方法详解聚焦元素的模拟键盘输入机制与延迟控制 ElementHandle.type 是 Pupp前端UI组件Angular TanStack Table 行选择Row Selection完整指南从状态管理到多选、范围选择与父子行联动Angular TanStack Table 行选择Row Selection完整指南从状态管理到多选、范围选择与父子行联动 导读 行选择Row Sel前端UI组件创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考