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

目录

需求

思考

需求拆分

解决


需求

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

【React】手写Antd Select联动效果组件_第1张图片

 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】有省市联动的例子:

【React】手写Antd Select联动效果组件_第2张图片

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

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

【React】手写Antd Select联动效果组件_第3张图片

需求拆分

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 (
  
    {
      optionsArr.map((item, index) => {
        return (
           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 (