业务场景
某些时候,我们需要将筛选条件(也就是筛选表单的表单值)同步记录在 url 上。可能你目前没有遇到,但是我相信你将来一定会遇到。什么时候有这种需求呢?常见的有下面的两种业务场景需要这种特性:
- 从列表页跳转到详情页,返回之后,我还是想看到之前的筛选结果;
- 把查询结果分享给第三方,被分享者一访问页面就能看到相应的筛选结果;
从列表页跳转到详情页,返回之后,我还是想看到之前的筛选结果
也许你会说,我详情页不做成路由不就行了吗?确实可以。但是你想想,假如你的详情页的内容很多,这个时候你还是用 modal 或者 drawer 去展示的话,那么,我相信产品经理或者 UI 设计师是不会同意的。因而,详情页用一个单独的路由去承载是一件避免不了的事。
那么问题就来了。假如我的筛选条件很多,我辛辛苦苦填完这个筛选条件后,点击查询按钮,查到了我想看的条目后,点击某个条目的详情。看完该条目的详情,我本意是想接着看其他的条目的详情的。结果发现,从详情页返回列表页后,我的筛选条件不见了。这个时候,产品经理或者 UI 设计师肯定会眉头紧皱,叹口气,摇摇头。最后,他们会找到你说,「从详情页返回到列表页后,能不能记住上一次的查询结果啊」?
把查询结果分享给第三方,被分享者一访问页面就能看到相应的筛选结果
某些业务场景下,可能需要前端去实现「查询结果可分享」的这种诉求的。那么这种情况下,查询条件必须要带到 url 上了。显而易见,这是没有什么选择的余地。
特性诉求
以上举例的业务场景对 <ProTable> 组件的特性诉求就是:「如何实现筛选条件跟 url 参数双向同步?」
特性实现
乍地一看,实现这个特性诉求似乎很常见。于是乎,我们到 ProTable 的官方文档上面一查,结果发现没有直接的介绍(其实,它是放在 ProForm那一节介绍了)。可能最终,我们通过不断地地毯式的搜索或者直接问 AI 大模型,最终找到了相应的配置字段:
<ProTable
...
form={{
syncToUrl: true,
}}
/>
你以为这样就完啦?不,当你执行下面的操作步骤后,你会发现问题:
- 录入筛选条件后,点击「查询」按钮;
- 查询成功后,
<ProTable>组件会把筛选条件以 url query 参数的形式同步到 url 上; - 然后,你刷新页面,模拟分享出去的页面被访问的场景
- 最后,你再点击「重置」按钮。
这个时候,就会发现了问题了:“重置功能失效了”。也就是说,筛选表单并没有被清空。
以上结论是基于这么一个基本事实:“99% 的用户对「重置功能」的预期肯定是「清空表单」,而不是将筛选表单重置到之前的某个状态”。
这种表现基本上等同于 bug 级别了。之前我使用了syncToUrl: true 这个配置后,被测试反馈了这个问题,第一直觉是这是 <ProTable> 一个bug。因为时间紧迫,当时的解决方案是弃用这个特性。
后面,随着对 pro-component 源码的深入,我发现,其实他们是提供了这个配置项去满足这个需求的。那就是 syncToInitialValues: false。源码里面对这个配置属性的注释是:
同步结果到 initialValues,默认为true。如果为false,reset的时将会忽略从url上获取的数据
这个字段的中文注释真的让人看不太懂。啥叫「同步xxx到initialValues」啊?按理说,initialValues的设置只有一次,为啥还有「同步」的说法。经过探索,我发现这个字段的用途应该是这么描述:
是否允许 url 上的查询参数参与到 initialValues 的计算(resolve)逻辑里面去。
那么 syncToInitialValues: false 的意思就是说「不允许 url 上的查询参数参与到 initialValues 的计算逻辑里面去」。那么,如果你在别的地方(表单层面或者单个字段层面)没有对 initialValues 进行设置,那么筛选表单的 initialValues 就是为空。在这种情况下,如果用户再点击「重置」按钮,那么就实现了「表单清空」的效果。
有细心的读者可能会发现,上面的阐述中,我们作了前置假设:
「如果你在别的地方(表单层面或者单个字段层面)没有对 initialValues 进行设置」。
为什么呢?那是因为 <ProTable> 内置的对「重置」功能的实现所决定的。「重置」按钮的 click handler 面核心的方法是 - form.resetFields()
// /packages/form/src/components/Submitter/index.tsx
import { proTheme, useIntl } from '@ant-design/pro-provider';
import type { ButtonProps } from 'antd';
import { Button, Form } from 'antd';
import omit from 'omit.js';
import React from 'react';
...
const Submitter: React.FC<SubmitterProps> = (props) => {
...
const reset = () => {
form.resetFields();
onReset?.();
};
...
if (resetButtonProps !== false) {
dom.push(
<Button
{...omit(resetButtonProps, ['preventDefault'] as any)}
key="rest"
onClick={(e) => {
if (!resetButtonProps?.preventDefault) reset();
resetButtonProps?.onClick?.(
e as React.MouseEvent<HTMLButtonElement, MouseEvent>,
);
}}
>
{resetText}
</Button>,
);
}
...
};
export default Submitter;
如你所见,内置的处理是,先调用 antd form 组件的 form.resetFields()。antd form 组件的文档明确指出,这个方法只是将 form 重置到它的 initialValues 状态。
所以,对 antd 生态里面这个 form 的 「重置功能」的正确理解是:「它是重置表单值回到它的 initialValues 而已,并不是实现我们所期待的“清空表单”」。
好了,到目前为止,我们得到了实现我们特性的配置组合,它就是:
<ProTable
...
form={{
syncToUrl: true,
syncToInitialValues: false
}}
/>
这个完事了吗?不,还没有!
还有一个细节我们要补上。这个细节是特别针对时间区间类型的字段的。举例说明,假设我们有这样的列表页:
这个列表页的筛选表单有一个时间区间类的字段:「创建时间」。因为时间区间类的字段最终是用antd 的时间类的原子组件来呈现的,这些组件原始值是一个具有两个元素的数组。而官方文档明确指出,url 的 query 参数值只能是字符串:
因此,这里就要涉及到 url 跟筛选表单双向同步的时候做转换处理:
- 表单 -> url - 从筛选表单同步到 url 的时候,要拆分为两个字段;
- url -> 表单 - 从 url 回显到筛选表单的时候,要把之前拆分的两个字段组合成一个数组。
从筛选表单同步到 url 的时候,要拆分为两个字段
要实现这个,我们在配置 column 的时候,<ProTable> 提供了一个 search 字段给我们去声明该怎么拆分:
const columns: ProColumns<GithubIssueItem>[] = [
{
title: "创建时间",
dataIndex: "created_at",
valueType: "dateRange",
hideInTable: true,
search: {
transform: (value) => {
return {
startTime: value?.[0],
endTime: value?.[1],
};
},
}
},
]
这里,我们声明了,我们要把 created_at 字段的值拆分为两个字段:startTime 和 endTime。startTime的值取数组的第一个元素值,endTime 取endTime的值取数组的第二个元素值。
从 url 回显到筛选表单的时候,要把之前拆分的两个字段组合成一个数组
这样做的目的是应底层原子组件对 value 的格式要求 - antd 时间区间类型的原子组件的值是一个具有两个元素的数组。我们要把它拼成人家所要求的格式,然后才能喂给它。这里还是利用回上面我提到的那个配置项:syncToUrl。只不过,要实现数据的重组,syncToUrl的值是一个函数了,而不是 boolean 值:
<ProTable
...
form={{
syncToUrl: (values, type) => {
if (type === "get") {
return {
...values,
created_at: [values.startTime, values.endTime],
};
}
return values;
}
}}
/>
在这里,values 指代的是解析 url 参数所得到的结果。而 type === "get" 对应的就是「从 url 解析得到表单值回显到筛选表单」的场景。
如果只是考虑「表单到url」场景下的字段拆分,其实我们可以不把字段拆分的声明写在 column 字段的配置那里,而是一并写在了在 syncToUrl 函数这里。因为,在 syncToUrl 函数还会被应用到「把筛选表单同步到 url」的场景,也就是 type === "set" 的分支情况。
所以,如果单单考虑「表单 -> url」场景下的字段拆分,那么我们还可以这么写:
<ProTable
...
form={{
syncToUrl: (values, type) => {
// 对应的是「url -> 表单」的同步
if (type === "get") {
return {
...values,
created_at: [values.startTime, values.endTime],
};
}else { // 对应的是「表单 -> url」的同步
if (Array.isArray(values.created_at)) {
const [startTime, endTime] = values.created_at;
delete values.created_at;
return {
...values,
startTime,
endTime,
};
}
return values;
}
}
}}
/>
但是这种写法有一种弊端,在内部实现中,针对这种写法,<ProTable> 并不会帮我们同步到 request 请求所用到的 params 里面去,比如,你在 request 里面做个打印,你会看到上面的这种分解声明并不会同步到request 的请求参数里面去:
<ProTable
...
request={async (params, sort, filter) => {
// params 里面,created_at并没有被拆解为 startTime 和 endTime 两个字段
console.log("🚀 ~ request={ ~ params:", params);
...
}}
/>
不过,通过这个探索,我们也知道了 syncToUrl 函数只会被应用到 url 跟筛选表单双向同步的场景。
总结
综上所述,要想实现「筛选条件跟 url 参数双向同步」特性以及需要连带处理的两个细节:
- 将「重置」按钮实现为「表单清空」
- 对于时间区间类型的字段进行拆分和重组
那么,我们就需要三个配置字段:
syncToUrl- 如果筛选表单没有需要拆分的字段,那么直接设置为true即可;否则的话,那就是配置为一个函数,然后在type === "get"分支情况下去声明如何将拆分过的字段重组起来,喂给底层的原子组件;search.transform- 在特定的字段的 column 配置中,声明一个transform函数,用于声明这个字段会被拆分为哪几个字段;syncToInitialValues- 把这个字段配置为false,用于解决 antd 对 「重置」功能的默认实现(重置到 form 的 initialValues)所带来的 bug 幻觉。在特定的前置条件下(没有在表单或者字段层面去设置初始值),实现人类直觉的「表单清空」功能。
最后,附上一个 playground 项目,大家可以上去看看,玩一玩~