# 学习 antd 级联选择器
# 前言
看了 antd@4.X 的级联选择器源码之后, 发现它的功能经过多次迭代后还是比较复杂的, 我们先学习它的基本功能, 完成一个简易版级联选择器
# css 名
全局会有一个 ConfigContext, 默认设置全局的类名前缀, 我们只需要通过 useContext 获取对应的 prefixCls 变量, 动态拼接类名即可
const { getPrefixCls } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls("cascade");
1
2
2
# 设计&实现
我们简单定义几个 props, 实现简单功能
# Props
prop | 说明 | 类型 |
---|---|---|
children | 用于自定义展示 | ReactNode |
options | 可选项数据源 | Option[] |
fieldNames | 自定义 options 中 label, value, children 的字段 | {label:label, value:value, children:children} |
onChange | 完成选择后的回调 | (value: SingleValueType | SingleValueType[], selectOptions: OptionType[] | OptionType[][]) => void; |
# Option
interface Option {
label: React.ReactNode;
value?: string | number | null;
children?: Option[];
}
1
2
3
4
5
2
3
4
5
# 外部依赖
名称 | 版本 |
---|---|
react | 18.2.0 |
react-dom | 18.2.0 |
rc-select | 14.1.13 |
classNames | 2.3.2 |
# 状态管理
cascade 本身也需要一个状态管理用于维护全局的数据, 我们这里使用 Context, 将 options, fieldNames, onSelect 事件放置于全局状态管理中
const cascadeContext = React.useMemo(
() => ({
options: mergeOptions,
fieldNames: mergeFieldNames,
onSelect: onInternalSelect, // 触发 change 回调
}),
[mergeOptions]
);
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 入口渲染组件
import { BaseSelect } from "rc-select";
<CascadeContext.Provider value={cascadeContext}>
<BaseSelect
ref={ref as any}
prefixCls={prefixCls}
displayValues={[]}
searchValue=""
OptionList={OptionList} // 渲染下拉框的参数
emptyOptions={false}
id=""
onDisplayValuesChange={onDisplayValuesChange}
onSearch={onSearch}
getRawInputElement={() => children}
/>
</CascadeContext.Provider>;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 渲染下拉框
我们接下来主要看下 OptionList 这个参数里面做了什么处理, 也是比较核心的地方
OptionList 里面包含子组件 Column, 也就是每一列的数据
我们准备一个 mock 数据
const addressOptions = [
{
label: "福建",
value: "fj",
children: [
{
label: "福州",
value: "fuzhou",
children: [
{
label: "马尾",
value: "mawei",
},
],
},
{
label: "泉州",
value: "quanzhou",
},
],
},
{
label: "浙江",
value: "zj",
children: [
{
label: "杭州",
value: "hangzhou",
children: [
{
label: "余杭",
value: "yuhang",
},
],
},
],
},
{
label: "北京",
value: "bj",
children: [
{
label: "朝阳区",
value: "chaoyang",
},
{
label: "海淀区",
value: "haidian",
},
],
},
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
初始化只有第一列也就是第一层的数据
const optionColumns = React.useMemo(() => {
const optionList = [{ options: mergeOptions }]; // mergeOptions 就是组件外层传递进来的 options 这里只取第一层 用户默认展示
let currentList = mergeOptions;
// activeValueCells 就是你每次点击用于记录点击路线的
// 循环 activeValueCells 层层往下找到这条点击路线上的每个点击列表, 组合成一个一维数据列表
for (let i = 0; i < activeValueCells.length; i++) {
const activeValueCell = activeValueCells[i];
const currentOption = currentList.find(
(current) => current[fieldNames.value] === activeValueCell
);
const subOptions = currentOption?.[fieldNames.children];
if (!subOptions?.length) {
break;
}
currentList = subOptions;
optionList.push({ options: subOptions });
}
return optionList;
}, [mergeOptions, fieldNames, activeValueCells]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 最终数据如下
const optionColumns = [
{
options: [
{ label: "福建", value: "fj", children: Array(2) },
{ label: "浙江", value: "zj", children: Array(1) },
{ label: "北京", value: "bj", children: Array(2) },
],
},
{
options: [
{ label: "福州", value: "fuzhou", children: Array(1) },
{ label: "泉州", value: "quanzhou" },
],
},
{
options: [{ label: "马尾", value: "mawei" }],
},
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
已经拿到一维数组列表了, 只用循环列表, 每列用 Column 组件进行渲染就能渲染出点击路线上每列的下一个列表了
# 点击
相对应的, 我们需要在 Column 上绑定点击事件, 用于记录点击路线
const columnNodes: React.ReactElement[] = optionColumns.map((col, index) => {
const prevValuePath = activeValueCells.slice(0, index);
const activeValue = activeValueCells[index];
// 记录点击路线
function onPathOpen(newValueCells: React.Key[]) {
setActiveValueCells(newValueCells);
}
function onPathSelect(valuePath: SingleValueType, isLeaf: boolean) {
onSelect(valuePath);
if (isLeaf) {
toggleOpen(false);
}
}
return (
<Column
key={index}
options={col.options}
prevValuePath={prevValuePath} // 每个节点上级路径组成的列表
activeValue={activeValue}
onActive={onPathOpen} // 点击每个卡片都会触发该事件
onSelect={onPathSelect} // 判断如果点击的是叶子节点(没有子节点了) 就触发该方法
prefixCls={prefixCls}
/>
);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 结尾
文章比较简单, 具体实现细节可以查看 jiang-design (opens new window)
线上访问: 地址 (opens new window)
← SSR 服务端渲染 学习 antd 弹窗 →