前提:老项目组件都是自己写的,之前多选都是用的多个checkbox。
为什么不写一个下拉多选呢?说干就干!!!
使用select + option样式无法改变,option标签还没法儿插入checkbox。所以用ul + li模拟下拉框。原生和react都写一份,权当复习原生了
先看原生效果,这里推荐一款录屏工具licecap,很是好用:官网下载链接
注意点:这个工具到官网下载最新版,之前的版本有个小bug:macOS Big Sur上白屏
一、代码可以直接复制看效果
要点:冒泡,事件委托
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
</style>
</head>
<body>
<div class="dropdown">
<div class="select"><i class="select-right select-down"></i></div>
<div class="mutliSelect">
<ul>
<li>
<input type="checkbox" id="Apple"/>
<label for="Apple">Apple</label>
</li>
<li>
<input type="checkbox" value="Blackberry" id="Blackberry"/>
<label for="Blackberry">Blackberry</label>
</li>
<li>
<input type="checkbox" value="HTC" id="HTC"/>
<label for="HTC">HTC</label>
</li>
<li>
<input type="checkbox" value="Sony Ericson" id="Sony"/>
<label for="Sony">Sony Ericson</label>
</li>
<li>
<input type="checkbox" value="Motorola" id="MotorolaMotorolaMotorolaMotorola"/>
<label for="MotorolaMotorolaMotorolaMotorola" title="MotorolaMotorolaMotorolaMotorola">MotorolaMotorolaMotorolaMotorola</label>
</li>
<li>
<input type="checkbox" value="Nokia" id="Nokia"/>
<label for="Nokia">Nokia</label>
</li>
</ul>
</div>
</div>
<script>
const select = document.querySelector('.select')
const selectUp = document.querySelector('.select-right')
const ul = document.querySelector('.mutliSelect ul')
const list = document.querySelectorAll('.mutliSelect input[type="checkbox"]')
select.addEventListener('click', function(e) {
// 阻止冒泡,不然下面window事件执行就尴尬了
e.stopPropagation()
// 下拉框隐藏时,点击删除不需要展开
if (e.target.tagName !== 'I') {
ul.style.display = "block"
selectUp.className = "select-right select-up"
return
}
const title = e.target.parentNode.title
select.removeChild(e.target.parentNode)
// 清理选中项
for(let i = 0; i < list.length ;i++) {
if (list[i].id === title) {
list[i].checked = false
break;
}
}
})
window.addEventListener('click', function(e) {
if(ul.contains(e.target) || select.contains(e.target)) {
return
}
ul.style.display = "none"
selectUp.className = "select-right select-down"
})
ul.addEventListener('click',function (e){
if(e.target.tagName === 'INPUT'){
const title = e.target.id
if (e.target.checked) {
const span = `<span class="tag" title=${title}><span >${title}</span><i class="tag-close"></i></span>`
// insertAdjacentHTML这个好用
select.insertAdjacentHTML('beforeend', span)
} else {
const target = document.querySelector(`span[title=${title}]`)
select.removeChild(target)
}
}
})
</script>
</body>
</html>
二、下面是react实现
先看普通效果
再看多选限制个数效果,模仿element下拉多选
代码部分也很简单,也可以直接复制看效果
import React, {useState, useEffect, useRef, Fragment} from 'react';
import PropTypes from 'prop-types';
import './style.less';
/**
* 使用:<SelectMultip data={list} width="500px" max={3} onClick={(val) => {}}/>
* @param {*} data [{key: 3, value: '姓名'}]
* @param {*} width
* @param {*} onClick
* @param {*} max 最多展示tag数量,多的用+n标识
*/
const SelectMultip = ({data, width, onClick, max = 10000}) => {
const [open, setOpen] = useState(false);
const [list, setList] = useState(data);
const ulRef = useRef();
const selectRef = useRef();
const handleOpen = (e) => {
setOpen(true);
};
// 给父组件
const getChecked = (list) => {
const checkedList = list.filter(item => item.checked).map(item => item.key);
onClick && onClick(checkedList);
};
const handleCheck = (i) => {
const newArray = [...list];
newArray[i].checked = !newArray[i].checked;
setList(newArray);
getChecked(newArray);
};
const handleClose = (key) => {
const newArray = [...list];
const target = newArray.find(item => item.key === key);
target.checked = false;
setList(newArray);
getChecked(newArray);
};
// 监听window点击部分
const handleWindow = (e) => {
if (ulRef.current.contains(e.target) || selectRef.current.contains(e.target)) {
return;
}
setOpen(false);
};
useEffect(() => {
window.addEventListener('click', handleWindow, false);
return () => {
window.removeEventListener('click', handleWindow, false);
};
}, []);
// 动态渲染节点部分
const renderItem = ({key, value}) => {
return <span className="tag" key={key} title={value}><span >{value}</span><i onClick={() => {handleClose(key);}} className="tag-close"></i></span>;
};
const checked = list.filter(item => item.checked);
const renderMax = () => {
const limit = checked.length > max;
const list = limit ? checked.splice(0, max) : checked;
return <Fragment>{list.map(item => renderItem(item))}{limit && checked.length > 0 && <span className="tag"><span >+{checked.length}</span></span>}</Fragment>;
};
return (
// eslint-disable-next-line react/forbid-dom-props
<div className="mutliDropdown" style={{width: width || '250px'}}>
<div className="select" ref={selectRef} onClick={handleOpen}>
{renderMax()}
<i className={`select-right ${open ? 'select-up' : 'select-down'}`}></i>
</div>
<div className="mutliSelect">
{/* eslint-disable-next-line react/forbid-dom-props */}
<ul ref={ulRef} style={{display: open ? 'block' : 'none'}} >
{list.map((item, i) => (
<li key={item.key} onClick={() => {
handleCheck(i);
}}>
<input readOnly type="checkbox" defaultChecked={item.checked}/>
<label title={item.key}>{item.value}</label>
</li>
))}
</ul>
</div>
</div>
);
};
SelectMultip.propTypes = {
width: PropTypes.string,
onClick: PropTypes.func,
max: PropTypes.number,
data(props, propName, componentName) {
const data = props[propName];
if (!(data && (Array.isArray(data) || typeof data == 'object'))) {
return new Error(
`Invalid prop \`${propName}\` supplied to \`${componentName}\`, expected \`array\` or \`object\`. `
);
}
}
};
export default SelectMultip;
关于react部分记录两点:
(1)React 16中错误# This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property target
on a released/nullified synthetic event. This is set to null.
当点击事件访问event.target的时候 target是null。请使用event.persist()
官方事件池说明
(2)Warning: A component is changing an uncontrolled input of type text to be controlled
由于item.checked初始值是undefined,所以爆出警告。
<input type="checkbox" checked={item.checked}/>
受控组件: 用到了value/checked的就是受控组件,可以通过value/checked控制显示值,使用onChange事件实时改变状态。一般都是使用受控组件
非受控组件: 使用this.$ref去取值,不需要时刻控制时使用.非受控组件建议使用defaultChecked / defaultValue设置初始值官方说明
当我加上初始值爆出以下错误
<input type="checkbox" checked={!!item.checked}/>
Failed prop type: You provided a checked
prop to a form field without an onChange
handler. This will render a read-only field. If the field should be mutable use defaultChecked
. Otherwise, set either onChange
or readOnly
意思是说:既然是受控组件,怎么不用onChange控制呢,要么用readOnly + checked/defaultChecked。要么用defaultChecked。
下面实际上是改成了非受控组件,非受控组件默认值item.checked是undefined也不会报错
<input readOnly type="checkbox" defaultChecked={item.checked}/>
或者
<input type="checkbox" defaultChecked={item.checked}/>
三、最后是css部分,完美结束
input {
cursor: pointer;
}
.select {
width:250px;
box-sizing: border-box;
position: relative;
min-height:30px;
display: flex;
padding: 3px 30px 3px 2px;
flex-wrap: wrap;
font-size: 14px;
line-height: 1.42857143;
color: #555555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: inset 0 1px 1px rgb(0 0 0 / 8%);
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
}
.select-right {
width: 5px;
height: 5px;
position: absolute;
right: 10px;
transition: transform .1s;
border: solid #999;
border-width: 0 1px 1px 0;
display: inline-block;
padding: 3px;
}
.select-down {
top: calc(40% - 5px);
transform: rotate(45deg);
}
.select-up {
top: 40%;
transform: rotate(-135deg);
}
.mutliSelect {
position: relative;
width: inherit;
}
.dropdown ul {
margin: -1px 0 0 0;
}
.dropdown ul {
z-index: 55;
display: none;
left: 0px;
padding: 2px 0px 2px 5px;
position: absolute;
width: 250px;
list-style: none;
height: auto;
max-height: 274px;
overflow: auto;
border: 1px solid #e4e7ed;
border-radius: 4px;
background-color: #fff;
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
box-sizing: border-box;
margin: 5px 0;
}
.dropdown ul li {
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
position: relative;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #606266;
height: 34px;
line-height: 34px;
box-sizing: border-box;
cursor: pointer;
}
.dropdown ul li:hover {
background-color: #f5f7fa;
}
.dropdown ul li label {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
margin-left: 5px;
cursor: pointer;
}
.tag {
height: 24px;
padding: 0 8px;
line-height: 22px;
background-color: #f4f4f5;
border-color: #e9e9eb;
color: #909399;
box-sizing: border-box;
margin: 2px 0 2px 6px;
display: flex;
max-width: 100%;
align-items: center;
}
.tag span {
overflow: hidden;
text-overflow: ellipsis;
}
.tag-close {
background-color: #c0c4cc;
top: 0;
flex-shrink: 0;
border-radius: 50%;
text-align: center;
position: relative;
cursor: pointer;
font-size: 12px;
height: 16px;
width: 16px;
line-height: 16px;
vertical-align: middle;
right: -5px;
transform: scale(.8);
position: relative;
}
.tag-close:hover {
color: #fff;
background-color: #909399;
}
.tag-close:after {
width: 100%;
position: absolute;
height: 1.5px;
background: #909399;
content: "";
top: 7px;
left: 0;
transform: rotate(134deg) scale(0.6);
}
.tag-close:hover::after {
background-color: #fff;
}
.tag-close:hover:before {
background-color: #fff;
}
.tag-close:before {
width: 100%;
position: absolute;
height: 1.5px;
background: #909399;
content: "";
top: 7px;
right: 0;
transform: rotate(45deg) scale(0.6);
}