面试官叫我手写Element之Select组件

2,220 阅读8分钟

写在前面

  代码系本人原创,如果存在某些地方可以改进或者大家有更好的思路,麻烦大家在评论区进行指正,蟹蟹大家。

实现效果

  原生效果:Element UI之Select组件传送门

  博文效果:程序重在模拟Select组件的业务逻辑,部分颜色padding可能会和原生的效果存在出入。有强迫症的小伙伴可以用拾色器和截图工具深究颜色padding值,然后自行修改。

  •  初始效果: 1615511850(1).png

  •  点击效果: 1615511896(1).png

  •  鼠标悬浮效果 & 点击效果: 1615511969(1).png

1615517832(1).png

  •  鼠标选择效果: 1615512058(1).png

  •  鼠标点击输入框外的效果: 1615512125(1).png

需求分析

*   初始效果:

  • 原生Select组件由文本框和字体图标组成,使用placaholder作为默认输入提示,使用readonly禁止文本编辑。其中的字体颜色均为灰色,没有input自带的外廓样式,需要修改css样式:
input {
    outline: none; //消除input自带的外廓样式
    box-sizing: border-box; //盒模型,消除paddinginput输入框带来的宽高影响
    color: #606266 !important; //使用!important给予更高的优先级
}

input:focus {
    border: 1px solid #409eff; //点击输入框,改变输入框的边框样式,原生组件边框的出现比较缓和,是因为有加过渡动画
}
  • 原生Select组件的字体图标使用了阿里字体,需要引入阿里字体,小伙伴可以自行下载和导入需要使用的字体图标。字体图标和文本框需要组合在一起,给icon添加一个icon类,赋予它绝对定位,并给input输入框一个相对定位,调整合适的位置即可。
<head>
    <!--引入项目所在的阿里字体CSS文件-->
    <link rel="stylesheet"  href="https://at.alicdn.com/t/font_2405460_rrlnzuc0m8.css">
</head>
<body>
    <!--类名使用iconfont + 项目上定义的类名调用阿里字体,默认使用下箭头图标加载-->
    <i id="icon" class="iconfont icon-shanglajiantou icon"></i>
</body>
.input {
    width: 240px;
    height: 40px;
    padding: 0px 30px 0px 15px;
    border: 1px solid #dcdfe6;
    position: relative;
}

.icon {
    position: absolute;
    left: 218px;
    top: 12px;
}

*   点击效果:

  • 原生Select组件点击文本框后会切换字体图标,同时显示选择列表。说明文本框和字体图标放在同一层,同时需要使用一个div进行包裹,点击会触发一个事件。所以,对应的html代码应该是:
<div onclick="handleWarpClicked()" id="select">
    <input id="input" type="text" class="input" placeholder="请选择" readonly="readonly">
    <!--类名使用iconfont + 项目上定义的类名调用阿里字体-->
    <i id="icon" class="iconfont icon-shanglajiantou icon"></i>
</div>
  • 切换图标使用一个全局变量进行判断,每次点击取反即可。如果为真,就取上箭头的字体图标,同时显示选择列表;如果为假,就取下箭头的字体图标,同时隐藏选择列表。所以div对应的js点击代码应该是:
var up = false 

//设置下拉图片的样式
function setDownIcon() {
    _getElementById("icon").className = "iconfont icon-shanglajiantou icon"
    _getElementById("warp").style.display = "none"
}

//包裹容器点击事件
function handleWarpClicked() {
    up = !up
    if(up) {
        _getElementById("icon").className = "iconfont icon-shangjiantou icon"
        _getElementById("warp").style.display = "block"
        _getElementById("input").style.border = "1px solid #409eff"
    } else setDownIcon()
}

*   鼠标悬浮效果 & 点击效果 & 鼠标选择效果:

  • 构建选择列表的html代码和对应样式
<body>
    <ul class="warp" id="warp">
        <!--使用CSS控制的正三角-->
        <span class="triangle"></span>
        <li onmouseover="Onmouseover(event)" onclick="handleClicked(event)">黄金糕</li>
        <li onmouseover="Onmouseover(event)" onclick="handleClicked(event)">双皮奶</li>
        <li onmouseover="Onmouseover(event)" onclick="handleClicked(event)">蚵仔煎</li>
        <li onmouseover="Onmouseover(event)" onclick="handleClicked(event)">龙须面</li>
        <li onmouseover="Onmouseover(event)" onclick="handleClicked(event)">北京烤鸭</li>
    </ul>
</body>
ul {
    list-style: none;
}

.triangle {
    position: absolute;
    left: 50px;
    top: 50px;
    height:0px; 
    width:0px;
    border-bottom:10px solid #f5f7fa;
    border-left:10px solid transparent;
    border-right:10px solid transparent;
}

.warp {
    height: 180px;
    width: 238px;
    background-color: #f5f7fa;
    padding: 5px 0px;
    display: none;
    margin-top: 20px;
}

.warp li {
    width: 238px;
    height: 34px;
    padding: 0px 20px;
    box-sizing: border-box;
    line-height: 34px;
}
  • 鼠标悬浮到选择列表上,当前列表项的背景颜色会发生更改。鼠标从选择列表移出后,仍会保留相应的背景颜色。如果使用CSS的hover伪类选择器,无法达到移出选择区域仍保留背景颜色状态的效果,所以可以考虑给每个小li增添一个onmouseover事件,用来保留更新的背景颜色状态。点击当前小li,当前小li的颜色和字体会发生变化,同时文本框的边框会发生变化。所以考虑为每个小li增添一个onclick事件,用来接收小li的值,并将得到的值赋给input文本框的value。每次点击后对up进行取反,同时关闭选择列表。
//小li点击事件
function handleClicked(event){
    const target = event.target
    //需要转换为数组
    const tags =Array.from(document.getElementsByTagName("li"))
    //拿到所有小li,遍历循环,使用排他思想重置样式
    tags.forEach(element => {
        element.style.color = "#000000"
        element.style.fontWeight = "normal"
    });
    //为选中的小li添加样式
    _getElementById("input").value = target.innerHTML
    _getElementById("input").style.color = "#000"
    _getElementById("input").style.border = "1px solid #409eff"
    _getElementById("warp").style.display = "none"
    _getElementById("icon").className = "iconfont icon-shanglajiantou icon"
    target.style.color = "#409eff"
    target.style.fontWeight = "bold"
    //点击小li需要对up进行取反
    up = !up
}

//添加鼠标事件
function Onmouseover(e) {
    //排他思想,清除所有小li的背景样式
    const tags =Array.from(document.getElementsByTagName("li"))
    tags.forEach(element => {
        element.style.backgroundColor = "#f5f7fa"
    });
    //为选中的小li添加背景样式
    e.target.style.backgroundColor = "pink"
}

*   鼠标点击输入框外的效果:

  • 点击输入框外的区域,会关闭选择列表,同时重置输入框的边框样式。既然是点击输入框外产生的效果,就必须得先把输入框这个区域给找出来。所以,本程序定义了一个全局变量root,用于判断当前点击的区域。同时由于页面根元素dom的点击事件可能会和文本框以及选择列表产生冲突,需要进行判断。
var root = document.documentElement

//为页面添加鼠标事件
root.onmouseup = function(e) {
    //如果鼠标松开区域在ul容器外,隐藏ul容器,并设置输入框的边框样式
    //此处会和输入框点击事件冲突,点击输入框会同时执行页面根元素的点击事件和输入框点击事件,都会隐藏选择列表,
    //但是由于input:focus的css权重要大于_getElementById("input").style.border = "1px solid #dcdfe6"的css权重,
    //所以点击输入框,最终还是会显示为蓝色边框。
    if(e.target.id !== "warp") {
        _getElementById("warp").style.display = "none"
        _getElementById("input").style.border = "1px solid #dcdfe6"
    }
    //如果鼠标松开区域不在输入框和ul内部,就给up取反,此种方法有点取巧
    //通过选择列表和文本框的e.path.length来判断,刚好它们的值都为6
    //点击区域在选择列表和文本框外需要对up进行取反,选择列表和文本框点击事件都已经有对up取反。
    if(e.path.length !== 6) {
        up = !up
    }
    setDownIcon()
}

源码放送

  本程序由html文件、css文件夹和js文件夹构成,它们处于同一层级。其中,css文件夹又包括select.css文件,js文件夹由select.js(用于处理选择器逻辑)和help.js(做项目养成的习惯,用于定义公共方法)组成。

1615513523(1).png

  select.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Element Select</title>
    <link rel="stylesheet" href="css/select.css" type="text/css">
    <!--引入项目所在的阿里字体CSS文件-->
    <link rel="stylesheet"  href="https://at.alicdn.com/t/font_2405460_rrlnzuc0m8.css">
</head>
<body>
    <div onclick="handleWarpClicked()" id="select">
        <input id="input" type="text" class="input" placeholder="请选择" readonly="readonly">
        <!--类名使用iconfont + 项目上定义的类名调用阿里字体-->
        <i id="icon" class="iconfont icon-shanglajiantou icon"></i>
    </div>
    <ul class="warp" id="warp">
        <!--使用CSS控制的正三角-->
        <span class="triangle"></span>
        <li onmouseover="Onmouseover(event)" onclick="handleClicked(event)">黄金糕</li>
        <li onmouseover="Onmouseover(event)" onclick="handleClicked(event)">双皮奶</li>
        <li onmouseover="Onmouseover(event)" onclick="handleClicked(event)">蚵仔煎</li>
        <li onmouseover="Onmouseover(event)" onclick="handleClicked(event)">龙须面</li>
        <li onmouseover="Onmouseover(event)" onclick="handleClicked(event)">北京烤鸭</li>
    </ul>
    <!--引入公共方法,且由于JS是同步执行,help.js必须在select.js之上,不然无法引用公共方法-->
    <script src="js/help.js"></script>
    <script src="js/select.js"></script>
</body>
</html>

  select.css:

* {
    margin: 0; //项目初始化,清除paddingmargin,可加可不加,看个人习惯
    padding: 0;
}

ul {
    list-style: none;
}

.input {
    width: 240px;
    height: 40px;
    padding: 0px 30px 0px 15px;
    border: 1px solid #dcdfe6;
    position: relative;
}

.triangle {
    position: absolute;
    left: 50px;
    top: 50px;
    height:0px; 
    width:0px;
    border-bottom:10px solid #f5f7fa;
    border-left:10px solid transparent;
    border-right:10px solid transparent;
}

input {
    outline: none;
    box-sizing: border-box;
    color: #606266 !important;
}

input:focus {
    border: 1px solid #409eff;
}

.icon {
    position: absolute;
    left: 218px;
    top: 12px;
}

.warp {
    height: 180px;
    width: 238px;
    background-color: #f5f7fa;
    padding: 5px 0px;
    display: none;
    margin-top: 20px;
}

.warp li {
    width: 238px;
    height: 34px;
    padding: 0px 20px;
    box-sizing: border-box;
    line-height: 34px;
}

  help.js:

//获取id为id的dom元素
function _getElementById(id) {
    return document.getElementById(id)
}

  select.js:

var up = false 
var root = document.documentElement

//设置下拉图片的样式
function setDownIcon() {
    _getElementById("icon").className = "iconfont icon-shanglajiantou icon"
    _getElementById("warp").style.display = "none"
}

//包裹容器点击事件
function handleWarpClicked() {
    up = !up
    if(up) {
        _getElementById("icon").className = "iconfont icon-shangjiantou icon"
        _getElementById("warp").style.display = "block"
        _getElementById("input").style.border = "1px solid #409eff"
    } else setDownIcon()
}

//小li点击事件
function handleClicked(event){
    const target = event.target
    //需要转换为数组
    const tags =Array.from(document.getElementsByTagName("li"))
    //拿到所有小li,遍历循环,使用排他思想重置样式
    tags.forEach(element => {
        element.style.color = "#000000"
        element.style.fontWeight = "normal"
    });
    //为选中的小li添加样式
    _getElementById("input").value = target.innerHTML
    _getElementById("input").style.color = "#000"
    _getElementById("input").style.border = "1px solid #409eff"
    _getElementById("warp").style.display = "none"
    _getElementById("icon").className = "iconfont icon-shanglajiantou icon"
    target.style.color = "#409eff"
    target.style.fontWeight = "bold"
    //点击小li需要对up进行取反
    up = !up
}

//添加鼠标事件
function Onmouseover(e) {
    //排他思想,清除所有小li的背景样式
    const tags =Array.from(document.getElementsByTagName("li"))
    tags.forEach(element => {
        element.style.backgroundColor = "#f5f7fa"
    });
    //为选中的小li添加背景样式
    e.target.style.backgroundColor = "pink"
}

//为页面添加鼠标事件
root.onmouseup = function(e) {
    //如果鼠标松开区域在ul容器外,隐藏ul容器,并设置输入框的边框样式
    //此处会和输入框点击事件冲突,点击输入框会同时执行页面根元素的点击事件和输入框点击事件,都会隐藏选择列表,
    //但是由于input:focus的css权重要大于_getElementById("input").style.border = "1px solid #dcdfe6"的css权重,
    //所以点击输入框,最终还是会显示为蓝色边框。
    if(e.target.id !== "warp") {
        _getElementById("warp").style.display = "none"
        _getElementById("input").style.border = "1px solid #dcdfe6"
    }
    //如果鼠标松开区域不在输入框和ul内部,就给up取反,此种方法有点取巧
    //通过选择列表和文本框的e.path.length来判断,刚好它们的值都为6
    //点击区域在选择列表和文本框外需要对up进行取反,选择列表和文本框点击事件都已经有对up取反。
    if(e.path.length !== 6) {
        up = !up
    }
    setDownIcon()
}

写在最后

  js功底很重要,大厂都比较注重

  js功底很重要,大厂都比较注重

  js功底很重要,大厂都比较注重

  重要的话说三遍,做完小程序项目,以后有时间需要多多学习和练习js。麻烦学会的兄dei比个小心心哦。