基于Antd封装的一个联动Select组件LinkedSelect
联动 Select 组件是我们经常在各系统中都会遇到的一个需求,今天做项目,发现该项目中并没有对联动 Select 进行封装抽离,多处都是各自实现,且有大量逻辑与该联动组件耦合,难以抽离。
于是乎,我们就来简单的封装一个联动 Select 吧,目前可能支持的组件不是那么多文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/16213.html
目前封装了 Select,Input,DatePicker, TreeSelect,但是也基本够用了,以后需要用什么就补充什么组件就好了,话不多说 开始!文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/16213.html
首先组件的效果如下图所示,切换第一个 Select,第二个输入框同步变换为对应的输入框。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/16213.html
文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/16213.html
思路说起来比较简单,首先我们既然有两个输入框,那么我们就需要两个 state 来保存他们的值;然后,这两个输入框有哪些选项,该如何配置,则需要一份配置数组,根据这份数组来渲染这两个输入框就可以了。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/16213.html
我们先看看这个配置数组吧:linkedSelectConfig
,目前也就 5 个属性,当然你也可以在这里自定义属性,后面在组件的 props 中获取到就行了。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/16213.html
name:字段名,label: 第一个 select 的选项名,type: 组件类型,typeOption:select 组件的 option 选项数组, defaultValue:默认值
文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/16213.html
yaml
const topOptions = [
{
label: 'TOP10',
value: 10,
},
{
label: 'TOP50',
value: 50,
},
{
label: 'TOP100',
value: 100,
},
{
label: 'TOP200',
value: 200,
},
{
label: 'TOP500',
value: 500,
},
{
label: 'TOP1000',
value: 1000,
},
];
export const linkedSelectConfig = [
{
name: 'limit',
label: 'Selcet选择',
type: 'select',
typeOptions: topOptions,
defaultValue: 10,
},
{
name: 'info',
label: 'Input选择',
type: 'text',
},
{
name: 'time',
label: 'Date选择',
type: 'datePicker',
defaultValue: '2024-05-20',
},
{
name: 'tree',
label: '树形选择',
type: 'treeSelect',
},
];
export const treeData = [
{
title: 'Node1',
value: '0-0',
key: '0-0',
children: [
{
title: 'Child Node1',
value: '0-0-0',
key: '0-0-0',
},
],
},
{
title: 'Node2',
value: '0-1',
key: '0-1',
children: [
{
title: 'Child Node3',
value: '0-1-0',
key: '0-1-0',
},
],
},
];
接下来,我们就来设计这两个输入框的 state 状态,我的思路是,将第一个输入框的 state 设计在组件内部自己控制。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/16213.html
不对外暴露因为使用这个 LinkedSelect 对于第一个 Select 的值不是那么关心,只是需要第二个 select 的值去做一些业务(当然,组件是不停迭代的哈,如果你的业务需要第一个 select 的值,那么模仿改一下就行)。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/16213.html
所以我们将第二个 select 的 state 保存在父组件,通过 props 将 state 和 setState 传递下来,然后在子组件的 onChange 中调用实现子传父。文章源自灵鲨社区-https://www.0s52.com/bcjc/javascriptjc/16213.html
那么 OK,我们来看第二部分组件核心代码,完整代码在最下方,现在分块解释:
这里就是组件的使用方法,我们只需要传入 config 组件配置和 state 及其 set 方法即可,这样在父组件中我们就可以拿这个 state 去完成我们想要的业务咯。
ini
export default () => {
// 这里就相当于父组件,维护这个state,拿去想用的地方使用即可
const [selectData, setSelectData] = useState<any>({ limit: 10 });
return (
<Card style={{ height: 400, display: 'flex', justifyContent: 'center' }}>
<Card style={{ margin: 20 }}>
{`当前选中条件: ${JSON.stringify(selectData)}`}
</Card>
<ZLinkedSelect
config={linkedSelectConfig}
setSelectData={setSelectData}
selectData={selectData}
/>
</Card>
);
};
对于 ZLinkedSelect
组件,我们从 props 中把参数解构出来了,然后定义了第一个 select 的状态,以 config 中第一个配置项字段名的值作为初值;然后,根据这个 state 创建了一个 memo 值,从所有的 config 中筛选出当前选择的 config 对象,也就是根据 name 字段来选出第一个 select 框选择的是哪个选项,并返回其对应的配置,如: {name: 'time', label: 'Date选择', type: 'datePicker', defaultValue: '2024-05-20'}
arduino
const { config, setSelectData, selectData } = props;
const [firstSelectValue, setFirstSelectValue] = useState<any>(
first(config)['name'] || null
);
const itemConfig: any = useMemo(
() => find(config, { name: firstSelectValue }),
[config, firstSelectValue]
);
接下来,我们看一下返回的 DOM 结构吧,
其实非常简单,就是一个 Space 包裹了两个组件,第一个 select 的 value 自然就是 state -> firstSelectValue
,其值就是我我们 config 中 name 字段的值,标志选择的哪个 select。
然后就是 Onchange 方法,每次值发生改变时,更新 firstSelectValue 的状态,同时调用setSelectData 将值传给父组件
,因为第一个 Select 值发生改变,其实对应行为就是选择了不同的选项,所以我们需要改变第二个选项框的初值,以 其 value 为 key,从配置 config 中筛选出对应的 config 并读取其默认值,当然没有设置就是 undefined 哦,比如我们的 Input 和 TreeSelect。
这里需要注意的就是不能直接使用 firstSelectValue 这个状态,因为此时 set 还没有完成,可能有人会说可以使用 useEffect 监听firstSelectValue 改变,拿到最新值后再进行 setSelectData。但如果你阅读过 React 关于 useEffect 的文档你就会知道,这样其实是对 useEffect 的滥用,类似这样的操作更希望从 useEffect 中抽离,在对应的逻辑代码中完成。如果在 useEffect 中监听,则每次firstSelectValue 改变都会引起setSelectData 触发一些不必要更新和页面重新渲染。
当然,在这个例子中你这样使用没有大问题。
第二个输入框就是通过我们的 getItem 方法来完成啦。
ini
return (
<div className="linkedSelect">
<Space>
<Select
style={{ width: 100 }}
value={firstSelectValue}
options={
config?.map((item: any) => ({
label: item?.label || item?.name,
value: item?.name,
})) || []
}
onChange={(value) => {
setFirstSelectValue(value);
// 此时firstValue还未更新完成,所以需要从原始值中再find一遍
setSelectData({
[value]: find(config, { name: value })?.defaultValue,
});
}}
/>
{getItem()}
</Space>
</div>
);
具体的 getItem 方法如下,前面我们筛选出了itemConfig,这里我们根据 type 字段判断选择的是哪个组件,渲染对应的组件就行啦,接下来的无非就是对 Antd 组件的属性进行配置了。
value:很简单,就是组件的值,将selectData 保存的值取出来即可,这里的 key 就是第一个 select 的值,我们用这种方法来区分不同的选项。
options:select 组件的选项,通过itemConfig 读取typeOptions 配置即可
defaultValue: 同样的读取 defaultValue 配置即可,这里要注意的就是数据结构和组件需要的数据结构一致即可(不知道的就在 onChange 中打印组件返回的 value) ,所以这里需要注意的就是 DatePicker 时间组件,他需要的是 moment 对象哈
onChange: 每次组件的数据变化时的回调函数,这里可以去 antd 官网找到他的 API 看他返回的值是什么,console 一下就知道具体的数据结构咯,然后将这个值setSelectData 即可
treeData: TreeSelect 组件配置树形选择需要的数据结构,示例在 Antd 官网都有,当然下面的全部代码中我也附带了一份
ini
const getItem = () => {
switch (itemConfig.type) {
case 'select':
return (
<Select
value={selectData[firstSelectValue]}
style={{ width: 150 }}
options={itemConfig.typeOptions}
defaultValue={itemConfig.defaultValue}
onChange={(value) => {
setSelectData({ [firstSelectValue]: value });
}}
/>
);
case 'datePicker':
return (
<DatePicker
value={moment(selectData[firstSelectValue])}
defaultValue={moment(itemConfig.defaultValue)}
onChange={(date, dateString) => {
console.log('date', dateString);
setSelectData({ [firstSelectValue]: date });
}}
/>
);
case 'treeSelect':
return (
<TreeSelect
value={selectData[firstSelectValue]}
defaultValue={itemConfig.defaultValue}
style={{ width: 250 }}
treeCheckable={true}
treeData={treeData}
onChange={(newValue) => {
console.log('newValue', newValue);
setSelectData({ [firstSelectValue]: newValue });
}}
/>
);
default:
return (
<Input
value={selectData[firstSelectValue]}
defaultValue={itemConfig.defaultValue}
style={{ width: 150 }}
onChange={(e) => {
setSelectData({ [firstSelectValue]: e.target.value });
}}
/>
);
}
};
至此,就全部解释完成了,对每个部分的代码都进行了拆解,需要直接用的可以复制下方的全部代码,当然父组件引用那部分我写在一起了,你需要在你需要的地方引用这个组件,当然你可能需要更改一下 import 的路径。
然后就是 code 中对 TS 的使用比较的 AnyScript,刚刚开始尝试使用,还不是很会,希望路过的大佬,小手一抬,指点一下怎么对这份代码进行一个 Type 化
全量代码:
ZLinkedSelect.tsx
ini
import { Card, Space } from 'antd';
import { first, find } from 'lodash';
import { FC, useState, useMemo } from 'react';
import { Select, Input, DatePicker, TreeSelect } from 'antd';
import { linkedSelectConfig, treeData } from '../utils/LinkedConfig';
import moment from 'moment';
const ZLinkedSelect: FC<any> = (props: any) => {
const { config, setSelectData, selectData } = props;
const [firstSelectValue, setFirstSelectValue] = useState<any>(
first(config)['name'] || null
);
const itemConfig: any = useMemo(
() => find(config, { name: firstSelectValue }),
[config, firstSelectValue]
);
const getItem = () => {
switch (itemConfig.type) {
case 'select':
return (
<Select
value={selectData[firstSelectValue]}
style={{ width: 150 }}
options={itemConfig.typeOptions}
defaultValue={itemConfig.defaultValue}
onChange={(value) => {
setSelectData({ [firstSelectValue]: value });
}}
/>
);
case 'datePicker':
return (
<DatePicker
value={moment(selectData[firstSelectValue])}
defaultValue={moment(itemConfig.defaultValue)}
onChange={(date, dateString) => {
console.log('date', dateString);
setSelectData({ [firstSelectValue]: date });
}}
/>
);
case 'treeSelect':
return (
<TreeSelect
value={selectData[firstSelectValue]}
defaultValue={itemConfig.defaultValue}
style={{ width: 250 }}
treeCheckable={true}
treeData={treeData}
onChange={(newValue) => {
console.log('newValue', newValue);
setSelectData({ [firstSelectValue]: newValue });
}}
/>
);
default:
return (
<Input
value={selectData[firstSelectValue]}
defaultValue={itemConfig.defaultValue}
style={{ width: 150 }}
onChange={(e) => {
setSelectData({ [firstSelectValue]: e.target.value });
}}
/>
);
}
};
return (
<div className="linkedSelect">
<Space>
<Select
style={{ width: 100 }}
value={firstSelectValue}
options={
config?.map((item: any) => ({
label: item?.label || item?.name,
value: item?.name,
})) || []
}
onChange={(value) => {
setFirstSelectValue(value);
// 此时firstValue还未更新完成,所以需要从原始值中再find一遍
setSelectData({
[value]: find(config, { name: value })?.defaultValue,
});
}}
/>
{getItem()}
</Space>
</div>
);
};
export default () => {
// 这里就相当于父组件,维护这个state,拿去想用的地方使用即可
const [selectData, setSelectData] = useState<any>({ limit: 10 });
return (
<Card style={{ height: 400, display: 'flex', justifyContent: 'center' }}>
<Card style={{ margin: 20 }}>
{`当前选中条件: ${JSON.stringify(selectData)}`}
</Card>
<ZLinkedSelect
config={linkedSelectConfig}
setSelectData={setSelectData}
selectData={selectData}
/>
</Card>
);
};
linkedSelectConfig.ts
yaml
const topOptions = [
{
label: 'TOP10',
value: 10,
},
{
label: 'TOP50',
value: 50,
},
{
label: 'TOP100',
value: 100,
},
{
label: 'TOP200',
value: 200,
},
{
label: 'TOP500',
value: 500,
},
{
label: 'TOP1000',
value: 1000,
},
];
export const linkedSelectConfig = [
{
name: 'limit',
label: 'Selcet选择',
type: 'select',
typeOptions: topOptions,
defaultValue: 10,
},
{
name: 'info',
label: 'Input选择',
type: 'text',
},
{
name: 'time',
label: 'Date选择',
type: 'datePicker',
defaultValue: '2024-05-20',
},
{
name: 'tree',
label: '树形选择',
type: 'treeSelect',
},
];
export const treeData = [
{
title: 'Node1',
value: '0-0',
key: '0-0',
children: [
{
title: 'Child Node1',
value: '0-0-0',
key: '0-0-0',
},
],
},
{
title: 'Node2',
value: '0-1',
key: '0-1',
children: [
{
title: 'Child Node3',
value: '0-1-0',
key: '0-1-0',
},
],
},
];
评论