TanStack Table Lit 快速上手:用 TableController 与 tableFeatures 构建 Web Components 表格
2026/9/20 13:49:56 网站建设 项目流程

TanStack Table Lit 快速上手:用 TableController 与 tableFeatures 构建 Web Components 表格

【免费下载链接】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 是一款 headless 表格库:它负责管理表格的状态与逻辑(排序、过滤、分页、行选择等),而标记与样式 100% 由你掌控。本文基于仓库中的 Lit 快速上手文档,带你从安装@tanstack/lit-table到渲染出第一张 Lit 表格,再逐步叠加排序等特性,并深入源码剖析TableControllertableFeaturesFlexRender与状态选择器的工作原理。读完本文,你将掌握在 Lit 自定义元素中集成 TanStack Table 的标准模式,以及如何按需注册功能特性、组合可复用表格钩子。

安装

在项目中使用 Lit 表格适配器,只需安装@tanstack/lit-table

npm install @tanstack/lit-table

该包由 packages/lit-table 提供,它在@tanstack/table-core之上构建,并通过@tanstack/lit-store接入 TanStack Store 的响应式系统。包的核心入口 packages/lit-table/src/index.ts 会重新导出 table-core 的全部内容,并额外导出FlexRenderTableControllersubscribe指令与createTableHook

第一张表格:从零到可运行

下面的组件是完整的:把它粘贴进一个 Lit 应用就能看到一张可运行的表格。@tanstack/lit-table适配器围绕TableController构建——它实现了 Lit 的ReactiveController,只构造一次表格实例、让宿主订阅状态变化,并在每次渲染时给你一个全新的表格实例。

import { LitElement, html } from 'lit' import { customElement, state } from 'lit/decorators.js' import { repeat } from 'lit/directives/repeat.js' import { FlexRender, TableController, tableFeatures } from '@tanstack/lit-table' import type { ColumnDef } from '@tanstack/lit-table' // 1. 定义数据形状 type Person = { firstName: string lastName: string age: number } // 2. v9 新增:声明该表格用到的特性(这里暂未启用任何特性) const features = tableFeatures({}) // 3. 定义列 const columns: Array<ColumnDef<typeof features, Person>> = [ { accessorKey: 'firstName', // accessorKey 简写 header: 'First Name', cell: (info) => info.getValue(), }, { accessorFn: (row) => row.lastName, // accessorFn 替代方案,需自定义 id id: 'lastName', header: () => html`<span>Last Name</span>`, cell: (info) => html`<i>${info.getValue<string>()}</i>`, }, { accessorKey: 'age', header: () => 'Age', }, ] @customElement('person-table') export class PersonTable extends LitElement { // 4. 把数据放在响应式属性里 @state() private data: Array<Person> = [ { firstName: 'tanner', lastName: 'linsley', age: 24 }, { firstName: 'tandy', lastName: 'miller', age: 40 }, { firstName: 'joe', lastName: 'dirte', age: 45 }, ] // 5. 为宿主元素创建一个 TableController private tableController = new TableController<typeof features, Person>(this) protected render() { // 6. 在渲染过程中创建表格实例 const table = this.tableController.table( { features, columns, data: this.data, }, () => ({}), // 状态选择器,未使用特性状态时传空即可 ) // 7. 用表格实例的 API 渲染标记 return html` <table> <thead> ${repeat( table.getHeaderGroups(), (headerGroup) => headerGroup.id, (headerGroup) => html` <tr> ${repeat( headerGroup.headers, (header) => header.id, (header) => html` <th> ${header.isPlaceholder ? null : FlexRender({ header })} </th> `, )} </tr> `, )} </thead> <tbody> ${repeat( table.getRowModel().rows, (row) => row.id, (row) => html` <tr> ${repeat( row.getAllCells(), (cell) => cell.id, (cell) => html`<td>${FlexRender({ cell })}</td>`, )} </tr> `, )} </tbody> </table> ` } }

关于这段代码,有几点值得注意:

  • tableFeatures({})声明了表格使用的可选特性。只注册需要的内容可以让打包体积更小,并让 TypeScript 为表格实例推导出精确的类型。
  • 核心行模型(core row model)始终自动包含。特性行模型(排序、过滤、分页)在你需要时,直接作为插槽注册到tableFeatures({...})调用上。
  • FlexRender负责渲染列定义中的headercellfooter,无论它们是普通值还是 Lit 模板。如果你不想显式导入,它也会以table.FlexRender的形式挂到实例上。
  • tableController.table(...)的第二个参数是状态选择器,它决定了table.state里包含什么;在用到特性状态之前,传一个空选择器完全没问题。

查看完整的 Basic TableController 示例,那里有带更多列和页脚的、可直接运行的版本。

从源码理解 TableController 的渲染生命周期

从源码看,TableController的关键设计是"一次构造、多次复用"。packages/lit-table/src/TableController.ts 中的table()方法首次调用时,会把coreReactivityFeature(即litReactivity())与用户传入的features合并,然后调用 table-core 的constructTable创建底层表格实例,并用createRenderPhaseSource建立渲染期状态源:

this._table = constructTable(mergedOptions) this._rootSource = createRenderPhaseSource<TableState<TFeatures>>( this._table.store, shallow, ) this._setupSubscriptions()

后续每次渲染调用table()时,只做三件事:通过table_setOptions暂存本轮 options(真正的发布延迟到hostUpdated())、读取当前渲染快照、记录最新的状态选择器,然后返回一个扩展了subscribeFlexRender和只读stateLitTable实例。这也是为什么你可以在render()里反复调用它——它不会重复构造表格,而是把新 options 合并进同一个实例。

订阅门控逻辑体现在_setupSubscriptions(TableController.ts):当传入选择器时,只有所选状态发生浅比较变化才触发host.requestUpdate();不传选择器则保持每次状态变更都更新宿主的行为。配合hostUpdated()中调用markCommittedtable_publishExternalState,外部可控状态会在 Lit 提交本轮渲染后被正确发布。

litReactivity()本身定义在 packages/lit-table/src/reactivity.ts,它基于@tanstack/table-core/reactivityrenderPhaseReactivity,并注入@tanstack/lit-storecreateAtombatch,让所有 atom 与用户提供的外部 atom 共享同一个 store 实例。

叠加一个特性:排序

v9 中特性是可选加入(opt-in)的。要让列可排序,需要在tableFeatures中注册rowSortingFeaturesortedRowModel工厂,然后接上表头的点击处理器:

import { FlexRender, TableController, createSortedRowModel, rowSortingFeature, sortFns, tableFeatures, } from '@tanstack/lit-table' const features = tableFeatures({ rowSortingFeature, // 启用排序 API 与状态 sortedRowModel: createSortedRowModel(), // 客户端排序 sortFns, }) @customElement('person-table') export class PersonTable extends LitElement { // data 与 tableController 与上文一致 protected render() { const table = this.tableController.table( { features, columns, data: this.data, }, (state) => ({ sorting: state.sorting }), // 选择排序状态 ) return html` <table> <thead> ${repeat( table.getHeaderGroups(), (headerGroup) => headerGroup.id, (headerGroup) => html` <tr> ${repeat( headerGroup.headers, (header) => header.id, (header) => html` <th> ${ header.isPlaceholder ? null : html`<div @click=${header.column.getToggleSortingHandler()} style="cursor: ${ header.column.getCanSort() ? 'pointer' : 'default' }" > ${FlexRender({ header })} ${ { asc: ' 🔼', desc: ' 🔽' }[ header.column.getIsSorted() as string ] ?? null } </div>` } </th> `, )} </tr> `, )} </thead> <!-- tbody 与上文一致 --> </table> ` } }

现在点击表头会在升序、降序、不排序之间切换。其他所有特性都遵循同样的模式:注册特性(如果它带行模型工厂,就把工厂作为插槽放到tableFeatures上),然后使用它给表格、列和行新增的 API。关于自定义排序函数、多列排序与每列选项,参见 Sorting Guide 和 Sorting 示例。

特性注册的源码依据

在 table-core 中,rowSortingFeaturecreateSortedRowModel()sortFns分别承担不同职责:特性本身提供排序状态(sorting)、列上的getCanSort()/getToggleSortingHandler()/getIsSorted()等 API;行模型工厂提供sortedRowModel这一排序后的行模型插槽;sortFns则是内置的比较函数表。三者在tableFeatures({...})中被组装成完整的特性集,并被ColumnDef<typeof features, Person>用于类型推导——这就是为什么启用特性后,表格实例上的 API 和类型会随之自动扩展。

状态模型:TanStack Store atom 与状态选择器

v9 的表格状态由 TanStack Store 的 atom 支撑,你通常不需要自己管理它:

  • 设置起始值使用initialState
  • 调用特性 API 如table.setSorting(...)table.nextPage()来修改状态;
  • 当你的应用需要亲自拥有一段状态切片(通过atoms选项),或需要细粒度订阅时,再深入阅读 Table State Guide——它是其余所有指南的基础。

细粒度订阅:subscribe 指令

除了状态选择器,TableController返回的LitTable还带有一个subscribe方法(来自 packages/lit-table/src/subscribe-directive.ts),它本质上是@tanstack/lit-storeTanStackStoreSelector与 LitAsyncDirective的桥接。通过"假"的ReactiveControllerHost,它把订阅回调接入指令的setValue()更新周期,从而只重渲染被包裹的那一段模板:

// 1. 订阅特定状态切片(仅当 rowSelection 变化时才重渲染) html` <div> ${table.subscribe( table.store, (state) => state.rowSelection, (rowSelection) => html`<span>Selected: ${JSON.stringify(rowSelection)}</span>`, )} </div> ` // 2. 订阅完整状态(任何状态变更都会重渲染) html` <div> ${table.subscribe( table.store, (state) => html`<span>Total rows: ${state.rowModel.rows.length}</span>`, )} </div> `

这种"状态选择器控制宿主级更新、subscribe控制模板岛级更新"的双层机制,是 Lit 适配器在响应式更新性能上的核心设计:宿主可以只对真正关心的状态切片做出反应,其余内容交给模板内的细粒度订阅。

FlexRender:统一渲染列头、单元格与页脚

FlexRender与底层的flexRender函数定义在 packages/lit-table/src/flexRender.ts:

  • flexRender(Comp, props)是最底层工具:如果Comp是函数则调用它并传入 props,否则原样返回(适用于字符串、TemplateResultDirectiveResultNodeLitRenderable类型);
  • FlexRender({ header })/FlexRender({ cell })/FlexRender({ footer })是便捷封装,一次只能传一个 prop,内部自动调用flexRender(columnDef.cell, cell.getContext())等。

值得一提的是,FlexRender对分组聚合做了特判:当单元格getIsAggregated()返回 true 时,优先渲染aggregatedCell,否则回退到普通cell;当getIsPlaceholder()为 true 时返回null。这意味着表格分组(grouping)场景下聚合单元格无需额外样板代码。

可组合表格:createTableHook 一次定义、处处复用

当应用里的多张表格共享特性、行模型与组件约定时,可以用createTableHook一次性定义:

const features = tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), sortFns, }) const { useAppTable, createAppColumnHelper } = createTableHook({ features })

然后在组件里直接调用useAppTable(this, { columns, data }),而不是手动管理TableController。从 packages/lit-table/src/createTableHook.ts 的实现看,createTableHook会:

  • 创建基于@lit/context的 table / cell / header 三个 context key;
  • 返回预绑定特性与组件的createAppColumnHelper(内部仍是 table-core 的createColumnHelper,类型层面挂上绑定组件);
  • 返回useAppTable——它在内部创建一个TableController和一个ContextProvider,并把默认 options 与调用处 options 合并(后者优先),最终通过Object.assign(table, { AppCell, AppHeader, AppFooter, FlexRender, ...tableComponents })产出带App*包装函数与预绑定组件的扩展表格 API;
  • 返回useTableContext/useCellContext/useHeaderContext,供嵌套的子组件(如分页控件、排序指示器、单元格组件)通过@lit/context消费最近的表格、单元格或表头实例。

AppCell/AppHeader/AppFooter会把注册的cellComponents/headerComponents预绑定到对应实例上,让列定义可以这样写:

columnHelper.accessor('age', { header: 'Age', cell: ({ cell }) => cell.NumberCell(), // cell 上已预绑定 NumberCell 组件 })

完整的模式(包括预绑定的 cell/header 组件)参见 Composable Tables Guide。

下一步去哪里

  • 表格状态:阅读 Table State Guide,它是理解其余一切的基础指南。
  • 特性指南:每个特性都有独立指南,例如 Column Filtering、Pagination、Row Selection、Column Visibility,以及 Sorting。
  • 可组合表格:阅读 Composable Tables Guide,掌握createTableHook的完整用法。
  • 示例:浏览仓库中可运行的 Lit 示例集,从 basic-table-controller 到各类特性演示,直观了解端到端的预期用法;特性对应的源码与测试位于 packages/lit-table/src 和 packages/lit-table/tests,可对照验证 API 行为。

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询