Refine v5 Ant Design FilterDropdown 组件全指南:表格列筛选的桥接、映射与源码剖析
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
<FilterDropdown>是@refinedev/antd提供的一个辅助组件,用于打通 Ant Design<Table>列头筛选面板与 Refine 数据层之间的数据同步,并自动生成"过滤 / 清除"操作按钮。本文以该组件为主体,结合仓库源码与测试用例,从基础用法、useSelect集成、mapValue双向映射、rangePickerFilterMapper日期范围处理到getDefaultFilter与syncWithLocation的联动细节,给出可直接复制运行的完整方案。
FilterDropdown 是什么:连接表格筛选面板与 Refine 数据的桥梁
在 Ant Design 的<Table>中,每列可以通过filterDropdown属性自定义筛选面板。<FilterDropdown>正是为这种自定义筛选面板而生的助手组件,它充当桥梁:同步其子组件(如<Select>、<DatePicker>)的输入值与<Table>的筛选值。
最基础的用法如下(完整示例位于examples/table-antd-use-table):
import { List, FilterDropdown, useTable, } from "@refinedev/antd"; import { Table, Select } from "antd"; const PostList: React.FC = (props) => { const { tableProps } = useTable<IPost>(); return ( <List> <Table {...tableProps} rowKey="id"> <Table.Column dataIndex="id" title="ID" /> <Table.Column dataIndex={["category", "id"]} title="Category" key="category.id" filterDropdown={(props) => ( <FilterDropdown {...props}> <Select mode="multiple" placeholder="Select Category" options={[ { label: "Ergonomic", value: "1" }, { label: "Island", value: "2" }, ]} /> </FilterDropdown> )} /> </Table> </List> ); }; interface IPost { id: number; category: { id: number; }; }从下拉框中选择分类后,分类的id会作为筛选值发送给<Table>,Refine 会在底层自动完成数据更新——你不需要手动管理筛选状态与请求参数。
此处为了简洁,分类选项是手动写死的。实际上更推荐配合
useSelectHook 动态填充,下文会专门讲解。
自动生成的操作按钮:源码中的 Filter 与 Clear
<FilterDropdown>会自动渲染两个按钮,分别执行"过滤"与"清除过滤"操作。查看其源码(packages/antd/src/components/table/components/filterDropdown/index.tsx)可以看到按钮区域的实际实现:
return ( <div style={{ padding: 10, display: "flex", flexDirection: "column", alignItems: "flex-end", }} > <div style={{ marginBottom: 15 }}>{childrenWithProps}</div> <Space> <Button type="primary" size="small" onClick={() => onFilter()}> <FilterOutlined /> {translate("buttons.filter", "Filter")} </Button> <Button danger size="small" onClick={() => clearFilter()}> {translate("buttons.clear", "Clear")} </Button> </Space> </div> );关键行为(均有测试用例在 filterDropdown/index.spec.tsx 中验证):
- Filter 按钮:触发
onFilter(),内部会将selectedKeys做归一化处理——数字会被转为字符串,dayjs对象会被转为 ISO 字符串数组,随后调用setSelectedKeys与confirm?.()关闭筛选面板并应用筛选; - Clear 按钮:触发
clearFilter(),调用 Ant Design 传入的clearFilters回调清空筛选; - 按钮文案:通过
useTranslate读取 i18n 翻译键buttons.filter与buttons.clear,未配置时回退为 "Filter" / "Clear"。
组件还会通过React.cloneElement向子元素注入onChange与value两个受控属性(源码 L75-L83),因此任意支持受控模式的输入组件都可以直接作为其子节点使用。
结合 useSelect 动态填充下拉选项
为了让分类选项来源于真实数据,可以把useSelect返回的selectProps展开到<Select>上。useSelect的详细说明可参考 useSelect Hook 文档:
const { selectProps: categorySelectProps } = useSelect<ICategory>({ resource: "categories", optionLabel: "title", optionValue: "id", }); <Select {...categorySelectProps} />;配合mapValue与getDefaultFilter,还能让筛选面板在初始化时正确回显已有的筛选值,并处理数字类型映射,完整代码如下:
import { getDefaultFilter } from "@refinedev/antd"; import { useTable, FilterDropdown, useSelect } from "@refinedev/antd"; import { Table, Select } from "antd"; const { tableProps, filters } = useTable<IPost>({ filters: { initial: [ { field: "category.id", value: [1, 2], operator: "in", }, ], }, }); const { selectProps: categorySelectProps } = useSelect<ICategory>({ resource: "categories", optionLabel: "title", optionValue: "id", defaultValue: getDefaultFilter("category.id", filters, "in"), }); <Table> <Table.Column dataIndex="id" title="ID" /> <Table.Column dataIndex={["category", "id"]} title="Category" key="category.id" filterDropdown={(props) => ( <FilterDropdown {...props} mapValue={(selectedKeys) => selectedKeys.map((i) => parseInt(i.toString())) } > <Select style={{ minWidth: 200 }} mode="multiple" placeholder="Select Category" {...categorySelectProps} /> </FilterDropdown> )} defaultFilteredValue={getDefaultFilter("category.id", filters, "in")} /> </Table>;mapValue:两种数据格式之间的双向映射
mapValue是FilterDropdown提供的一个核心工具函数,用于根据事件类型转换selectedKeys的格式。其类型签名如下:
function mapValue(selectedKeys: React.Key[], event: "onChange" | "value"): any;selectedKeys:下拉框中当前选中的键集合;event:触发mapValue的事件类型:"onChange":下拉框值发生变化时触发,用于把值映射成 Refine 期望的格式(供 data provider、syncWithLocation等使用);"value":需要把值映射给子组件时触发。
典型的场景是配合useSelect使用<Select />时,把值映射为number类型。从源码(filterDropdown/index.tsx L55-L83)可以看到两个事件的分工:
- 子组件触发
onChange时,组件会把变更值交给mapValue(e, "onChange")处理后写入setSelectedKeys; - 渲染子组件时,通过
cloneElement把value: mapValue(selectedKeys, "value")注入给子组件,保证面板回显。
源码中mapValue的默认实现是恒等函数(value) => value,即不传时原样透传。测试用例 filterDropdown/index.spec.tsx L156-L198 验证了onChange与value两个事件分别会得到不同的映射结果,并分别作用于筛选状态与输入框回显。
rangePickerFilterMapper:日期范围筛选的完整解决方案
当筛选条件是日期范围时,会遇到一个典型矛盾:Refine 的 data provider 期望 ISO 8601 字符串格式,而 Ant Design 的<DatePicker.RangePicker />内部使用的是 Dayjs 对象。rangePickerFilterMapper正是为此提供的官方映射工具。
完整示例(过滤created_at在 2022-01-01 至 2022-01-31 之间的记录):
import { getDefaultFilter } from "@refinedev/antd"; import { DateField, FilterDropdown, rangePickerFilterMapper, useTable, } from "@refinedev/antd"; import { Table, DatePicker } from "antd"; export const Posts = () => { const { tableProps, filters } = useTable({ filters: { initial: [ { field: "created_at", value: ["2022-01-01", "2022-01-31"], operator: "between", }, ], }, }); return ( <Table {...tableProps} rowKey="id"> <Table.Column dataIndex="id" title="ID" /> <Table.Column dataIndex="title" title="Title" /> <Table.Column dataIndex="createdAt" title="Created At" filterDropdown={(props) => ( <FilterDropdown {...props} mapValue={(selectedKeys, event) => { return rangePickerFilterMapper(selectedKeys, event); }} > <DatePicker.RangePicker /> </FilterDropdown> )} defaultFilteredValue={getDefaultFilter( "created_at", filters, "between", )} /> </Table> ); };源码剖析:双方向转换逻辑
rangePickerFilterMapper的实现位于 packages/antd/src/definitions/filter-mappers/index.ts:
import type { FilterDropdownProps, MapValueEvent, } from "@components/table/components"; import dayjs from "dayjs"; export const rangePickerFilterMapper = ( selectedKeys: FilterDropdownProps["selectedKeys"], event: MapValueEvent, ) => { if (!selectedKeys) { return selectedKeys; } if (event === "value") { return selectedKeys.map((key) => { if (typeof key === "string") { return dayjs(key); } return key; }); } if (event === "onChange") { if (selectedKeys.every(dayjs.isDayjs)) { return selectedKeys.map((date: any) => dayjs(date).toISOString()); } } return selectedKeys; };其行为完全对应两个事件方向:
event === "value":把字符串形式的日期转换为 Dayjs 对象,供<DatePicker.RangePicker />正确回显(如果已经是 Dayjs 对象则原样返回);event === "onChange":当所有值都是 Dayjs 对象时,统一转换为 ISO 8601 字符串,交给 Refine 的 data provider /syncWithLocation使用;- 兜底逻辑:
selectedKeys为空、事件类型无效或值类型不符合预期时,原样返回,保证组件在异常输入下不崩溃。
该行为在 filter-mappers/index.spec.ts 中有完整覆盖,包括:空值原样返回、无效事件类型原样返回、字符串日期转 Dayjs、Dayjs 转 ISO 字符串等 6 组断言。其中测试注释特别说明:虽然 Ant Design 的类型定义把selectedKeys声明为React.Key[],但实际运行时它可以是任意类型(包括 Dayjs 元组),因此测试使用了as any——这也解释了为什么mapValue的返回类型被设计为宽泛的any。
getDefaultFilter 与 syncWithLocation 的联动注意点
在筛选面板的初始化回显中,getDefaultFilter扮演关键角色,但有两个容易踩坑的细节值得注意。
从 URL 解析出的筛选值一定是字符串
如果启用了syncWithLocation,页面刷新时筛选值会从 URL 查询参数中解析出来,因此类型一定是string。这可能会与来自 API 的非字符串类型筛选数据产生不兼容——例如分类id在数据库中可能是number,而从 URL 恢复后变成了"1"。此时就需要mapValue配合类型转换(如parseInt)来消解差异。
关于syncWithLocation的行为与默认值(默认关闭,需显式开启),可参考 Refine 核心组件文档。
getDefaultFilter 的取值逻辑
getDefaultFilter用于从给定的filters中按字段与操作符取出默认筛选值。在上面的示例中,传给它的filters来自useTable的返回值,其中已包含从 URL 解析出的筛选值,因此defaultFilteredValue与useSelect的defaultValue能够正确回显当前筛选状态。
仓库中@refinedev/antd对它的实现(packages/antd/src/definitions/table/index.ts L35-L41)是直接委托给@refinedev/core的同名函数,并提供了operatorType参数(默认"eq"):
export const getDefaultFilter = ( columnName: string, filters?: CrudFilters, operatorType: CrudOperators = "eq", ): CrudFilter["value"] | undefined => { return getDefaultFilterCore(columnName, filters, operatorType); };因此在多操作符(如同时存在eq、in、between)的筛选场景中,务必像示例那样显式传入正确的操作符("in"、"between"),否则会因默认"eq"而取不到值。
属性一览
<FilterDropdown>的完整属性定义见 packages/antd/src/components/table/components/filterDropdown/index.tsx L13-L16,核心属性如下:
| 属性 | 类型 | 说明 |
|---|---|---|
selectedKeys | React.Key[] | 当前选中的键,来自<Table.Column>的filterDropdownprop |
setSelectedKeys | (keys: any) => void | 更新选中键的回调,同样来自filterDropdownprop |
confirm | () => void | 应用筛选并关闭面板的回调,由 Ant Design 注入 |
clearFilters | () => void | 清除筛选的回调,由 Ant Design 注入 |
mapValue | (selectedKeys: React.Key[], event: "onChange" \| "value") => any | 可选,双向格式映射函数,默认恒等返回 |
children | React.JSX.Element | 筛选输入组件,会由内部cloneElement注入onChange与value |
其中selectedKeys、setSelectedKeys、confirm、clearFilters四个属性必须从<Table.Column>的filterDropdownprop 透传(即示例中的{...props}),它们是 Ant Design 表格与筛选面板通信的契约。
测试用例如何验证组件行为
如果希望深入理解组件行为,仓库中的测试是很好的教材:
- filterDropdown/index.spec.tsx:验证渲染出 "Filter"/"Clear" 按钮、点击 Filter 触发
confirm与setSelectedKeys、点击 Clear 触发clearFilters、mapValue在输入变化与回显两个方向分别生效,以及Input、Select、DatePicker三种输入控件的组合场景; - filter-mappers/index.spec.ts:针对
rangePickerFilterMapper的 6 组边界断言。
完整可运行示例
组件文档配套的完整示例为table-antd-use-table,对应仓库目录 examples/table-antd-use-table,其中包含上述全部用法的可运行代码(App.tsx中的列表页与筛选逻辑),可以在本地按示例目录的package.json脚本启动查看实际效果。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考