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

前提:老项目组件都是自己写的,之前多选都是用的多个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);
    }

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,063评论 6 510
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,805评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,403评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,110评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,130评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,877评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,533评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,429评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,947评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,078评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,204评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,894评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,546评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,086评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,195评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,519评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,198评论 2 357

推荐阅读更多精彩内容