本文正在参加「金石计划」
一:引言
本篇文章仿写antd组件库的Select组件,文章重点是模拟Select组件的内部功能实现,样式比较丑希望大家包涵。
二:组件分析
观察下图,我们设计一个可配置的select组件,它主要有两种模式。
单选
多选
- className 类名
- mode 选择模式,单选或多选
- defaultValue 默认选择项
- disabled 禁止选择
- onChange 选择时触发回调
三:问题拆解
在设计组件前,我们可以先考子问题,最后组装起来就实现了目标组件。
问题1:点击非选择框区域,如何关闭下拉列表(重点)?
-
在select组件根元素上添加ref获取dom对象。
-
全局添加点击事件监听
document.addEventListener('click',handle);
-
handle添加方法,判断当前点击区域是否属于select组件内。
const handle = (e:MouseEvent) => { const isOutside = !domRef.current?.contains(e.target as Node); if(isOutside) setDownModal(false); }
-
组件销毁时,释放全局点击事件
问题2:多选模式下如何控制选择项与下拉列表数据状态同步?
初始化选中数组muiltpe,每次点击下拉列表时更新选中数组,如果存在该项则删除,否则添加。
问题3:下拉列表如何基于输入框定位?多选模式下输入框高度发生变化如何定位?
可以给输入框盒子内部添加下拉列表盒子,设置输入框是相对定位,设置下拉列表是绝对定位,考虑到输入框高度是变化的,因此需要动态的获取输入框盒子高度,同步设置下拉框的top=输入框盒子高度就可以保证下拉框始终保持定位一致了。
五:Select代码实现
import React,{useState,useRef, useEffect} from "react";
import classNames from "classnames";
interface selectProps {
defaultValue?:string|string[]|number|number[],
className?:string
options?:Array<any>,
onChange?:(e:any)=>void,
mode?:string,
children?:React.ReactNode
}
const Select = (props:selectProps) => {
const {
defaultValue,
className,
options,
onChange,
mode
} = props;
const domRef = useRef<HTMLDivElement>(null);
const [divHeight,setDivHeight] = useState(0);
const [downModal,setDownModal] = useState(false);
const [choiceItem,setChoiceItem] = useState<{label:any,value:any}>();
const [muiltItems,setMuiltItems] = useState<any>([]);//多选模式
const classes = classNames('select',className,{})
//初始化选择列表(排除用户随意添加默认值)
const initSelect = () => {
if(mode==='tags') {
options?.forEach(item=>{
if(item.value===defaultValue) {
setChoiceItem(item);
}
})
}else{
if(options) {
if(Array.isArray(defaultValue)) {
let tmp = [];
for(let i=0;i<defaultValue.length;i++) {
let val = options?.find(item=>item.value===defaultValue[i])
if(val) tmp.push(val);
}
setMuiltItems(tmp);
}else {
let val = options?.find(item=>item.value===defaultValue)
if(val) {
setMuiltItems([val])
}
}
}
}
}
useEffect(()=>{
/** 监听是否点击非元素自身 */
const handle = (e:MouseEvent) => {
const isOutside = !domRef.current?.contains(e.target as Node);
if(isOutside) setDownModal(false);
}
document.addEventListener('click',handle);
/**初始化选择列表 */
initSelect();
//组件卸载时销毁全局监听事件
return ()=>{
document.removeEventListener('click',handle);
}
},[])
//动态计算盒子高度
useEffect(()=>{
setDivHeight(domRef.current?.offsetHeight!);
},[muiltItems])
//点击下拉列表
const clickItems = (e:any,item:any) => {
e.stopPropagation()//阻止冒泡,否则触发列表关闭
if(mode === 'tags') {
setDownModal(false);
setChoiceItem(item);
onChange && onChange(item.value)
} else {
//多选是数组,存在添加,否则删除
let tmp:any = [...muiltItems]
let isHave = false;
for(let i=0;i<tmp.length;i++) {
if(tmp[i]=== item) {
tmp.splice(i,1);
isHave = true;
break;
}
}
if(!isHave) tmp.push(item);
setMuiltItems(tmp);
onChange && onChange(tmp);
}
}
//点击祖先元素
const clickSelectBox = (e:any)=> {
e.stopPropagation();
setDownModal(!downModal);
}
//根据下拉状态动态绑定class
const judgeSelectClassName = (item:any) => {
if(mode==='tag' && item.value===choiceItem?.value) {
if(item.disabled){
return "select_list_item select_list_item_disabled";
}else{
return "select_list_item select_list_item_actived";
}
}
if(mode==='multiple' && muiltItems.includes(item)){
if(item.disabled){
return "select_list_item select_list_item_disabled";
}else{
return "select_list_item select_list_item_actived";
}
}
if(item.disabled) {
return "select_list_item select_list_item_disabled";
}
else{
return "select_list_item"
}
}
return (
<div
className={classes}
onClick={clickSelectBox}
ref={domRef}
>
<div className="select_box">
{mode==='tags' && choiceItem && choiceItem?.value}
{mode === 'multiple' && (
<>
{muiltItems && muiltItems?.length>0 && muiltItems.map((item:any)=>(
<div className="select_muilt" key={item.value}>
{item?.value}
<span
onClick={(e)=>{
//关闭
e.stopPropagation()
let tmp:any = [...muiltItems]
for(let i=0;i<tmp.length;i++) {
if(tmp[i]=== item) {
tmp.splice(i,1);
break;
}
}
setMuiltItems(tmp);
onChange && onChange(tmp);
}}
>
x
</span>
</div>
))}
</>
)}
</div>
{
<div className='select_list' style={{top:`${divHeight}px`}}>
{downModal && options && options.length>0 && options.map(item=>(
<div
className={judgeSelectClassName(item)}
key={item.label}
onClick={(e)=>{
if(!item.disabled)
clickItems(e,item)
}}
>
{item.value}
</div>
))}
</div>
}
</div>
)
}
Select.defaultProps = {
mode:'tags',
options:[]
}
export default Select;
六:功能演示
演示1:单选默认
const options=[
{label:'小米',value:'小米'},
{label:'华为',value:'华为'},
{label:'网易',value:'网易'},
{label:'京东',value:'京东'},
]
<Select options={options}/>
演示2:单选添加默认值和禁止选择项
const options=[
{label:'小米',value:'小米'},
{label:'华为',value:'华为'},
{label:'网易',value:'网易'},
{label:'京东',value:'京东',disabled:true},
]
<Select options={options} defaultValue={'小米'}/>
演示3:单选触发回调函数
const options=[
{label:'小米',value:'小米'},
{label:'华为',value:'华为'},
{label:'网易',value:'网易'},
{label:'京东',value:'京东',disabled:true},
]
<Select options={options} defaultValue={'小米'} onChange={(e:any)=>console.log(e)}/>
演示4:多选默认
const options=[
{label:'小米',value:'小米'},
{label:'华为',value:'华为'},
{label:'网易',value:'网易'},
{label:'京东',value:'京东'},
]
<Select options={options} mode='multiple'/>
演示4:多选添加默认值和禁止
const options=[
{label:'小米',value:'小米'},
{label:'华为',value:'华为'},
{label:'网易',value:'网易'},
{label:'京东',value:'京东',disabled:true},
]
<Select options={options} mode='multiple' defaultValue={['小米']}/>
演示4:多选添加回调函数
const options=[
{label:'小米',value:'小米'},
{label:'华为',value:'华为'},
{label:'网易',value:'网易'},
{label:'京东',value:'京东',disabled:true},
]
<Select
options={options}
mode='multiple'
defaultValue={['小米']}
onChange={(e)=>console.log(e)}
/>
总结
今天Select组件到此结束,希望大家多多支持,我们下一个组件见。