美文网首页
原生代码,react实现下拉多选

原生代码,react实现下拉多选

作者: 云鹤道人张业斌 | 来源:发表于2021-12-09 17:20 被阅读0次
前提:老项目组件都是自己写的,之前多选都是用的多个checkbox。

为什么不写一个下拉多选呢?说干就干!!!
使用select + option样式无法改变,option标签还没法儿插入checkbox。所以用ul + li模拟下拉框。原生和react都写一份,权当复习原生了

先看原生效果,这里推荐一款录屏工具licecap,很是好用:官网下载链接
注意点:这个工具到官网下载最新版,之前的版本有个小bug:macOS Big Sur上白屏

oring.gif

一、代码可以直接复制看效果

要点:冒泡,事件委托

<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实现

先看普通效果


react.gif

再看多选限制个数效果,模仿element下拉多选

react1.gif

代码部分也很简单,也可以直接复制看效果

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);
    }

相关文章

网友评论

      本文标题:原生代码,react实现下拉多选

      本文链接:https://www.haomeiwen.com/subject/admffrtx.html