模拟select,隐藏下拉列表的几种实现
前言
平时开发过程中,出于各种原因模拟原生slect的要求并不算少见。
在实现的过程中,点击其他区域隐藏下拉列表,又是一个必备的功能,
最近在一次开发的过程中引发了点思考,做下总结。
现象
实际中的实现比较复杂,列表中还要增删改查等操作。这里就只放个最简单的demo。
目的是点击select以外的其他区域,隐藏下拉列表。
效果大概这个样子(简单粗暴纯演示用):
首先这确实不难实现,上来像方法一一样撸袖子干就完了
开始之前,先列下基本结构,待会好描述:
外层一个warper,里面是Input,下面就是ul,li绑定点击事件。
            <div className="match-select-warper" name={`this.idName`}>
                <Input></Input>
                <ul className={`${showOption ? '' : 'hidden'}`}>
                    <li onClick={this.clickHanler}>{问题1}</li>
                    <li onClick={this.clickHanler}>{问题1}</li>
                </ul>
            </div>
            // 点击列表,提示并隐藏弹框
            clickHanler(){
                alert('1')
                this.changeShow(false)
            }
实现方式有下面这么几种:
实现一:全局监听点击事件,判断是否为select区域的子元素。
这是原本比较熟悉和一直在使用的方式:
//组件挂载之后添加事件
componentDidMount(){
            // 非匿名函数的目的在于移除时解除事件
            this.clickTriggerHandler = ((idName) => {
                let id = idName;
                return (event) => {
                    // 是否属于子元素
                    !isParent(id, event.target) && (this.changeShow(false));
                }
            })(this.idName)
            document.addEventListener('click', this.clickTriggerHandler)
    }
    componentWillUnmount() {
        // 若绑定事件,则移除该事件
        if(this.clickTriggerHandler){
            document.removeEventListener('click', this.clickTriggerHandler)
        }
    }
至于如何判断事件元素的归属也比较常见:
判断当前元素的父元素是否为置顶元素,不满足则循环上溯祖先元素,直到document。
/**
 * 判断是否属于指定元素的子元素
 * @param {*} id 指定元素的标识
 * @param {*} dom 触发事件的dom
 */
const isParent=(id, dom)=>{
        let tempNode = dom.parentNode;
        while (tempNode && tempNode !== document) {
            // 满足则返回true
            if (tempNode.getAttribute('name') == id) {
                return true;
            } else {
                // 否则继续获取祖先元素
                tempNode = tempNode.parentNode;
            }
        }
        // 最终返回false
        return false;
}
这样达到了我们的目的,不过是有些缺点的。
缺点一:性能消耗
每次都溯源去判断,性能消耗是个问题,特别是稍微复杂页面,展示多个组件时。
缺点二:受其他dom元素行为影响
假如有元素阻止了冒泡,如果点到了这个元素,那么全局就监听不到该事件了。
 <button onClick={(e) => {
        e.nativeEvent.stopImmediatePropagation();
        alert('我就是来阻止冒泡的')
    }}>测试</button>
那么效果就如下图所示了:
此外实现方式总感觉不够优雅,所以我们应该考虑其他实现方式。
实现二:select元素的焦点事件
可能一开始思维固话之后,就不太好转变,因为上面的方式是一直所熟悉的,一时想不到其他方法。
这时候可以去跟别人交流一下(这里的交流包括但不限于老司机面谈,搜索某种实现思路,优秀开源框架)。
得到了另一个方向:点击其他区域的时候,意味着当前区域失去了焦点,
基于这一点可以从input操作了。
 <div className="match-select-warper" name={`${this.idName}`}>
    <Input
        onFocus={(e) => {
            // 聚焦或者失焦时,完全可以操作
            this.changeShow(true)
        }}
        onBlur={(e) => {
            this.changeShow(false)
        }}
    ></Input>
    <ul className={`${showOption ? '' : 'hidden'}`}>
        <li onClick={this.clickHanler}>{问题1}</li>
        <li onClick={this.clickHanler}>{问题1}</li>
    </ul>
</div>
这样看起来很美好,但是点击列表的时候,直接关闭了,没有执行this.clickHanler回调。
因为下拉列表操作点击的时候,其实对于Input而言也是失去焦点。
所以先执行了input的onBlur,隐藏列表,state更新之后,
列表的click操作并没有得到相应。
既然是执行顺序的问题,那么我们可以有下面两种解决思路:
2.1 事件执行顺序不变,修改回调事件执行时机
既然blur执行顺序在前,重新渲染后会影响后续执行,那么我们将blur事件的回调延迟执行,即不立即去setState,那么li的click事件就会执行,然后再去隐藏列表。
至于如何延迟执行,显然就是我们的万能setTimeout了:
 <div className="match-select-warper" name={`${this.idName}`}>
    <Input
        onFocus={(e) => {
            // 聚焦或者失焦时,完全可以操作
            this.changeShow(true)
        }}
        onBlur={(e) => {
            // 延迟执行 blur的回调,先执行
            setTimeout(this.changeShow.bind(this,false),200)
        }}
    ></Input>
    <ul className={`${showOption ? '' : 'hidden'}`}>
        <li onClick={this.clickHanler}>{问题1}</li>
        <li onClick={this.clickHanler}>{问题1}</li>
    </ul>
</div>
这样可以满足我们的需求,此外还有另一种方式
2.2 改变事件执行顺序,即使用触发时机在blur之前的事件来替换click,即mouseDown
大致说下几个事件的执行顺序(毕竟我对这方面掌握的也不是很不足,所以后面也会专门总结下相关内容)。
// 这里也顺便解释了下问题出现的原因
mousedown->blur->mouseup->click
既然click触发时机晚于blur,那我们换成mouseDown不就绕过去了。
<div className="match-select-warper" name={`${this.idName}`}>
    <Input
        onFocus={(e) => {
            // 聚焦或者失焦时,完全可以操作
            this.changeShow(true)
        }}
        onBlur={(e) => {
            // 延迟执行 blur的回调,先执行
            setTimeout(this.changeShow.bind(this,false),200)
        }}
    ></Input>
    // 列表的选择回调在mousedown时执行
    <ul className={`${showOption ? '' : 'hidden'}`}>
        <li onMouseDown={this.clickHanler}>{问题1}</li>
        <li onMouseDown={this.clickHanler}>{问题1}</li>
    </ul>
</div>
效果同上,这里就不重复放图了。
如果我们的目的是点击列表的时候,完全不触发blur事件,可以在clickHanler回调里加上event.preventDefault(),这样就不会按照原来的顺序出发blur事件了。例如这里:
            // 本身自行处理了列表显示,就不用调用blur事件了
            clickHanler(event){
                event.preventDefault()
                alert('1')
                this.changeShow(false)
            }
具体是否阻止默认事件,就看具体应用了,示例代码这里就没有阻止默认事件,
而是将列表的显示隐藏全交给焦点事件来处理。
            // 只关注点击的逻辑,公共逻辑交给blur统一管理
            clickHanler(){
                alert('1')
            }
方式三: 下拉列表显示时增加背景遮罩
即点击其他区域时,点击的是背景mask,交给他来统一处理。
因为这样点击存在一个比较明显的问题,如果想要点击其他元素例如radio时,需要二次点击。
所以这里就不去折腾这种实现了。
结束语
参考文章和组件
浏览器点击屏幕事件触发顺序
eagle-ui
https://segmentfault.com/q/1010000004950602
本文是自己的一篇学习总结记录,不过我感觉最有用的还是对自己的触动。因为平时都习惯于第一种方式去实现功能,特别是在业务开发过程中,第一选择肯定是自己常用的。还是在空闲时候才有心情去优化。
这时候才清晰的理解我们所谓的读优秀开源作品源码,学习的是什么,不要为了读源码而读源码,有目的有思维的读才能学习更多。望诸君共勉,再次对参考文章表示感谢。
模拟select,隐藏下拉列表的几种实现的更多相关文章
- 模拟select样式,自定义下拉列表为树结构
		
效果图如下: 首先,需要用到的库jQuery,zTree(官网API:http://www.treejs.cn/v3/api.php) 注意:因为zTree是基于jQuery的,所以应该先引入jQue ...
 - ul -- li 模拟select下拉框
		
在写项目中 用到下拉框,一般用 <select name="" id=""> <option value=</option> &l ...
 - div 模拟<select>事件
		
IE7 下,不能够自定义<select>/<option>的样式,所以为了方便起见,用div可以进行模拟 <!doctype html> <html> ...
 - 模拟select控件,css模拟下拉
		
<!DOCTYPE html > <head> <meta http-equiv="Content-Type" content="t ...
 - Bootstrap 3之美06-Page Header、Breadcrumbs、Dropdowns、Button Dropdowns、用Button和Dropdowns模拟Select、Input Groups、Thumbnails、Panels、Wells
		
本篇主要包括: ■ Page Header■ Breadcrumbs■ Button Groups■ Dropdowns■ Button Dropdowns■ 用Button和Dropdo ...
 - div模拟select/option解决兼容性问题及增加可拓展性
		
个人博客: http://mcchen.club 想到做这个模拟的原因是之前使用select>option标签的时候发现没有办法操控option的很多样式,比如line-height等,还会由此 ...
 - CSS中隐藏内容的3种方法及属性值
		
CSS中隐藏内容的3种方法及属性值 (2011-02-11 13:33:59) 在制作网页时,隐藏内容也是一种比较常用的手法,它的作用一般有:隐藏文本/图片.隐藏链接.隐藏超出范围的内容.隐藏弹出 ...
 - jQuery插件:模拟select下拉菜单
		
没搞那么复杂,工作中,基本够用.. <!doctype html> <html> <head> <meta charset="utf-8" ...
 - CSS隐藏元素的几种妙法
		
一说起CSS隐藏元素,我想大部分小伙伴们都会想到的第一种方法就是设置display为none.这是最为人所熟知也是最常用的方法.我相信还有不少人想到使用设置visibility为hidden来隐藏元素 ...
 
随机推荐
- [九省联考2018]IIIDX
			
题目描述 这一天,Konano接到了一个任务,他需要给正在制作中的游戏<IIIDX>安排曲目的解锁顺序.游戏内共有n首曲目 ,每首曲目都会有一个难度d,游戏内第i首曲目会在玩家Pass第t ...
 - OSU! on tree
			
dsu on tree 好吧,这个毒瘤...... 树剖和启发式合并的杂合体. 用于解决静态子树问题,复杂度O(nlogn * insert时间) 因为dsu是并查集的意思所以算法名字大概就是什么树上 ...
 - 第三十九篇-RecyclerView的使用
			
RecyclerView介绍 RecyclerView的出现可以替代ListView,并且比ListView更高级且更具灵活性.如果有数据集合,其中的元素将因用户操作或网络事件而在运行时发生改变,请使 ...
 - Python终极coding
			
作为一名程序员,除了需要具备解决问题的思路以外,代码的质量和简洁性也很关键.因为从一个人的代码可以直接看出你的基本功.对于Python而言,这就意味着你需要对Python的内置功能和库有很深入的了解. ...
 - [Android] Android 锁屏实现与总结 (一)
			
实现锁屏的方式有多种(锁屏应用.悬浮窗.普通Activity伪造锁屏等等).但国内比较主流并且被广泛应用的Activity伪造锁屏方式. 实例演示图片如下: 系列文章链接如下: [Android] A ...
 - John von Neumann和Ulam
			
John von Neumann和Ulam是好朋友,两人经常在一起喝酒.旅行.谈女人.有一次诺伊曼认出身边的一位女士,他们交谈了几句.随后他给Ulam介绍那是他的一位老朋友,刚刚离婚.Ulam就问:那 ...
 - [再寄小读者之数学篇](2014-06-23 积分不等式 [中国科学技术大学2013年高等数学B 考研试题])
			
设 $f(x)$ 在 $[a,b]$ 上一阶连续可导, $f(a)=0$. 证明: $$\bex \int_a^b f^2(x)\rd x\leq \cfrac{(b-a)^2}{2}\int_a^b ...
 - [物理学与PDEs]第2章习题2 质量力有势时的能量方程
			
试证明: 如果质量力有势, 即存在 $\phi$ 使 ${\bf F}=-\n \phi$, 那么理想流体的能量守恒方程的微分形式可写为 $$\bex \cfrac{\rd}{\rd t}\sex{e ...
 - DUMP102 企业级电商FE
			
101 完成 webpack 配置后,有一套类似 live-reload 自动刷新提供 REPL 环境. [配置 webpack.config.js 别名,方便 js 文件做require 支持路径别 ...
 - webpack 代码优化压缩方法
			
在配置基于webpack的react单页面脚手架时,公共依赖库代码打包至vender.js中,页面逻辑代码打包至app.js中,使用webpack-bundle-analyzer分析发现,两个js中包 ...