- 前端
- UI组件
【免费下载链接】table
🤖 Headless UI for building powerful tables & datagrids for TS/JS - React-Table, Vue-Table, Solid-Table, Svelte-Table
本篇指南基于 TanStack Table 的 Lit 适配器(@tanstack/lit-table) 的官方列过滤文档展开,系统讲解如何在一个 Lit 自定义元素中为表格启用列过滤、在客户端与服务端过滤之间做选择、管理列过滤状态,以及如何利用 18 个内置过滤函数与自定义过滤函数定制每一列的匹配逻辑。读完本文,你将掌握从零配置到复杂场景(范围过滤、日期过滤、子行过滤、服务端手动过滤)的完整实现方案,并能直接参考仓库中的 Lit 过滤示例 落地到自己的项目中。
示例先行
如果希望直接跳到可运行实现,仓库中为 Lit 适配器提供了四个与列过滤直接相关的示例,推荐按下面的顺序阅读:
- Column Filters 示例:基础列过滤,包含文本、范围、下拉选择、日期范围四种过滤输入;
- Faceted Filters 示例:分面过滤(基于数据分布统计每个选项的可用性);
- Bucketed Faceted Filters 示例:分桶分面过滤;
- Fuzzy Search 示例:模糊搜索,包含完整的自定义过滤函数注册示例。
列过滤基础配置
TanStack Table 把过滤分为两种口味:列过滤(Column Filtering)与全局过滤(Global Filtering)。本文聚焦列过滤——它作用于单个列的 accessor 取值。全局过滤的细节可参考 全局过滤指南。
TanStack Table 同时支持客户端过滤与手动服务端过滤。下面先看最基础的表配置:把columnFilteringFeature加入 features,即可启用列过滤相关的全部 API。如果你使用客户端过滤,还需要在它之后注册filteredRowModel槽位——因为 row model 槽位是经过类型检查的。
import { LitElement, html } from 'lit' import { customElement, state } from 'lit/decorators.js' import { TableController, tableFeatures, columnFilteringFeature, createFilteredRowModel, filterFn_includesString, filterFn_inNumberRange, } from '@tanstack/lit-table' const features = tableFeatures({ columnFilteringFeature, filteredRowModel: createFilteredRowModel(), // 使用客户端过滤时必须注册 // manualFiltering: true, // 使用手动服务端过滤时开启 filterFns: { includesString: filterFn_includesString, inNumberRange: filterFn_inNumberRange, }, }) @customElement('my-table') class MyTable extends LitElement { @state() private data = defaultData private tableController = new TableController(this) protected render() { const table = this.tableController.table({ features, columns, data: this.data, }) return html`...` } }[!NOTE] 上面
filterFns注册表中只列出了该表用到的内置过滤函数。虽然整体展开内置注册表(filterFns: { ...filterFns })也能工作,但那会把每一个内置过滤函数都打进你的产物。更优做法是:只注册你实际用到的函数,或者干脆不注册,直接把函数作为filterFn列选项传给列定义。
这一点在核心源码中有明确呼应:packages/table-core/src/features/column-filtering/filterFns.ts中导出的聚合注册表filterFns对象被显式标注为@deprecated,其 JSDoc 说明「注册整个对象会放弃 tree-shaking,所有内置过滤函数都会进入产物」,并推荐改为按需导入单个filterFn_*函数(见 filterFns.ts)。注意该文件还额外导出了filterFn_greaterThan、filterFn_greaterThanOrEqualTo、filterFn_lessThan、filterFn_lessThanOrEqualTo四个比较型函数,它们不在默认注册表内,需要时可单独导入直接传给列。
客户端 vs 服务端过滤:先选对边界
过滤应与排序、分页作用于同一份数据集。判断依据很简单:
- 客户端过滤:浏览器持有完整数据集时使用;
- 服务端过滤:浏览器只拿到一页或某个子集时使用,除非你刻意只想过滤已加载的行。
完整的决策框架、性能因素以及多数据操作组合的指导见 客户端 vs 服务端指南。
从源码结构看,客户端的过滤流水线是一条同步的内存转换链:createFilteredRowModel工厂返回一个被tableMemo包裹的 row model 函数,它的 memo 依赖是「过滤前的 row model +columnFiltersatom +globalFilteratom」,一旦任一依赖变化就重算(见 createFilteredRowModel.ts)。这是 TanStack Table 始终是同步状态管理器的直接体现——真正的数据获取与后端查询发生在表格之外。
另外有一个容易踩坑的细节:客户端过滤 row model 会在列过滤输入变化时触发 page-index 自动重置钩子。分页索引是否重置,取决于autoResetPageIndex、autoResetAll与manualPagination选项。如果过滤是手动模式且该 row model 被省略或绕过,列过滤状态变化不会触发这个钩子,这时你需要在过滤变更处理器里手动重置服务端分页。
手动服务端过滤
当你决定采用服务端过滤而不是内置的客户端过滤时,做法如下:
手动服务端过滤不需要filteredRowModel。你传给表格的data应当已经是过滤后的结果。不过,如果你的features里已经注册了filteredRowModel,可以通过把manualFiltering选项设为true让表格跳过它:
const features = tableFeatures({ columnFilteringFeature }) const table = this.tableController.table({ features, data: this.data, columns, manualFiltering: true, })[!NOTE] 使用手动过滤时,本指南后面讨论的很多选项都将不再生效。当
manualFiltering为true,表格实例不会对传入的行应用任何过滤逻辑,而是假定行已被过滤,直接按传入的data原样使用。
在完全手动的服务端配置里,客户端过滤/排序/分页 row model 被省略后,其对应的页索引重置钩子也不会运行,因此典型的做法是在onColumnFiltersChange等变更回调中一并重置pageIndex。完整的查询键集成模式(含 TanStack Query 的useQuery与游标分页useInfiniteQuery两种形态)可参考 客户端 vs 服务端指南 中的示例。
客户端过滤
使用内置客户端过滤时,把columnFilteringFeature加入 features,并在tableFeatures上以槽位形式注册filteredRowModel工厂。从@tanstack/lit-table导入createFilteredRowModel与所需的过滤函数:
import { TableController, tableFeatures, columnFilteringFeature, createFilteredRowModel, filterFn_includesString, filterFn_inNumberRange, } from '@tanstack/lit-table' const features = tableFeatures({ columnFilteringFeature, filteredRowModel: createFilteredRowModel(), filterFns: { includesString: filterFn_includesString, inNumberRange: filterFn_inNumberRange, }, }) const table = this.tableController.table({ features, data: this.data, columns, })过滤流水线内部是这样工作的(见 createFilteredRowModel.ts):表格遍历columnFilters数组,对每个过滤项通过column_getFilterFn(column)解析出该列实际使用的过滤函数,并在测试任何一行之前先对过滤值执行resolveFilterValue做一次归一化(filterFn.resolveFilterValue?.(columnFilter.value) ?? columnFilter.value),然后把解析后的过滤项逐个打到每一行上;只有当行在所有列过滤与全局过滤上全部通过时(row.columnFilters[id] !== false),该行才会被保留(同一文件 L182-L195)。
列过滤状态(Column Filter State)
无论客户端还是服务端过滤,你都可以直接使用 TanStack Table 内置的列过滤状态管理。表格与列都提供了大量 API 来变更、交互与读取过滤状态。
列过滤状态被定义为对象数组,形状如下:
interface ColumnFilter { id: string value: unknown } type ColumnFiltersState = ColumnFilter[]由于它是对象数组,你可以同时叠加多个列过滤。
读取列过滤状态
在render方法中读取时,使用table.state.columnFilters(这是通过tableController.table的第二个参数——选择器——挑选出的状态)。TableController会把宿主订阅到table.store上,因此列过滤状态一变化,宿主就会自动更新。在事件处理器或其他非渲染代码中,则可以用table.atoms.columnFilters.get()读取当前快照。
const table = this.tableController.table( { features, columns, data: this.data, //... }, (state) => ({ columnFilters: state.columnFilters }), ) table.state.columnFilters // 在 render 中读取被选中的状态 table.atoms.columnFilters.get() // 在事件处理器中读取快照这与 Lit 适配器的设计完全一致:LitTable类型显式区分了table.state(供渲染读取的被选状态)、table.atoms.<slice>.get()(快照读取)与table.subscribe(细粒度订阅)三种读取方式(见 TableController.ts)。
如果你需要在表格之外也能拿到列过滤状态,可以像下面这样「受控」地拥有这块状态。
受控列过滤状态(Controlled)
如果你需要在应用的其他部分方便地访问列过滤状态,可以自己持有columnFilters状态切片。v9 推荐的方式是把一个外部 atom通过atoms表选项传入。Atom 保留细粒度的订阅能力,过滤值可以从任意模块读取或订阅(例如放进服务端过滤的 query key 中),而无需经由拥有表格的组件:
import { createAtom } from '@tanstack/store' import type { ColumnFiltersState } from '@tanstack/lit-table' // 在模块作用域(或共享的 store 模块)中创建稳定的 atom const columnFiltersAtom = createAtom<ColumnFiltersState>([]) // 可在此设置初始列过滤状态 // 在元素的 render 方法内部 const table = this.tableController.table({ features, columns, data: this.data, //... atoms: { columnFilters: columnFiltersAtom, // 表格的过滤 API 现在会更新 columnFiltersAtom }, }) const columnFilters = columnFiltersAtom.get() // 在任意需要的地方读取 atom 的值另外,v8 风格的state.columnFilters+onColumnFiltersChange组合仍然受支持。对于简单集成或从 v8 迁移的场景它很方便,但细粒度不如外部 atom。更深入对比见 Table State 指南。
@state() private columnFilters: ColumnFiltersState = [] //... const table = this.tableController.table({ features, columns, data: this.data, //... state: { columnFilters: this.columnFilters, }, onColumnFiltersChange: (updater) => { this.columnFilters = typeof updater === 'function' ? updater(this.columnFilters) : updater }, })初始列过滤状态
如果不需要在自己管理的状态作用域里控制列过滤,只想设置一个初始过滤状态,使用initialState表选项而不是state:
const table = this.tableController.table({ features, columns, data: this.data, //... initialState: { columnFilters: [ { id: 'name', value: 'John', // 默认按 'John' 过滤 name 列 }, ], }, })[!NOTE] 不要同时使用
initialState.columnFilters和state.columnFilters,因为受控的state.columnFilters值会覆盖initialState.columnFilters。
FilterFns:每一列独有的过滤逻辑
每一列都可以拥有自己的过滤逻辑。你可以从 TanStack Table 内置的过滤函数中选择,也可以编写自定义函数。
默认提供18 个内置过滤函数,在 filterFns.ts 的注册表中对应列出:
| 函数名 | 行为 |
|---|---|
includesString | 大小写不敏感的字符串包含 |
includesStringSensitive | 大小写敏感的字符串包含 |
startsWith | 大小写不敏感的前缀匹配 |
endsWith | 大小写不敏感的后缀匹配 |
equalsString | 大小写不敏感的字符串相等 |
equalsStringSensitive | 大小写敏感的字符串相等 |
equals | 严格相等=== |
weakEquals | 宽松相等==(便于用字符串输入匹配数字行值) |
empty | 行值为 nullish 或纯空白即通过(过滤值充当开关标志) |
notEmpty | 行值非 nullish 且非纯空白即通过(过滤值充当开关标志) |
arrIncludes | 行的数组(或字符串)值包含至少一个过滤值 |
arrIncludesAll | 行的数组值包含每一个过滤值 |
arrIncludesSome | 行的数组值包含至少一个过滤值 |
arrHas | 行的标量值等于至少一个过滤值 |
inNumberRange | 闭区间[min, max]数字范围(端点归一化,反序自动交换) |
inDateRange | 闭区间[min, max]日期范围,接受Date对象、时间戳或日期字符串(空白端点为开区间) |
between | 开区间 min/max 范围(空白端点为开区间) |
betweenInclusive | 闭区间 min/max 范围(空白端点为开区间) |
从源码看这些内置函数在比较语义上有细致的考量:例如filterFn_inNumberRange会先做typeof dataValue !== 'number' || Number.isNaN(dataValue)守卫,防止null、空串、布尔值被 JavaScript 的宽松关系强制转换滑进数字区间(否则null >= 0 && null <= 20会意外成立),见 filterFns.ts;filterFn_inDateRange则通过resolveDataValue把行值统一转成时间戳,并用toDateTimestamp处理Date对象、时间戳与可解析的日期字符串(同一文件 L331-L357)。
你也可以定义自己的自定义过滤函数:要么内联作为filterFn列选项,要么按名称注册到tableFeatures的filterFns槽位。
自定义过滤函数
[!NOTE] 这些过滤函数只在客户端过滤期间运行。
无论你把自定义函数注册在filterFns槽位,还是直接作为filterFn列选项传递,它都应具备如下签名:
const myCustomFilterFn: FilterFn<typeof features, MyData> = ( row, // Row<typeof features, MyData> columnId: string, filterValue: any, addMeta?: (meta: FilterMeta) => void, ): boolean => ...每个过滤函数都会收到:
- 待过滤的行(row);
- 用于取出行值的
columnId; - 过滤值(filterValue)。
并应返回true(该行保留在过滤结果中)或false(该行被移除)。
const columns = [ { header: () => 'Name', accessorKey: 'name', filterFn: 'includesString', // 使用内置过滤函数 }, { header: () => 'Age', accessorKey: 'age', filterFn: 'inNumberRange', }, { header: () => 'Birthday', accessorKey: 'birthday', filterFn: 'myCustomFilterFn', // 引用注册在 filterFns 槽位中的自定义函数 }, { header: () => 'Profile', accessorKey: 'profile', // 直接内联自定义过滤函数 filterFn: (row, columnId, filterValue) => { return // 基于你的自定义逻辑返回 true 或 false }, }, ] //... const features = tableFeatures({ columnFilteringFeature, filteredRowModel: createFilteredRowModel(), filterFns: { includesString: filterFn_includesString, inNumberRange: filterFn_inNumberRange, myCustomFilterFn: (row, columnId, filterValue) => { return // 基于你的自定义逻辑返回 true 或 false }, startsWith: startsWithFilterFn, // 在别处定义 }, }) const table = this.tableController.table({ features, columns, data: this.data, })TypeScript 说明:像
filterFn: 'myCustomFilterFn'这样的字符串引用,只要函数注册在tableFeatures的filterFns槽位中,就会被自动类型推断。该注册表槽位取代了旧的declare module增强方式。或者,你也可以完全跳过注册表,直接把函数传给filterFn列选项。完整的注册示例参见 Fuzzy Search 示例。
定制过滤函数行为(resolveFilterValue / resolveDataValue / autoRemove)
你可以给过滤函数挂上几个可选属性来定制它的行为:
filterFn.resolveFilterValue:这个挂在任意filterFn上的可选方法,允许过滤函数在把过滤值传给比较逻辑之前先做转换/清洗/格式化。表格每个过滤只应用一次(而非每行一次),所以它也是做昂贵预处理工作的正确位置。filterFn.resolveDataValue:这个可选方法在每一行的值与过滤值比较之前归一化该行值。所有用constructFilterFn辅助函数构建的过滤函数都会遵循它,这包括所有内置过滤函数。filterFn.autoRemove:这个可选方法接收过滤值,返回true表示该过滤值应从过滤状态中移除。例如某些布尔风格过滤器可能希望在过滤值被设为false时把它从状态中移除。一旦提供,这个判断就是权威的:它决定保留的值即使为空字符串也会留在过滤状态中(否则默认启发式规则会移除空串)。而undefined的过滤值无论如何都会清除过滤。
constructFilterFn辅助函数可以从一个「值级比较器」加上上述可选解析器构建出过滤函数(实现见 filterFns.ts,它把比较与归一化分离,并将定义挂到返回函数上,从而支持通过展开已有函数来派生变体):
const startsWithFilterFn = constructFilterFn({ // 用(解析后的)行值与(解析后的)过滤值比较 filter: (dataValue, filterValue) => Boolean(dataValue?.startsWith(filterValue)), // 在测试任何行之前,把过滤值归一化一次 resolveFilterValue: (value) => String(value).toLowerCase().trim(), // 每一行的值在进入比较器之前先归一化 resolveDataValue: (value) => String(value ?? '').toLowerCase(), // 过滤值若为 falsy(此处即空字符串),则从过滤状态中移除 autoRemove: (value) => !value, })把比较放在filter、归一化放在解析器里,当你需要某个既有过滤函数的变体时非常划算。定义被挂在返回的函数上,因此你可以展开任何用constructFilterFn构建的过滤函数,只覆盖有差异的部分。例如:一个额外忽略变音符号的includesString版本(这样搜索 "eric" 也能命中 "Éric"):
const normalize = (value: unknown) => String(value ?? '') .toLowerCase() .normalize('NFD') .replace(/\p{Diacritic}/gu, '') const includesStringIgnoreDiacritics = constructFilterFn({ ...filterFn_includesString, // 复用比较器与 autoRemove 行为 resolveFilterValue: normalize, resolveDataValue: normalize, })像任何自定义过滤函数一样,把该变体按名称注册进filterFns注册表,或直接传给filterFn列选项即可。
[!NOTE] 表格会在测试任何行之前对每个过滤应用一次
resolveFilterValue。如果你在表格之外直接调用过滤函数,需要自己先解析过滤值:myFilterFn(row, columnId, myFilterFn.resolveFilterValue?.(rawValue) ?? rawValue)。
定制列过滤行为
除了过滤函数本身,还有大量表格级与列级选项可以进一步定制列过滤行为。
禁用列过滤
默认情况下,所有列都启用了列过滤。你可以通过enableColumnFilters表选项(禁用全部列)或enableColumnFilter列选项(禁用特定列)关闭它;也可以用enableFilters: false表选项同时关闭列过滤与全局过滤。
对某列禁用列过滤后,该列的column.getCanFilterAPI 会返回false。
const columns = [ { header: () => 'Id', accessorKey: 'id', enableColumnFilter: false, // 禁用该列的列过滤 }, //... ] //... const table = this.tableController.table({ features, columns, data: this.data, enableColumnFilters: false, // 禁用所有列的列过滤 })过滤子行(与展开/分组/聚合特性联动)
当同时使用展开(expanding)、分组(grouping)、聚合(aggregation)等特性时,还有几个表选项可以定制列过滤对树形行的行为。
从叶子行过滤(filterFromLeafRows)
默认情况下,过滤从父行向下进行:如果父行被过滤掉,它的所有子行也会一并被过滤掉。如果你的需求是只让用户搜索顶层行、不搜索子行,这种默认行为正是你想要的——它也是性能最优的选择。
但如果你希望子行也能被过滤和搜索,无论父行是否被过滤掉,可以把filterFromLeafRows表选项设为true。设为true后,过滤将从叶子行向上进行:只要某个子行或孙行被保留,父行就会被包含进来。
const features = tableFeatures({ columnFilteringFeature, rowExpandingFeature, filteredRowModel: createFilteredRowModel(), expandedRowModel: createExpandedRowModel(), filterFns: { includesString: filterFn_includesString, inNumberRange: filterFn_inNumberRange, }, }) const table = this.tableController.table({ features, columns, data: this.data, filterFromLeafRows: true, // 过滤并搜索子行 })最大叶子行过滤深度(maxLeafRowFilterDepth)
默认情况下,过滤会作用于树中所有行,无论是根级父行还是父行的子叶子行。把maxLeafRowFilterDepth表选项设为0,过滤将只作用于根级父行,所有子行保持不过滤;设为1则只过滤 1 层深的子叶子行,以此类推。
如果你希望父行通过过滤时保留其子行不被过滤掉,用maxLeafRowFilterDepth: 0。
const features = tableFeatures({ columnFilteringFeature, rowExpandingFeature, filteredRowModel: createFilteredRowModel(), expandedRowModel: createExpandedRowModel(), filterFns: { includesString: filterFn_includesString, inNumberRange: filterFn_inNumberRange, }, }) const table = this.tableController.table({ features, columns, data: this.data, maxLeafRowFilterDepth: 0, // 只过滤根级父行 })列过滤 API 速查
有大量的 Column 与 Table API 可以用于与列过滤状态交互、并把过滤控件接到你的 UI 组件上。以下是可用 API 及其最常见的用途:
| API | 用途 |
|---|---|
table.setColumnFilters | 用新的状态整体覆盖列过滤状态 |
table.resetColumnFilters | 适合做「清除全部/重置过滤」按钮 |
column.getFilterValue | 获取默认初始过滤值(用于输入框回填),或直接把过滤值提供给过滤输入 |
column.setFilterValue | 把过滤输入接到onChange/onBlur处理器上 |
column.getCanFilter | 用于禁用/启用过滤输入 |
column.getIsFiltered | 用于展示「该列正在被过滤」的视觉指示器 |
column.getFilterIndex | 用于展示当前过滤应用的顺序 |
column.getAutoFilterFn | 内部使用:当列未指定过滤函数时,为列查找默认过滤函数 |
column.getFilterFn | 用于展示当前正在使用的过滤模式/函数 |
端到端实战:一个可运行的 Lit 过滤表
仓库中的 Column Filters 示例 把上述所有概念串成了一个完整可运行的表,值得逐行研读(入口在 main.ts)。它的关键设计如下:
按需注册过滤函数:
tableFeatures中只注册了includesString、inNumberRange、inDateRange、equalsString四个函数,配合columnMeta: metaHelper<MyColumnMeta>()定义自定义列元数据(main.ts)。用 meta 驱动过滤输入形态:通过
meta: { filterVariant: 'range' | 'select' | 'dateRange' }声明每列的过滤控件类型,然后在一个独立的column-filter自定义元素里用switch渲染文本输入、数字范围、下拉选择与日期范围四种控件(main.ts)。其中范围类输入借助setFilterValue((old) => [min, old[1]])这种函数式更新,只修改区间的单端。渲染层:在表头渲染中,通过
header.column.getCanFilter()判断是否渲染过滤控件,并把header.column通过.column属性绑定传给column-filter子元素(main.ts);数据行通过table.getRowModel().rows遍历渲染,表格同时开启了debugTable: true以便在<pre>中观察完整表格状态(main.ts)。大体积数据验证:示例默认生成 5 万行数据,并提供一键生成 100 万行的「Stress Test」按钮(main.ts),用于验证列过滤在客户端处理大数据量时的性能表现。对应的端到端测试位于 smoke.spec.ts。
运行该示例的方式与仓库内其他示例一致:在 examples/lit/filters 目录下安装依赖后启动 Vite 开发服务器(vite.config.js已就绪),即可在浏览器中交互体验列过滤全流程。
小结
列过滤是 TanStack Table 数据流水线(过滤 → 排序 → 分页)的第一环,也是filteredRowModelrow model 的核心职责。本文覆盖了从「选择客户端还是服务端」到「状态管理方式(非受控 / 受控 / 外部 atom)」「18 个内置过滤函数 +constructFilterFn派生变体」「filterFromLeafRows与maxLeafRowFilterDepth的树形行为定制」的完整链路。在动手实现前,建议先通读 客户端 vs 服务端指南 确定数据边界,再以 Column Filters 示例 为模板起步;分面与模糊搜索场景则分别参考 filters-faceted、filters-faceted-bucketed 与 filters-fuzzy 示例。
- 前端
- UI组件
【免费下载链接】table
🤖 Headless UI for building powerful tables & datagrids for TS/JS - React-Table, Vue-Table, Solid-Table, Svelte-Table
相关推荐
TanStack Lit Table 全局过滤(Global Filtering)实战指南:从客户端筛选到服务端过滤
TanStack Lit Table 全局过滤(Global Filtering)实战指南:从客户端筛选到服务端过滤 本指南以 @tanstack/lit ta
前端UI组件K3s 如何为 btrfs 文件系统启用 containerd btrfs 快照器并验证快照生成
K3s 如何为 btrfs 文件系统启用 containerd btrfs 快照器并验证快照生成 如果你的节点数据目录建在 btrfs 文件系统上,K3s 内置
前端UI组件@tanstack/lit-table 参考指南:面向 Lit 的 TanStack Table 适配层 API 全景
@tanstack/lit table 参考指南:面向 Lit 的 TanStack Table 适配层 API 全景 @tanstack/lit table
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考