前言

平时开发过程中,出于各种原因模拟原生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,隐藏下拉列表的几种实现的更多相关文章

  1. 模拟select样式,自定义下拉列表为树结构

    效果图如下: 首先,需要用到的库jQuery,zTree(官网API:http://www.treejs.cn/v3/api.php) 注意:因为zTree是基于jQuery的,所以应该先引入jQue ...

  2. ul -- li 模拟select下拉框

    在写项目中 用到下拉框,一般用 <select name="" id=""> <option value=</option> &l ...

  3. div 模拟<select>事件

    IE7 下,不能够自定义<select>/<option>的样式,所以为了方便起见,用div可以进行模拟 <!doctype html> <html> ...

  4. 模拟select控件,css模拟下拉

    <!DOCTYPE html > <head>     <meta http-equiv="Content-Type" content="t ...

  5. 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 ...

  6. div模拟select/option解决兼容性问题及增加可拓展性

    个人博客: http://mcchen.club 想到做这个模拟的原因是之前使用select>option标签的时候发现没有办法操控option的很多样式,比如line-height等,还会由此 ...

  7. CSS中隐藏内容的3种方法及属性值

    CSS中隐藏内容的3种方法及属性值 (2011-02-11 13:33:59)   在制作网页时,隐藏内容也是一种比较常用的手法,它的作用一般有:隐藏文本/图片.隐藏链接.隐藏超出范围的内容.隐藏弹出 ...

  8. jQuery插件:模拟select下拉菜单

    没搞那么复杂,工作中,基本够用.. <!doctype html> <html> <head> <meta charset="utf-8" ...

  9. CSS隐藏元素的几种妙法

    一说起CSS隐藏元素,我想大部分小伙伴们都会想到的第一种方法就是设置display为none.这是最为人所熟知也是最常用的方法.我相信还有不少人想到使用设置visibility为hidden来隐藏元素 ...

随机推荐

  1. 帝国cms 不能正常显示最新文章

    后台能正常刷新,但前台就是不能正常显示, 把网站从c盘换到d盘,好了,原来是权限的问题

  2. CentOS7安装jdk8及环境变量配置

    下载jdk8 这里可以使用Windows下载,然后传到虚拟机 进入jdk下载页面 https://www.oracle.com/technetwork/java/javase/downloads/in ...

  3. Redis系列八:redis主从复制和哨兵

    一.Redis主从复制 主从复制:主节点负责写数据,从节点负责读数据,主节点定期把数据同步到从节点保证数据的一致性 1. 主从复制的相关操作 a,配置主从复制方式一.新增redis6380.conf, ...

  4. Java基础 -- 复用类(组合和继承)

    复用类有两种实现方式. 在新的类中产生现有类的对象,由于新的类是由现有类的对象所组成,所以这种方法称之为组合. 采用继承实现. 一  组合语法 下面创建两个类WaterSource和Sprinkler ...

  5. Pandas系列(五)-分类数据处理

    内容目录 1. 创建对象 2. 常用操作 3. 内存使用量的陷阱 一.创建对象 1.基本概念:分类数据直白来说就是取值为有限的,或者说是固定数量的可能值.例如:性别.血型. 2.创建分类数据:这里以血 ...

  6. Flask进阶

    Threading.local 作用:为每个线程创建一个独立的空间,使得线程对自己的空间中的数据进行操作(数据隔离). 应用: flask上下文管理中的local中比这更高级,为协程. DBUtils ...

  7. CMDB服务器管理系统【s5day89】:深入理解Java的接口和抽象类

    对于面向对象编程来说,抽象是它的一大特征之一.在Java中,可以通过两种形式来体现OOP的抽象:接口和抽象类.这两者有太多相似的地方,又有太多不同的地方.很多人在初学的时候会以为它们可以随意互换使用, ...

  8. 金融量化分析【day112】:股票数据分析Tushare2

    目录 1.使用tushare包获取某股票的历史行情数据 2.使用pandas包计算该股票历史数据的5日局限和60日均线 3.matplotlib包可视化历史数据的收盘价和历史均线 4.分析输出所有金叉 ...

  9. 语义化标签和jQuery选择器

    关于语义化标签 https://blog.csdn.net/nongweiyilady/article/details/53885433 更详细的语义化标签:https://www.cnblogs.c ...

  10. Form -- 文件上传

    当我们选中文件,点击上传时即可. 而此按钮一般是一张图片覆盖了一个input标签而以.基于这个原理我们可以定制自己喜欢的样式 <div style="text-align: cente ...