尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

TanStack Table v9 Angular 自定义功能开发指南:利用 TableFeature 与 tableFeatures() 扩展表格能力

TanStack Table v9 Angular 自定义功能开发指南:利用 TableFeature 与 tableFeatures() 扩展表格能力 TanStack Table v9 Angular 自定义功能开发指南利用 TableFeature 与 tableFeatures() 扩展表格能力【免费下载链接】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本指南以 Angular 生态下的 TanStack Tabletanstack/angular-table为例系统讲解如何借助 v9 的TableFeature特性机制与tableFeatures()选项为表格实例添加自定义功能Custom Features。读完本文你将掌握特性对象完整的生命周期钩子、类型安全的声明合并技巧并能够从零实现一个可打包、可树摇tree-shaking的自定义插件例如本文完整实现的表格密度切换density功能。为什么 TanStack Table 刻意保持精简排序、过滤、分页等核心功能已经内置于 TanStack Table 中。开源社区长期向项目提交各种新功能建议其中不乏设计良好的 PR。但 TanStack Table 团队坚持保持库的精简不把大多数场景用不到的代码塞进核心库。即便某个 PR 确实解决了真实问题也不一定适合进入核心库——这会让核心库解决 90% 需求、但还差一点控制力的开发者感到挫败。为此TanStack Table 从 v7 起就构建了高度可扩展的架构无论通过哪个框架适配器React 的useReactTable、Angular 的injectTable等创建出的table实例本质上都是一个普通 JavaScript 对象可以随时附加额外的属性或 API。在 v9 之前开发者主要通过组合composition的方式在框架适配器的创建函数外层包一层自定义封装例如社区中流行的 Material React Table 就是围绕表格创建函数做自定义包装。v9 则引入了更规范的扩展入口——features选项通过tableFeatures()构造用来声明当前表格使用哪些特性。这样一来按需打包tree-shaking只为表格实际声明的特性打包代码未使用的内置特性不会进入产物统一扩展模型自定义特性与内置特性走完全相同的注册、初始化与 API 注入管线类型安全通过声明合并让 TypeScript 精确推导特性带来的状态、选项与 API。v9 中特性是显式选择的opt-in。请使用tableFeatures({ ... })声明表格使用的特性包括自定义特性。特性Feature机制的工作原理TanStack Table 的源码组织方式相当直观每个特性的全部代码被拆分到独立的对象/文件中内含创建初始状态、默认表格与列选项的实例化方法以及挂载到table、header、column、row、cell实例上的 API 方法。所有特性对象的功能外形都由导出的TableFeature类型TypeScript 接口描述核心定义位于 packages/table-core/src/types/TableFeatures.ts 中导入、并由TableFeature接口约定完整接口见文档 custom-features.md。export interface TableFeature { assignCellPrototype?: TFeatures extends TableFeatures, TData extends RowData( prototype: Recordstring, any, table: Table_InternalTFeatures, TData, ) void assignColumnPrototype?: TFeatures extends TableFeatures, TData extends RowData( prototype: Recordstring, any, table: Table_InternalTFeatures, TData, ) void assignHeaderPrototype?: TFeatures extends TableFeatures, TData extends RowData( prototype: Recordstring, any, table: Table_InternalTFeatures, TData, ) void assignRowPrototype?: TFeatures extends TableFeatures, TData extends RowData( prototype: Recordstring, any, table: Table_InternalTFeatures, TData, ) void constructTableAPIs?: TFeatures extends TableFeatures, TData extends RowData( table: Table_InternalTFeatures, TData, ) void initTableInstanceData?: TFeatures extends TableFeatures, TData extends RowData( table: Table_InternalTFeatures, TData, ) void getDefaultColumnDef?: TFeatures extends TableFeatures, TData extends RowData, TValue extends CellData CellData, () ColumnDefBase_AllTFeatures, TData, TValue getDefaultTableOptions?: TFeatures extends TableFeatures, TData extends RowData( table: Table_InternalTFeatures, TData, ) PartialTableOptions_AllTFeatures, TData getInitialState?: (initialState: PartialTableState_All) TableState_All initCellInstanceData?: TFeatures extends TableFeatures, TData extends RowData, TValue extends CellData CellData, (cell: CellTFeatures, TData, TValue) void initColumnInstanceData?: TFeatures extends TableFeatures, TData extends RowData, TValue extends CellData CellData, (column: ColumnTFeatures, TData, TValue) void initHeaderGroupInstanceData?: TFeatures extends TableFeatures, TData extends RowData, (headerGroup: HeaderGroupTFeatures, TData) void initHeaderInstanceData?: TFeatures extends TableFeatures, TData extends RowData, TValue extends CellData CellData, (header: HeaderTFeatures, TData, TValue) void initRowInstanceData?: TFeatures extends TableFeatures, TData extends RowData( row: RowTFeatures, TData, ) void resetTableInstanceData?: TFeatures extends TableFeatures, TData extends RowData( table: Table_InternalTFeatures, TData, ) void }接口中的每个方法都是可选的特性只需要实现自己需要的那一部分。下面逐一拆解这些方法的职责与调用时机。默认选项与初始状态getDefaultTableOptions / getDefaultColumnDef / getInitialState这三个方法共同决定特性的默认行为在表格创建早期执行getDefaultTableOptions负责设置该特性的默认表格选项。例如 column-resizing 特性 通过它把默认的columnResizeMode选项设为onEnd表示默认只在拖拽结束时才应用列宽。getDefaultColumnDef负责设置该特性的默认列选项。例如 row-sorting 特性 通过它把默认的sortUndefined列选项设为1排序时把undefined值视为最大值处理。getInitialState负责设置该特性的默认状态。例如 row-pagination 特性 通过它把默认的pageSize状态设为10、pageIndex设为0。从源码实现看状态更新统一走makeStateUpdater工具定义于 packages/table-core/src/utils.ts 的makeStateUpdater导出它优先把更新写入table.options.atoms中的外部原子外部受控状态否则回落到table.baseAtoms中的内部状态原子并通过functionalUpdate同时支持直接传值与传更新函数两种写法见 utils.ts#L11-L15 的functionalUpdate若 updater 是函数则以旧值调用之否则原样返回。实例 API 构造器initTableInstanceData / resetTableInstanceData / constructTableAPIsinitTableInstanceData用于存放可变、非响应式且属于单个表格实例的数据例如交互锚点或命令式缓存。它在表格选项、状态原子与 store 创建完成后运行一次。所有特性按注册顺序单趟处理每个特性的初始化钩子恰好先于其constructTableAPIs钩子执行因此后注册的特性可以依赖先前特性已经就绪的数据与 API。resetTableInstanceData用于在table.reset()执行时清空上述临时数据。重置钩子在内部持有的表格状态原子恢复为table.initialState之后运行它不会重置表格状态切片或外部受控状态且table.reset()不会重新执行initTableInstanceData。constructTableAPIs专责向table实例添加方法。它运行在所有特性持有的表格实例数据初始化之后。例如 row-selection 特性 通过它添加toggleAllRowsSelected、getIsAllRowsSelected、getIsSomeRowsSelected等大量实例 API——当你调用table.toggleAllRowsSelected()时调用的正是由该特性constructTableAPIs注入的方法。最佳实践是API 的分配放在constructTableAPIs初始化与重置钩子只负责特性自己拥有的数据。API 注入底层依赖 utils.ts#L544 的assignTableAPIs遍历传入的 API 对象对每个条目解析出函数名无memoDeps时直接赋值到表格实例有memoDeps时则用tableMemo包裹成记忆化版本表格是单例因此方法直接分配而非走原型。共享原型扩展与实例数据assignXxxPrototype / initXxxInstanceData这些钩子成对出现覆盖header、headerGroup、column、row、cell五种实例类型assignHeaderPrototypeinitHeaderInstanceData前者向共享的header原型添加方法。例如 column-sizing 特性 添加getStart因此调用header.getStart()实际调用的是该特性注入的方法。后者用于存放无法放到共享原型上的、每个 header 独立的实例数据或缓存它在 header 构建期间、子表头填充之前、以及 header 关联到 header group 之前运行。由于每次 header group 重算都会重建 header该钩子在每次重建时都会重新执行。initHeaderGroupInstanceDataheader group 唯一的每实例扩展点header group没有共享原型。它在 header group 的depth、id与完整填充的headers数组分配完毕之后运行并在 header group 重建时重新执行。assignColumnPrototypeinitColumnInstanceData前者向共享的column原型添加方法。例如 row-sorting 特性 添加getNextSortingOrder、toggleSorting等列级 API。后者用于存放每列独立的实例数据或缓存例如 row-aggregation 特性 用它建立按列的聚合缓存。assignRowPrototypeinitRowInstanceData前者向共享的row原型添加方法。例如 row-selection 特性 添加toggleSelected、getIsSelected等行级 API。assignCellPrototypeinitCellInstanceData前者向共享的cell原型添加方法。例如 Column Groupingcolumn-grouping 特性添加getIsGrouped、getIsPlaceholderAggregation 添加getIsAggregated。后者用于存放每格独立的实例数据或缓存。Cell 按行/列组合在首次访问时惰性构造并缓存因此该钩子对每个 cell 实例恰好执行一次。实战为 Angular 表格实现一个 density 自定义特性假设我们要为表格实例添加一个允许用户切换密度单元格内边距的特性。完整实现可直接查看仓库中的 custom-plugin 示例源码位于src/app/density/density-feature.ts配套的 Angular 组件与模板在src/app/下下面按五步深入拆解。Step 1搭建 TypeScript 类型为了让自定义特性获得与内置特性一致的完整类型安全先为它定义表格选项、状态与实例 API 的类型。这些命名遵循 TanStack Table 内部的命名惯例TableState_*、TableOptions_*、Table_*你可以自由改名它们目前只属于你的特性尚未注册进类型系统// define types for our new features custom state export type DensityState sm | md | lg export interface TableState_Density { density: DensityState } // define types for our new features table options export interface TableOptions_Density { enableDensity?: boolean onDensityChange?: OnChangeFnDensityState } // Define types for our new features table APIs export interface Table_Density { setDensity: (updater: UpdaterDensityState) void toggleDensity: (value?: DensityState) void }Step 2通过声明合并注册到 Feature MapTanStack Table 依靠传给tableFeatures({ ... })的键来推导表格上存在哪些特性状态、选项与 API。要让自定义特性的键类型安全需要用 TypeScript 的**声明合并declaration merging**把它追加到tanstack/angular-table导出的Plugins、TableState_FeatureMap、TableOptions_FeatureMap与Table_FeatureMap四个接口上declare module tanstack/angular-table { interface Plugins { densityPlugin: TableFeature } interface TableState_FeatureMap { densityPlugin: TableState_Density } interface TableOptions_FeatureMap TFeatures extends TableFeatures, TData extends RowData, { densityPlugin: TableOptions_Density } interface Table_FeatureMap TFeatures extends TableFeatures, TData extends RowData, { densityPlugin: Table_Density } }注册完成后TypeScript 只会在features包含densityPlugin的表格上推导出该特性的状态、选项与 API。这一机制的核心实现在 packages/table-core/src/types/TableFeatures.ts文件顶部的Plugins接口就是文档注释所写的自定义表格特性的声明合并目标见 TableFeatures.ts#L53-L60而ExtractFeatureMapTypes类型会把TFeatures中出现的键对应的 feature map 条目交叉intersection合并为最终可用的类型集合——当TFeatures为any时则保留所有条目以维持宽泛兼容。此外文件还定义了NonFeatureKeystableMeta、各 row model 工厂与 fn 注册表等不是表格特性的槽位和FeatureSlotPrereqs描述槽位对特性的前置依赖例如columnResizingFeature依赖columnSizingFeature自定义特性同样可以声明合并自己的槽位前置条件以获得与内置特性一致的校验。Step 3创建特性对象类型就绪后就可以创建特性对象了。使用TableFeature类型约束只要类型声明正确编写过程中不会产生 TypeScript 报错export const densityPlugin: TableFeature { // define the new features initial state getInitialState: (initialState) { return { density: md, ...initialState, // must come last } }, // define the new features default options getDefaultTableOptions: (table) { return { enableDensity: true, onDensityChange: makeStateUpdater(density, table), } }, // if you need to add a default column definition... // getDefaultColumnDef: () {}, // define the new features table instance methods constructTableAPIs: (table) { assignTableAPIs(densityPlugin, table, { table_setDensity: { fn: (updater: UpdaterDensityState) { const safeUpdater: UpdaterDensityState (old) { const newState functionalUpdate(updater, old) return newState } return table.options.onDensityChange?.(safeUpdater) }, }, table_toggleDensity: { fn: (value?: DensityState) { const safeUpdater: UpdaterDensityState (old) { if (value) return value return old lg ? md : old md ? sm : lg } return table.options.onDensityChange?.(safeUpdater) }, }, }) }, // if you need to add row instance APIs... // assignRowPrototype: (prototype, table) {}, // initRowInstanceData: (row) {}, // if you need to add cell instance APIs... // assignCellPrototype: (prototype, table) {}, // initCellInstanceData: (cell) {}, // if you need to add column instance APIs... // assignColumnPrototype: (prototype, table) {}, // initColumnInstanceData: (column) {}, // if you need to add header instance APIs... // assignHeaderPrototype: (prototype, table) {}, // initHeaderInstanceData: (header) {}, // if you need to add header group instance data... // initHeaderGroupInstanceData: (headerGroup) {}, }几个值得注意的细节getInitialState中...initialState必须放在最后以保证调用方传入的初始状态优先于特性的默认值getDefaultTableOptions借助makeStateUpdater(density, table)生成默认的onDensityChange让状态在非受控模式下自动写入表格内部状态原子constructTableAPIs中的assignTableAPIs接受以table_为前缀的命名键前缀用于内部推导方法名与特性归属fn内部把Updater统一包装成函数形式再交给onDensityChange因此既支持直接传新值也支持传更新函数。Step 4把特性接入表格将特性对象放入tableFeatures()调用并把结果传给injectTable的features选项const features tableFeatures({ densityPlugin }) readonly table injectTable(() ({ features, columns, data, //.. }))tableFeatures()本身是一个轻量但带有类型校验的包装函数定义于 packages/table-core/src/helpers/tableFeatures.ts#L46其签名tableFeaturesTFeatures extends TableFeatures(features: TFeatures ValidateFeatureSlotsTFeatures): TFeatures在编译期对特性槽位前置条件FeatureSlotPrereqs做校验并原样返回传入对象。示例中的用法见 custom-plugin 的 app.ts展示了它与内置特性混用const features tableFeatures({ rowPaginationFeature, densityPlugin, // pass in our plugin just like any other stock feature paginatedRowModel: createPaginatedRowModel(), })Step 5在应用中使用新状态、选项与 API示例中用 Angular signal 承载density状态并通过新的onDensityChange选项把表格的状态更新接回 signal受控模式TypeScript 全程保持类型推导const features tableFeatures({ densityPlugin }) export class App { readonly density signalDensityState(md) readonly table injectTable(() ({ features, columns, data: this.data(), //... state: { density: this.density(), // passing the density state to the table, TS is still happy :) }, onDensityChange: (updater) typeof updater function ? this.density.update(updater) : this.density.set(updater), })) }模板中直接调用注入的实例 APItable.toggleDensity()并把 density 映射为单元格内边距button (click)table.toggleDensity()Toggle Density/button td [style.padding]density() sm ? 4px : density() md ? 8px : 16px styletransition: padding 0.2s ng-container *flexRenderCellcell; let renderCell {{ renderCell }}/ng-container /td参考示例还展示了另一种更贴近实战的做法见 custom-plugin 的 app.html 与 app.css把 density 写到表格根元素的自定义属性data-table-density上再由 CSS 的:where(td, th)选择器配合padding: 4px / 8px / 16px与transition: padding 0.2s实现平滑过渡同时示例还提供了Regenerate Data与Stress Test (1M rows)按钮用于验证特性在百万行数据下的行为配套的端到端冒烟测试见 custom-plugin 的 tests。内置特性的聚合导出与按需打包仓库中的 stockFeatures.ts 定义了全部可选内置特性cell-selection、cell-spanning、column-faceting、column-filtering、column-grouping、column-ordering、column-pinning、column-resizing、column-sizing、column-visibility、global-filtering、row-aggregation、row-expanding、row-pagination、row-pinning、row-selection、row-sorting并导出聚合常量stockFeatures。文档注释明确指出见 stockFeatures.ts#L39-L43优先按需导入单个特性以获得 tree-shaking 收益仅当需要包含全部内置特性时才使用聚合对象。自定义特性与这些内置特性在tableFeatures()中地位完全对等——这正是以同样的方式扩展表格这一设计哲学的体现。我们一定要这样做吗需要说明的是上述特性只是把自定义代码与内置特性整合进表格实例的一种新方式。在上面的 density 示例中你完全可以把density状态存在一个 signal 里、在自己的组件中定义toggleDensity处理器、并在模板里独立使用——完全不经过表格实例。把自定义逻辑与表格实例深度整合、还是作为独立逻辑并存两种方式都是完全合法的。是否采用特性机制取决于你的具体场景需要复用表格的状态管道makeStateUpdater、functionalUpdate、外部受控状态桥接时特性机制能帮你免去重复实现需要把逻辑以插件形态分发、让多个项目共享时TableFeature对象 声明合并是标准、可预期的交付形态需要向header/column/row/cell实例注入方法时原型钩子提供了唯一受支持的注入点而如果只是单页面的局部交互独立 signal 组件方法可能更简单直接。从源码结构看见 packages/table-core/src/features/ 中每个特性目录的*Feature.ts实现与 packages/table-core/src/types/TableFeatures.ts 的类型约定特性机制把初始状态、默认选项、实例 API、实例数据四类关注点拆解得非常清晰。遵循这一结构编写自定义特性不仅能让你的扩展与内置特性在行为上完全一致也能让其他维护者一眼读懂其生命周期。深入理解这套机制是驾驭 TanStack Table v9 扩展能力的关键一步。【免费下载链接】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创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表