结合 antd 的 select 组件简单封装一个具有全选功能的 select

483 阅读2分钟

需求

在 antd 的 select 多选框中,我们希望增加一个全选的功能,如下:

image.png

目前 antd select 组件并没有帮我们实现这样的功能,需要我们自己去实现。

思路分析

接下来我们分析一下这个功能的几个关键点:

第一个需要注意的点其实也就是:全选和取消全选的基本逻辑

  1. 点击全选 option
    • 全选:全选 option 处于选中状态,其他 option 全部变成未选中状态
    • 取消全选:所有 option 变成未选中状态
  2. 点击其他 option
    • 如果选中数量等于 option 总数,则全选 option 处于选中状态
    • 如果选中数量小于 option 总数,则全选 option 处于未选中状态

第二个需要注意的点是:我们可以完全将全选和取消全选的逻辑封装在组件内部,不让外部感知

第三个需要注意的点是:注意受控组件与非受控组件的兼容处理。

代码实现

代码比较简单,咱们直接上代码:

import { Select as AntdSelect } from 'antd'
import { Children, useEffect, useRef, useState } from 'react'

const MODE = {
  multiple: 'multiple',
  tags: 'tags',
  single: 'single',
}

const allOptionValue = `all-${Math.random()}`
const Option = AntdSelect.Option

const Select = (props) => {
  const {
    mode = MODE.single,
    allLabel = '全部',
    defaultValue = [],
    children,
    ...rest
  } = props

  const [innerValue, setInnerValue] = useState(defaultValue)
  const allOptionsRef = useRef()

  useEffect(() => {
    const allOptions = new Set()
    Children.map(children, (child) => {
      if (child.type === Option) {
        allOptions.add(child.props.value)
      }
    })
    allOptionsRef.current = [...allOptions]
  }, [children?.length])

  // 如果不是多选,那就没有全选可言
  if (mode !== MODE.multiple && mode !== MODE.tags) {
    return <AntdSelect {...props} />
  }

  const onChange = (currentValue = []) => {
    const value = props.value || innerValue
    let preIsAll = value?.length > 0 && value?.length === children?.length
    let newValue = []
    if (preIsAll) {
      if (!currentValue.includes(allOptionValue)) {
        // 说明是点击 全选option,取消全选
        newValue = []
      } else {
        // 说明是点击 非全选option,取消点击的 option,同时取消全选 option
        newValue = currentValue.filter((item) => item !== allOptionValue)
      }
    } else {
      if (currentValue.includes(allOptionValue)) {
        // 说明是点击 全选option,全选
        newValue = allOptionsRef.current
      } else {
        newValue = currentValue
      }
    }
    setInnerValue(newValue)
    props.onChange?.(newValue)
  }

  const getValue = () => {
    let value = props.value || innerValue // 如果上层没有管理value(受控组件),那就用本层管理value(非受控组件)
    return value?.length > 0 && value?.length === children?.length
      ? allOptionValue
      : value
  }

  return (
    <AntdSelect {...rest} mode={mode} value={getValue()} onChange={onChange}>
      <Option value={allOptionValue} key={allOptionValue}>
        {allLabel}
      </Option>
      {children}
    </AntdSelect>
  )
}

Select.Option = Option

export default Select

其实整体的代码还是比较简单的,需要注意的有以下几点:

  1. 通过捕获 option 可以获取全选的所有内容
useEffect(() => {
    const allOptions = new Set()
    Children.map(children, (child) => {
      if (child.type === Option) {
        allOptions.add(child.props.value)
      }
    })
    allOptionsRef.current = [...allOptions]
}, [children?.length])
  1. 通过 getValue 结合 innerValue 来兼容受控组件和非受控组件的情况

使用

就正常去使用即可,在组件外层并不会感知 all option 选项 的存在

import Select from './select.jsx'

export default () => {
  const onChange = (value) => {
    console.log(value)
  }

  return (
    <Select
      defaultValue="lucy"
      mode="multiple"
      style={{ width: 260 }}
      onChange={onChange}
    >
      <Select.Option value="jack">Jack</Select.Option>
      <Select.Option value="lucy">Lucy</Select.Option>
      <Select.Option value="Yiminghe">yiminghe</Select.Option>
    </Select>
  )
}