【React】手写Antd Select联动效果组件

目录

需求

思考

需求拆分

解决


需求

1.联动select,而不是像Cascader那样【级联选择 Cascader - Ant Design】。

 2.传入对应options生成对应的联动select。

options示例数据

const options = [
  {
    "label": "病区A",
    "value": "bingquA",
    "children": [{
      "label": "科室A-A",
      "value": "keshiAA"
    }, {
      "label": "科室A-B",
      "value": "keshiAB"
    }, {
      "label": "科室A-C",
      "value": "keshiAC"
    }]
  },
  {
    "label": "病区B",
    "value": "bingquB",
    "children": [{
      "label": "科室B-A",
      "value": "keshiBA"
    }, {
      "label": "科室B-B",
      "value": "keshiBB"
    }, {
      "label": "科室B-C",
      "value": "keshiBC"
    }]
  }
]

3.当组件用Form.Item包裹,在表单提交时也可以获取到所有select组件的value。

4.开发者能高效地定制化每一个下拉框地样式

思考

antd中的select中【选择器 Select - Ant Design】有省市联动的例子:

但是这个联动的例子是写死的,无法通过配置的形式直接生成(即传入后台请求回来的需要联动的数据直接生成两个甚至三个四个联动的select),组件复用性不强,并且当表单提交的时候无法获取到两个select组件的value。

参考antd的Cascader【级联选择 Cascader - Ant Design】达到了我的需求,但是样式不同

需求拆分

1.如何使得select1 selected的改变控制select2的options select2控制select3...... ?
2.如何处理后台返回的数据?
3.如何在表单提交时也可以获取到所有select组件的value?

4.如何制定一种配置规范方便开发者配置每一个下拉框的样式呢?

解决

1. 生成如下的数据结构,使得除了第一个select,其他的select依赖上一个select选中的value

const optionsArr = [{
  "default": [{
    "label": "病区A",
    "value": "bingquA"
  }, {
    "label": "病区B",
    "value": "bingquB"
  }]
}, {
  "bingquA": [{
    "label": "科室A-A",
    "value": "keshiAA"
  }, {
    "label": "科室A-B",
    "value": "keshiAB"
  }, {
    "label": "科室A-C",
    "value": "keshiAC"
  }],
  "bingquB": [{
    "label": "科室B-A",
    "value": "keshiBA"
  }, {
    "label": "科室B-B",
    "value": "keshiBB"
  }, {
    "label": "科室B-C",
    "value": "keshiBC"
  }]
}]

 看代码

// 存放所有下拉框的options
const [optionsArr, setOptionsArr] = useState([]) 
// 存放所有已经选中的下拉框value
const [selectedArr, setSelectedArr] = useState([])

// 第一个下拉框没有父级value,默认值default
const DEFAULT_VALUE = "default"

return (
  <Fragment>
    {
      optionsArr.map((item, index) => {
        return (
          <Select
            value={selectedArr[index]}
            onChange={(value) => handleChange(value, index)}
            // 除了第一个select外,其他的select依赖上一个select选中的value
            options={index === 0 ? item[DEFAULT_VALUE] : item[selectedArr[index - 1]]}
          />
        )
      })
    }
  </Fragment>
)

2.处理后台返回的数据

// 根据props中的options抽离出所有下拉框的options
const getOptionsArr = (options, deep, optionsArr, fatherValue) => {
  // 根据deep动态决定要生成多少个下拉框
  if (optionsArr.length <= deep) optionsArr.push({})

  // wrapperArr中存储了 上一个select选中的value是fatherValue时,当前这个select的options
  const wrapperArr = []

  options.forEach(item => {
    const obj = {label: item.label, value: item.value}
    wrapperArr.push(obj)

    // 如果有children属性,则递归调用
    if (item.children) {
      // 当前的value即是children的fatherValue
      getOptionsArr(item.children, deep + 1, optionsArr, item.value)
    }
  })

  // optionsArr数组中的第deep个(从0开始)对象中 key为fatherValue,value是wrapperArr
  optionsArr[deep][fatherValue] = wrapperArr
}

3.从props中获取Form.Item中的提供的onChange方法,当下拉框选项改变的时候调用

// 下拉框被改变
const handleChange = (val, index) => {
  const originArr = [...selectedArr]
  originArr[index] = val
  // 当父级下拉框被改变,子孙级下拉框选中的值需要清空
  for (let i = index + 1; i < originArr.length; i++) originArr[i] = undefined
  setSelectedArr(originArr)
  // 如果外面有用Form.Item包裹则调用传来的onChange方法,使得在表单提交的时候能获取到这个Form.Item的值
  onChange && onChange(originArr)
}

4.传入如下配置信息

props: {
  placeholders: ["病区", "科室"],
  allowClear: true,
  style: {
    widths: ["140px", "200px"],
    marginRight: '10px'
  }
}

代表第一个下拉框默认值为”病区“,宽度为140px,第二个下拉框默认值为”科室“,宽度为200px,

它们都允许清除清除,右外边距为10px

对配置信息的处理函数如下

// 根据配置信息config与对应下标index获取select的配置信息
// 比如config为style,如果是正常的属性例如width则直接用,如果是widths=[100, 110, 120]则第一个下拉框width=100第二个110
const getConfigProps = (config, index = 0) => {
  return Object.keys(config).reduce((prev, cur) => {
    if (cur.match(/[s]$/) && config[cur] instanceof Array) { // key以s结尾 value是数组
      const key = cur.substring(0, cur.length - 1) // 获得真实的prop

      if (config[cur].length <= index) { // 错误处理
        console.error(`联动下拉框组件,参数配置有误 ,${cur}参数个数小于生成的下拉框个数,当前索引为${index}~`)
      } else {
        prev[key] = config[cur][index]
      }

    } else {
      prev[cur] = config[cur]
    }

    return prev
  }, {})
}
  return (
    <>
      {
        optionsArr.map((item, index) => {
          return (
            <Select
              key={index}
              value={selectedArr[index]}
              onChange={(value) => handleChange(value, index)}
              // 除了第一个select外,其他的select依赖上一个select选中的value
              options={index === 0 ? item[DEFAULT_VALUE] : item[selectedArr[index - 1]]}
              // placeholders包含所有select的默认值
              placeholder={otherProps.placeholders && otherProps.placeholders[index]}
              // otherProps包含select的所有其他配置信息
              {...getConfigProps(otherProps, index)}
              // 低效的写法(对比):style={{...otherProps.style, width: (otherProps.widths) ? (otherProps.widths && otherProps.widths[index]) : otherProps.style.width }}
              style={{...getConfigProps(otherProps.style, index)}}
            />
          )
        })
      }
    </>
  )

总体代码

import React, { Fragment, memo, useState, useEffect } from "react"

import { Select } from "antd"

const ZCSelectCascader = (props) => {
  // onChange和value是由Form.Item提供的,options是一个包含{label,value,children}的数组,otherProps获取用户传入的其他配置信息
  const { options, onChange, value, ...otherProps } = props

  // 存放所有下拉框的options
  const [optionsArr, setOptionsArr] = useState([])
  // 存放所有已经选中的下拉框value
  const [selectedArr, setSelectedArr] = useState([])

  // 第一个下拉框没有父级value,默认值default
  const DEFAULT_VALUE = "default"

  // 设置Form.Item的initialValue或Form的initialValues会改变value,select的value实际受控于selectedArr,因此要setSelectedArr(value)
  useEffect(() => {
    setSelectedArr(value)
  }, [value])

  // 根据props中的options抽离出所有下拉框的options
  const getOptionsArr = (options, deep, optionsArr, fatherValue) => {
    // 根据deep动态决定要生成多少个下拉框
    if (optionsArr.length <= deep) optionsArr.push({})

    // wrapperArr中存储了 上一个select选中的value是fatherValue时,当前这个select的options
    const wrapperArr = []

    options.forEach(item => {
      const obj = {label: item.label, value: item.value}
      wrapperArr.push(obj)

      // 如果有children属性,则递归调用
      if (item.children) {
        // 当前的value即是children的fatherValue
        getOptionsArr(item.children, deep + 1, optionsArr, item.value)
      }
    })

    // optionsArr数组中的第deep个(从0开始)对象中 key为fatherValue,value是wrapperArr
    optionsArr[deep][fatherValue] = wrapperArr
  }

  // 下拉框被改变
  const handleChange = (val, index) => {
    const originArr = [...selectedArr]
    originArr[index] = val
    // 当父级下拉框被改变,子孙级下拉框选中的值需要清空
    for (let i = index + 1; i < originArr.length; i++) originArr[i] = undefined
    setSelectedArr(originArr)
    // 如果外面有用Form.Item包裹则调用传来的onChange方法,使得在表单提交的时候能获取到这个Form.Item的值
    onChange && onChange(originArr)
  }

  useEffect(() => {
    // options是通过发送网络请求获取的,一开始可能传的是空值
    if (options) {
      const originArr = []
      // 调用该方法生成符合要求的数组
      getOptionsArr(options, 0, originArr, DEFAULT_VALUE)
      setOptionsArr(originArr)

      // 初始化selectedArr,数组的长度和optionsArr一致
      setSelectedArr(new Array(originArr.length).fill())
    }
  }, [options])

  // 根据配置信息config与对应下标index获取select的配置信息
  // 比如config为style,如果是正常的属性例如width则直接用,如果是widths=[100, 110, 120]则第一个下拉框width=100第二个110
  const getConfigProps = (config, index = 0) => {
    return Object.keys(config).reduce((prev, cur) => {
      if (cur.match(/[s]$/) && config[cur] instanceof Array) { // key以s结尾 value是数组
        const key = cur.substring(0, cur.length - 1) // 获得真实的prop

        if (config[cur].length <= index) { // 错误处理
          console.error(`联动下拉框组件,参数配置有误 ,${cur}参数个数小于生成的下拉框个数,当前索引为${index}~`)
        } else {
          prev[key] = config[cur][index]
        }

      } else {
        prev[cur] = config[cur]
      }

      return prev
    }, {})
  }

  return (
    <>
      {
        optionsArr.map((item, index) => {
          return (
            <Select
              key={index}
              value={selectedArr[index]}
              onChange={(value) => handleChange(value, index)}
              // 除了第一个select外,其他的select依赖上一个select选中的value
              options={index === 0 ? item[DEFAULT_VALUE] : item[selectedArr[index - 1]]}
              // placeholders包含所有select的默认值
              placeholder={otherProps.placeholders && otherProps.placeholders[index]}
              // otherProps包含select的所有其他配置信息
              {...getConfigProps(otherProps, index)}
              // 低效的写法(对比):style={{...otherProps.style, width: (otherProps.widths) ? (otherProps.widths && otherProps.widths[index]) : otherProps.style.width }}
              style={{...getConfigProps(otherProps.style, index)}}
            />
          )
        })
      }
    </>
  )
}

export default memo(ZCSelectCascader)

调用该组件的示例代码

import React from 'react'
import SelectCascader from '../../components/ZCAntdComponents/SelectCascader'

const options = [
  {
    "label": "病区A",
    "value": "bingquA",
    "children": [{
      "label": "科室A-A",
      "value": "keshiAA"
    }, {
      "label": "科室A-B",
      "value": "keshiAB"
    }, {
      "label": "科室A-C",
      "value": "keshiAC"
    }]
  },
  {
    "label": "病区B",
    "value": "bingquB",
    "children": [{
      "label": "科室B-A",
      "value": "keshiBA"
    }, {
      "label": "科室B-B",
      "value": "keshiBB"
    }, {
      "label": "科室B-C",
      "value": "keshiBC"
    }]
  }
]

const props = {
  placeholders: ["病区", "科室"],
  allowClear: true,
  options,
  style: {
    widths: ["140px", "200px"],
    marginRight: '10px'
  }
}

export default function Test() {
  return <SelectCascader {...props} />
}

示例代码的效果

 

欢迎在评论区提问或指正错误!

如果觉得本文对您有帮助,点个赞支持一下再走吧,感谢!

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值