Ant Design 的组件库的 Select 组件我们经常用,但是你有没有想过,Select 是如何实现的呢?
本文中,我将一步一步实现 Select,通过实践和思考,学一种新的组件设计模式:组合模式。
功能分析
首先,我们实现最基础的功能。
1.引入 Select 后,从 Select 中结构出 Option。
2.写入多个 Option 后,点击输入框正常展示所有 Option; 再次点击输入框后,Option 包裹框关闭。
3.点击 Option 内容,输入框展示对应的 Option, Option 包裹框关闭。
常规实现
我做了一个简易版的,效果如下:
代码实现如下:
components/Select/index.js
import React, { useState } from 'react';
import './index.css';
const Select = ({ value, children, ...props }) => {
const [visible, setVisible] = useState(false);
const onOptionVisibelChange = (e) => {
setVisible(!visible)
}
return (
<div className='common-container' onClick={onOptionVisibelChange}>
<div className='select-container' {...props} >{value}</div>
<div className='option-container' hidden={!visible}>
{children}
</div>
</div>
)
}
Select.Option = ({ value, children, ...props }) => {
return <div className='option-style' {...props} >{children}</div>
}
export default Select;
components/Select/index.css
/*Select 样式*/
.common-container{
width: fit-content;
margin:100px auto 0;
}
.select-container{
width: 200px;
height:40px;
border:1px solid #ccc;
}
.option-container{
width: 200px;
height:auto;
border:1px solid #666;
margin-top:10px;
}
/* Option Style*/
.option-style{
background-color: #eee;
padding:16px;
margin:10px auto;
width: 96%;
}
引用及其他使用如下:
import React, { useState } from 'react';
import Select from './components/Select';
const { Option } = Select;
const SelectDemo = () => {
const [select, setSelect] = useState(null);
const list = [{
label: 'Lucy', value: 1
}, {
label: 'Tom', value: 2
}, {
label: 'Mike', value: 3
}, {
label: 'Steven', value: 4
}]
const onOptionClick = (label) => {
setSelect(label);
}
return <>
<Select value={select}>
{
list.map(item => <Option value={item.value} onClick={() => onOptionClick(item.label)}>{item.label}</Option>)
}
</Select>
</>
}
export default SelectDemo;
眼尖的同学可能发现了,为了实现 Option 点击之后,改变 Select 的值,我不仅在每个 Option 上都加了 onClick 事件,还在 Select 传入了 选中值。
这是因为一般来说,Select 组件内部我无法操作 children ,所以一切的传参行为,都在 Select 和 Option 同时出现的地方进行操作。
这样的写法,未免有些冗余。我们可以看到,antd 组件库的 Select 于 Option 用法如下:
<Select defaultValue="lucy" style={{ width: 120 }} onChange={handleChange}>
<Option value="jack">Jack</Option>
<Option value="lucy">Lucy</Option>
</Select>
很明显,antd 并没有像常规写法一样添加 onClick 事件,那么,它是怎么做到的呢?
React.Children
React.Children 提供了用于处理 this.props.children 不透明数据结构的实用方法。
即,该方法可以对 props.children 进行进一步处理。此时,我们可以修改 components/Select/index.js 内容如下:
import React, { useState } from 'react';
import './index.css';
const Select = ({ value, children, ...props }) => {
const [visible, setVisible] = useState(false);
const [select, setSelect] = useState(null);
const onOptionVisibelChange = (e) => {
console.log('on click event', e);
setVisible(!visible)
}
const newChildren = React.Children.map(children, (child, index) => {
// 给每个子元素添加一个 click 方法
const { props } = child;
const { children } = props;
return <div onClick={() => setSelect(children)}>{child}</div>
})
return (
<div className='common-container' onClick={onOptionVisibelChange}>
<div className='select-container' {...props} >{select}</div>
<div className='option-container' hidden={!visible}>
{newChildren}
</div>
</div>
)
}
Select.Option = ({ value, children, ...props }) => {
return <div className='option-style' {...props} >{children}</div>
}
export default Select;
selectDemo.js 修改如下:
import React from 'react';
import Select from './components/Select';
const { Option } = Select;
const SelectDemo = () => {
const list = [{
label: 'Lucy', value: 1
}, {
label: 'Tom', value: 2
}, {
label: 'Mike', value: 3
}, {
label: 'Steven', value: 4
}]
return <>
<Select >
{
list.map(item => <Option value={item.value} >{item.label}</Option>)
}
</Select>
</>
}
export default SelectDemo;
此时,我们发现,使用该组件的方式优雅了许多,也依然能够实现原有功能。
我们可以进一步完善,给该组件添加 onChange 功能。
components/Select/index.js
import React, { useState } from 'react';
import './index.css';
const Select = ({ value, children, onChange, ...props }) => {
const [visible, setVisible] = useState(false);
const [select, setSelect] = useState(null);
const onOptionVisibelChange = (e) => {
setVisible(!visible)
}
const onOptionClick = (label, value) => {
setSelect(label);
onChange?.(value);
}
const newChildren = React.Children.map(children, (child, index) => {
// 给每个子元素添加一个 click 方法
const { props } = child;
const { children, value } = props;
return <div onClick={() => onOptionClick(children, value)}>{child}</div>
})
return (
<div className='common-container' onClick={onOptionVisibelChange}>
<div className='select-container' {...props} >{select}</div>
<div className='option-container' hidden={!visible}>
{newChildren}
</div>
</div>
)
}
Select.Option = ({ value, children, ...props }) => {
return <div className='option-style' {...props} >{children}</div>
}
export default Select;
selectDemo.js
import React from 'react';
import Select from './components/Select';
const { Option } = Select;
const SelectDemo = () => {
const list = [{
label: 'Lucy', value: 1
}, {
label: 'Tom', value: 2
}, {
label: 'Mike', value: 3
}, {
label: 'Steven', value: 4
}]
const onSelectChange = (value) => {
console.log('value', value);
}
return <>
<Select onChange={onSelectChange} >
{
list.map(item => <Option value={item.value} >{item.label}</Option>)
}
</Select>
</>
}
export default SelectDemo;
组合组件
优化后的组件有什么特点呢?
1.使用时,减少了 props 的传递。
2.将主要的处理内容都放在了组件内部。
这就是组合组件的实现实践。我对它的概念是这么理解的:它的核心目的是,将有关联的的组件组合起来使用即可,不需要做额外的处理。
结语
后面如果想进一步的实现功能,可以自己再实践一把。
如果本文对你有帮助的话,记得给我点个赞哦,每一个赞都是对我莫大的鼓励~