基于Antd封装的一个联动Select组件LinkedSelect

零 JavaScript教程评论92字数 8047阅读26分49秒阅读模式

基于Antd封装的一个联动Select组件LinkedSelect

基于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

下载 (2).gif文章源自灵鲨社区-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',
      },
    ],
  },
];

零
  • 转载请务必保留本文链接:https://www.0s52.com/bcjc/javascriptjc/16213.html
    本社区资源仅供用于学习和交流,请勿用于商业用途
    未经允许不得进行转载/复制/分享

发表评论