TanStack Angular Table 全局过滤(Global Filtering)完整指南:特性注册、状态管理与服务端/客户端方案
【免费下载链接】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
全局过滤(Global Filtering)是 TanStack Table 中跨所有列执行统一搜索的核心能力,本指南以@tanstack/angular-table在 Angular 环境下的官方文档(docs/framework/angular/guide/global-filtering.md)为骨架,结合仓库内table-core的过滤实现源码与 Angular 官方示例,系统讲解全局过滤的完整接入流程。读完本文,你将掌握:如何注册过滤特性并正确启用客户端/服务端过滤、如何使用内置与自定义globalFilterFn、如何通过外部 Atom 或 Angular signal 掌控过滤状态、如何把搜索输入框接入表格,以及禁用与排查全局过滤的 API 用法。
前置准备:注册过滤相关特性
全局过滤在特性层面依赖列过滤(Column Filtering),因此必须先注册columnFilteringFeature,再注册globalFilteringFeature。若使用客户端过滤,还需在特性之后挂载filteredRowModel: createFilteredRowModel()——因为 row model 槽位是类型检查的,顺序错误会导致类型错误。整个特性配置通过tableFeatures聚合器传入injectTable:
import { signal } from '@angular/core' import { injectTable, tableFeatures, columnFilteringFeature, globalFilteringFeature, createFilteredRowModel, filterFn_includesString, } from '@tanstack/angular-table' const features = tableFeatures({ columnFilteringFeature, globalFilteringFeature, filteredRowModel: createFilteredRowModel(), // if using client-side filtering // manualFiltering: true, // if using manual server-side filtering filterFns: { includesString: filterFn_includesString }, }) export class App { readonly data = signal(defaultData) readonly table = injectTable(() => ({ features, columns, data: this.data(), })) }关于filterFns注册表的打包优化
上面的filterFns注册表只列出了该表实际用到的内置过滤函数。虽然也可以直接展开整个内置注册表(filterFns: { ...filterFns }),但这样做会把所有内置过滤函数打进产物包,破坏 tree-shaking。从源码 filterFns.ts 可以看到,内置注册表filterFns对象本身带有@deprecated标注,官方建议按需引入单个filterFn_*函数并只注册用到的那些;或者干脆不注册,直接把函数传给globalFilterFn/ 列级filterFn选项。
需要说明的是,table-core的完整内置注册表实际包含 18 个过滤函数(除了下文的 12 个之外,还有empty、notEmpty、startsWith、endsWith、equalsStringSensitive、inDateRange等),官方指南中列出的 12 个是全局过滤最常用的内置函数。
injectTable的响应式工作方式
injectTable接受一个 options 工厂函数,injectTable.ts 中将它包进computed,每当工厂内部读取的任何 signal 变化时,就会重新求值并通过setOptions同步到表格实例,这正是 Angular 适配器与响应式模型保持同步的机制。因此建议把columns、特性配置、row model 等昂贵/静态的值放在工厂函数外部保持稳定引用,只在工厂内部读取data()、过滤/分页/排序等响应式状态。
客户端过滤 vs 服务端过滤:先做决策
过滤应当与排序、分页作用于同一份数据集。TanStack Table 官方给出的决策框架是:
- 客户端过滤:浏览器持有完整数据集时使用。过滤、排序、分页全部在本地完成,交互即时。
- 服务端过滤:浏览器只持有单页或其他子集时使用,此时数据应由服务端按过滤条件下发。除非你有意只过滤当前已加载的行,否则不应在子集上做客户端过滤。
完整的决策框架、性能考量以及数据操作组合建议,参见 客户端 vs 服务端指南。
客户端过滤的行模型与页码自动重置
客户端过滤行模型createFilteredRowModel在全局过滤输入变化时还会触发页码自动重置钩子(page-index auto-reset hook)。从 createFilteredRowModel.ts 的源码可以看到,它把columnFilters与globalFilter两个状态切片作为 memo 依赖,并在更新后通过onAfterUpdate: skipFirstRun(() => table_autoResetPageIndex(table))调用table_autoResetPageIndex。
页码是否真的重置,取决于autoResetPageIndex、autoResetAll与manualPagination三个选项的取值。如果过滤是手动的(manual)且这个行模型被省略或绕过,全局过滤状态变化就不会触发该钩子,此时如果使用服务端分页,需要在过滤变化的处理函数里手动重置分页。
手动服务端全局过滤(Manual Server-Side)
如果你决定用服务端全局过滤取代内置的客户端过滤,做法如下:
- 不需要
filteredRowModel。传给表格的data应当是服务端已经过滤好的数据。 - 但如果你在 features 中已经添加了
filteredRowModel,可以通过把manualFiltering选项设为true让表格跳过它:
import { injectTable, tableFeatures, columnFilteringFeature, globalFilteringFeature, } from '@tanstack/angular-table' const features = tableFeatures({ columnFilteringFeature, globalFilteringFeature }) readonly table = injectTable(() => ({ features, data, columns, manualFiltering: true, }))[!NOTE] 使用手动全局过滤时,本指南后续讨论的许多选项将不起作用。当
manualFiltering为true,表格实例不会对传入的行应用任何全局过滤逻辑,而是假定行已被过滤,按传入的data原样使用。
在服务端方案下,globalFilter状态本身仍然很有价值——它可以作为请求参数或查询 key 的一部分,把搜索词发送给后端,详见下文"全局过滤状态"一节。
客户端全局过滤(Client-Side)
使用内置的客户端全局过滤时,在 features 中加入globalFilteringFeature(以及它必需的columnFilteringFeature前置依赖)和filteredRowModel工厂即可:
import { injectTable, tableFeatures, columnFilteringFeature, globalFilteringFeature, createFilteredRowModel, filterFn_includesString, } from '@tanstack/angular-table' const features = tableFeatures({ columnFilteringFeature, globalFilteringFeature, filteredRowModel: createFilteredRowModel(), filterFns: { includesString: filterFn_includesString }, }) readonly table = injectTable(() => ({ features, // other options... }))底层过滤流程
从 createFilteredRowModel.ts 的实现可以看到客户端全局过滤的完整链路:
- 空过滤值(
undefined/null/'')会整体跳过过滤,直接返回预过滤行模型; - 解析列过滤与全局过滤函数,其中全局过滤函数通过
table_getGlobalFilterFn(table)解析(函数直接返回、'auto'委托给includesString、字符串从filterFns注册表查找,见 globalFilteringFeature.utils.ts); - 用
column_getCanGlobalFilter筛出所有可全局过滤的叶子列,对每一列都执行一次全局过滤函数,并给行打上__global__标记——任意一列命中即为保留行(命中即break); - 最后通过
filterRows综合列过滤与全局过滤结果,横向过滤每一行。
这就是"全局过滤 = 一个搜索词作用于所有允许过滤的列"的实现本质。
全局过滤函数:globalFilterFn
globalFilterFn选项决定全局过滤所用的过滤函数。它可以是一个字符串,引用注册在tableFeatures的filterFns槽位中的内置或自定义过滤函数;也可以直接传一个函数:
readonly table = injectTable(() => ({ features, // filteredRowModel and filterFns are registered in features data, columns, globalFilterFn: 'includesString', // built-in filter function }))12 个内置过滤函数
默认情况下有 12 个内置过滤函数可选:
| 函数名 | 行为说明 |
|---|---|
includesString | 不区分大小写的字符串包含匹配(全局过滤的默认值) |
includesStringSensitive | 区分大小写的字符串包含匹配 |
equalsString | 不区分大小写的字符串相等 |
equals | 严格相等=== |
weakEquals | 宽松相等== |
arrIncludes | 行的数组(或字符串)值包含过滤值中的至少一个 |
arrIncludesAll | 行的数组值包含每一个过滤值 |
arrIncludesSome | 行的数组值包含过滤值中的至少一个 |
arrHas | 行的标量值等于过滤值中的至少一个 |
inNumberRange | 闭区间[min, max]数字范围(端点会归一化,若反向则自动交换) |
between | 开区间 min/max 范围(空端点视为开放) |
betweenInclusive | 闭区间 min/max 范围(空端点视为开放) |
以默认的includesString为例,filterFns.ts 中它的实现通过resolveFilterValue/resolveDataValue把过滤值与单元格数据统一String(...).toLowerCase()后再做includes比较——这就是"不区分大小写"的来源。inNumberRange则会先把端点parseFloat归一化,反向端点自动交换,并且明确拒绝非数字行值(null、''、布尔值等不会因 JS 宽松关系强转而混入区间)。
你也可以定义自己的自定义全局过滤函数并直接传给globalFilterFn选项,详见下文"自定义全局过滤函数"。
全局过滤状态(Global Filter State)
globalFilter状态切片保存当前的全局过滤值,通常是一个搜索字符串(切片类型为any,以便自定义全局过滤函数接受其他形状的值)。在 Angular 中,读取方式是table.atoms.globalFilter.get()——表格的 atom 读取就是 signal 读取,在模板表达式、computed(...)或effect(...)中读取会自动追踪更新。
方式一:外部 Atom(v9 推荐)
如果需要在表格之外访问全局过滤状态,官方推荐在 v9 中使用外部 Atom:用@tanstack/angular-store的createAtom创建,并通过atoms表格选项传入。Atom 保留细粒度的订阅,过滤值可以被其他位置(例如服务端过滤的查询 key)直接使用,而无需在每次变化时重新执行injectTable的 options 初始化器:
import { createAtom } from '@tanstack/angular-store' export class App { readonly globalFilterAtom = createAtom<string>('') readonly table = injectTable(() => ({ features, // filteredRowModel and filterFns are registered in features // other options... atoms: { globalFilter: this.globalFilterAtom, // table.setGlobalFilter now updates globalFilterAtom }, })) // read the atom wherever you need the value (e.g. for a query key) // this.globalFilterAtom.get() }注意:一旦通过atoms.globalFilter传入外部 Atom,table.setGlobalFilter更新的是这个 Atom 的值,外部即可实时读取。
方式二:v8 风格受控状态(Angular signal)
v8 风格的state.globalFilter+onGlobalFilterChange模式仍然受支持。在 Angular 中这意味着用一个 Angular signal 来持有切片,如下面 Basic External State 示例 所示:
readonly globalFilter = signal<string>('') readonly table = injectTable(() => ({ features, // filteredRowModel and filterFns are registered in features // other options... state: { globalFilter: this.globalFilter(), }, onGlobalFilterChange: (updater) => typeof updater === 'function' ? this.globalFilter.update(updater) : this.globalFilter.set(updater), }))这种模式适合简单集成或从 v8 迁移的代码,但它不如外部 Atom 细粒度。两者的深入对比参见 表格状态指南。
在 UI 中添加全局过滤输入框
TanStack Table不会自动渲染全局过滤输入框,需要手动在 UI 中添加入口。典型做法是在表格上方放一个搜索输入框:用table.atoms.globalFilter.get()响应式读取当前值,用table.setGlobalFilter更新它:
<input type="text" [value]="table.atoms.globalFilter.get() ?? ''" (input)="table.setGlobalFilter($any($event.target).value)" placeholder="Search all columns..." />更完整的实战形态可以参考仓库里的 filters-fuzzy 示例模板:它使用了一个debouncedInput指令对输入做 500ms 防抖,再通过(changeEvent)回调调用table.setGlobalFilter,配合@for渲染表头与行,是全局过滤 + 分页 + 排序组合的完整样板。对应的组件逻辑见 filters-fuzzy 示例入口。
自定义全局过滤函数
如果需要自定义全局过滤逻辑,定义一个过滤函数并传给globalFilterFn选项即可。函数签名接收(row, columnId, filterValue),返回布尔值表示该行是否应保留:
const customFilterFn = (row, columnId, filterValue) => { return // true if the row should be included in the filtered rows } readonly table = injectTable(() => ({ features, // filteredRowModel and filterFns are registered in features // other options... globalFilterFn: customFilterFn, }))[!NOTE] 一个非常流行的做法是用模糊匹配(fuzzy)函数做全局过滤,这已在 模糊过滤指南 中完整讨论。
仓库的 filters-fuzzy 示例 就是现成范例:它借助@tanstack/match-sorter-utils的rankItem实现fuzzyFilter(通过addMeta把RankingInfo写入行的columnFiltersMeta,再返回itemRank.passed),将其注册到filterFns: { fuzzy: fuzzyFilter },并通过globalFilterFn: 'fuzzy'启用;同时用compareItems实现fuzzySort,让命中行按匹配度排序。可对照 模糊过滤指南 阅读其完整代码。
初始全局过滤状态(Initial State)
如果希望在表格初始化时就带有一个全局过滤值,可以把该状态放进initialState选项。但如果你自己掌控了这个切片(外部 Atom 或 Angular signal),就应该在 Atom/signal 上设置起始值,而不是initialState:
readonly table = injectTable(() => ({ features, // filteredRowModel and filterFns are registered in features // other options... initialState: { globalFilter: 'search term', // if not controlling globalFilter state, set initial state here }, }))[!NOTE]不要同时使用
initialState.globalFilter和受控的globalFilter(通过atoms或state),因为受控值会覆盖initialState.globalFilter。
禁用全局过滤
默认情况下,所有列都参与全局过滤。可以通过以下方式控制:
- 列级:在列定义中设置
enableGlobalFilter: false,仅关闭该列的全局过滤; - 表级:在表格选项中设置
enableGlobalFilter: false,关闭所有列的全局过滤; - 总开关:把
enableFilters设为false,同时关闭列过滤与全局过滤。
const columns = [ { header: () => 'Id', accessorKey: 'id', enableGlobalFilter: false, // disable global filtering for this column }, //... ] //... readonly table = injectTable(() => ({ features, // filteredRowModel and filterFns are registered in features // other options... columns, enableGlobalFilter: false, // disable global filtering for all columns }))禁用全局过滤后,该列的column.getCanGlobalFilterAPI 会返回false。
getCanGlobalFilter的判定逻辑
从源码 globalFilteringFeature.ts 与 globalFilteringFeature.utils.ts 可以看到,一个列参与全局过滤需要同时满足以下条件:
- 列定义
enableGlobalFilter默认true(未被列级关闭); - 表格选项
enableGlobalFilter默认true(未被表级关闭); - 表格选项
enableFilters默认true(未被总开关关闭); - 可选的
getColumnCanGlobalFilter回调返回true; - 该列存在
accessorFn(即列可取值)。
默认的getColumnCanGlobalFilter还会检查列首行值是否为string或number类型——这解释了为什么对象值或undefined列默认不参与全局过滤。示例 filters-fuzzy 入口 的注释里也给出了getColumnCanGlobalFilter: column => column.id !== 'status'这种把单列排除在全局过滤之外的用法。
全局过滤 API 速查
以下 API 在接入全局过滤 UI 时最常用:
| API | 作用 |
|---|---|
table.setGlobalFilter | 设置全局过滤值,适合接到搜索输入框的input事件处理器 |
table.resetGlobalFilter | 把全局过滤值重置为初始状态;传true(table.resetGlobalFilter(true))则忽略初始状态、清空为undefined |
table.getGlobalFilterFn | 返回当前实际使用的全局过滤函数(用户自定义或自动解析后的结果) |
table.getGlobalAutoFilterFn | 返回默认的全局过滤函数(目前是includesString) |
column.getCanGlobalFilter | 返回该列是否参与全局过滤,常用于调试哪些列会被搜索 |
其中setGlobalFilter与resetGlobalFilter的实现细节也值得了解:table_setGlobalFilter直接把 updater 交给onGlobalFilterChange(globalFilteringFeature.utils.ts),updater 既可以是新值也可以是"接收旧值返回新值"的函数,Angular 适配器会把它路由到表格自己的状态管理器或你传入的外部 Atom / signal;resetGlobalFilter在默认情况下cloneState(table.initialState.globalFilter),传true则重置为undefined。当globalFilterFn指定了字符串但该函数未注册时,开发环境下会输出globalFilterFn 'xxx' is not registered的警告,便于排查拼写或注册遗漏。
组合建议与调试提示
- 先注册、后引用:
globalFilterFn: 'includesString'这类字符串引用依赖filterFns槽位中已注册同名函数;直接传函数则无需注册,且天然利于 tree-shaking。 - 与分页/排序协作:客户端过滤在行模型管线中位于预过滤行模型之后,过滤结果再交给排序与分页;服务端过滤则要自行处理
globalFilter变化时的数据请求与分页重置(见 客户端 vs 服务端指南)。 - 响应式接入:在 Angular 中凡是读取
table.atoms.globalFilter.get()的模板表达式、computed或effect都会随过滤值自动更新;配合ChangeDetectionStrategy.OnPush与FlexRender指令即可获得高性能的按需渲染。 - 对照示例:完整可运行代码可参考 列过滤示例 与 模糊搜索示例(Angular 版本均位于
examples/angular目录),前者覆盖基础列过滤,后者展示了模糊全局过滤 + 排序 + 分页的完整组合。
【免费下载链接】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),仅供参考