原生JavaScript实现自定义下拉选择组件
本文介绍如何使用原生JavaScript开发一个自定义下拉选择组件。通过事件委托机制解决动态生成元素的事件监听问题,实现成人及儿童人数选择、儿童年龄动态显示等功能。文章详细解析了组件的结构设计、样式控制及核心逻辑,并提供完整代码参考。
原生JavaScript自定义下拉框组件开发
本文分享了一个使用JavaScript实现自定义下拉框选择组件的具体代码示例,供读者参考学习。
组件功能需求分析
该组件主要满足以下功能需求:
1. 点击容器div后,div进入聚焦状态,并展开显示下拉选项内容; 2. 当用户选择儿童人数且数值大于0时,界面下方动态生成对应数量的儿童年龄选择框; 3. 成人人数的可选范围限制为1至7人,儿童人数限制为0至4人,儿童年龄分为小于1岁和1至17岁两个区间; 4. 点击确认按钮后,将最终选定的成人和儿童人数信息回显至顶部的div区域; 5. 支持通过参数控制选择框是否处于可交互状态; 6. 当多个ul列表同时存在时,点击新的ul列表会自动隐藏之前显示的列表; 7. 点击已展开列表区域外(除绑定事件元素外)的任何位置,当前显示的ul列表将自动隐藏; 8. 点击页面任意空白区域,整个下拉框内容将被整体隐藏。
当组件处于不可操作状态时的视觉表现如下:

当组件处于可操作状态时的视觉表现如下:

选定儿童人数后,界面下方会自动生成对应数量的儿童年龄选择框:

点击确认按钮后,选定的结果将显示在顶部的div区域内:

事件处理与委托机制设计
在初始构思阶段,计划对select元素、ul下拉列表以及btn按钮分别绑定独立的事件监听器。同时,还需要处理点击下拉列表内部其他位置时隐藏ul列表,以及点击body时隐藏整个下拉框内容的逻辑。这种方案存在监听事件过多、事件冒泡可能导致执行多次等复杂性问题。
由于儿童年龄选择框是根据儿童人数动态创建的,数量不固定,因此无法直接针对每个生成的select元素绑定事件。为了解决这一问题,采用事件委托机制,将所有针对select、ul下拉列表、btn按钮的点击事件,以及点击container内部其他位置时隐藏ul列表的逻辑,统一委托给dropDownContainer元素处理。
以下是具体的代码实现部分。
核心代码实现
HTML结构代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>select</title>
</head>
<body>
<script type="module">
import Main from './js/Main.js';
//参数为false时,选择框不可点击;为true时,选择框可使用
let main=new Main(true);
main.appendTo("body");
</script>
</body>
</html>
Main.js文件主要逻辑如下:
import Utils from './Utils.js';
export default class Main{
static styles=false;
listPrep;
constructor(state){
//state控制下拉框是否可点击
this.state=state;
this.elem=this.createE();
}
createE(){
if(this.elem) return this.elem;
let div=Utils.createE("div");
div.className="guestsNum";
div.innerHTML=`<span>人数未定</span><i></i>
<div class="dropDownContainer none" id="dropDownContainer">
<div class="dropDownItem clearfix">
<span>每间</span>
<div class="dropDownSelect">
<div class="dropDownCont"><span id="adultNum">2 成人</span><i></i></div>
<ul class="dropDownList" tag="adult">${this.setDropDownList("adult")}</ul>
</div>
<div class="dropDownSelect">
<div class="dropDownCont"><span id="childrenNum">0 儿童</span><i></i></div>
<ul class="dropDownList" tag="children"><li>0</li>${this.setDropDownList("children")}</ul>
</div>
</div>
<div class="dropDownItem clearfix none" id="ItemAge"></div>
<div class="dropDownBottom clearfix">
${this.state?'':'<em class="dropDownTips">请优先选择日期,以便查询实时价格。</em>'}
${this.state?'<a class="dropDownBtn" id="dropDownBtn" href="javascript:void(0)" rel="external nofollow" rel="external nofollow" >确认</a>':'<a class="dropDownBtn disabled" href="javascript:void(0)" rel="external nofollow" rel="external nofollow" >确认</a>'}
</div>
</div>`;
//设置样式,因为样式只设置一次就好,不需要实例化,所以使用静态方法
Main.setStyles();
//获取元素
Utils.getIdElem(div,this);
//监听div的点击事件
div.addEventListener("click",(e)=>this.guestsNumClickHandler(e));
//如果state为true,下拉框监听点击事件
if(this.state) this.dropDownContainer.addEventListener("click",e=>this.dropDownContainerClick(e));
//document监听点击事件,隐藏下拉框
document.addEventListener("click",e=>this.documentClick(e));
return div;
}
appendTo(parent){
Utils.appendTo(this.elem,parent);
}
guestsNumClickHandler(e){
//如果下拉框是显示状态,则直接跳出,避免重复操作
if(!Utils.hasClass(this.dropDownContainer,"none")) return;
//如果点击的不是guestsNum,直接跳出,避免事件冒泡
if(e.target.nodeName!=="SPAN"&&e.target.nodeName!=="I"&&!Utils.hasClass(e.target,"guestsNum")) return;
//给div添加聚集样式
Utils.addClass(this.elem,"focus");
//将dropDownContainer显示
Utils.removeClass(this.dropDownContainer,"none");
}
dropDownContainerClick(e){
if(e.target.nodeName==="LI"){
//点击ul选择列表
this.dropDownListClick(e);
}
else if(e.target.id==="dropDownBtn"){
//点击确认按钮
this.dropDownBtnClick();
}
else if(e.target.nodeName==="SPAN" || e.target.nodeName==="I") {
//点击span或者i标签时,将它们的父元素div作为参数
this.dropDownSelectClick(e.target.parentElement);
}
else if(Utils.hasClass(e.target,"dropDownCont")){
//点击div选择框时,将div作为参数
this.dropDownSelectClick(e.target);
}
else {
//点击下拉框内其它位置时,让当前的ul列表隐藏
if(this.listPrep) this.listPrep.style.display="none";
}
}
dropDownSelectClick(div){
//隐藏掉上一个显示的ul列表
if(this.listPrep) this.listPrep.style.display="none";
//当前点击的ul列表赋值给this.listPrep
this.listPrep=div.nextElementSibling;
//将当前点击的ul列表显示
this.listPrep.style.display="block";
}
dropDownListClick(e){
//获取当前点击的ul的tag属性值
let tag=this.listPrep.getAttribute("tag");
let unit="";
switch (tag){
case "adult": unit="成人";break;
case "children":
unit="儿童";
let txt=Number(e.target.innerText);
//根据li的数值,自动创建下面的年龄选择框
this.setDropDownItemAge(txt);
break;
case "age": unit="岁";break;
}
//将选择的li的值,显示出来
this.listPrep.previousElementSibling.firstElementChild.textContent=e.target.innerText+" "+unit;
//显示完成后,将当前显示的ul隐藏
this.listPrep.style.display="none";
}
setDropDownItemAge(txt){
let str="<span>儿童年龄</span>";
if(txt===0){
//如果是0,则年龄选择框不显示
this.ItemAge.style.display="none";
}else{
this.ItemAge.style.display="block";
//循环选择的数值,创建年龄选择框
for(let i=0;i<txt;i++){
str+=`<div class="dropDownSelect">
<div class="dropDownCont"><span><1岁</span><i></i></div>
<ul class="dropDownList" tag="age"><li><1</li>${this.setDropDownList("age")}</ul>
</div>`;
}
this.ItemAge.innerHTML=str;
}
}
dropDownBtnClick(){
//将选择的内容显示在最上方的select框内
let resultStr=this.adultNum.innerText.replace(/\s/g,"")+" "+this.childrenNum.innerText.replace(/\s/g,"");
this.elem.firstElementChild.textContent=resultStr;
//隐藏dropDownContainer
this.dropDownContainerHide();
}
documentClick(e){
//避免事件冒泡
if(e.target!==document.documentElement && e.target!==document.body) return;
//隐藏dropDownContainer
this.dropDownContainerHide();
}
dropDownContainerHide(){
//div去掉聚集状态
Utils.removeClass(this.elem,"focus");
//dropDownContainer隐藏
Utils.addClass(this.dropDownContainer,"none");
//隐藏当前显示的ul列表
if(this.listPrep) this.listPrep.style.display="none";
}
setDropDownList(type){
//创建ul下拉列表内容
let li="";
let max=0;
switch (type){
case "adult": max=8;break;
case "children": max=5;break;
case "age": max=18;break;
}
for(let i=1;i<max;i++){
li+="<li>"+i+"</li>";
}
return li;
}
static setStyles(){
if(Main.styles) return;
Main.style=true;
Utils.insertCss(".guestsNum",{
width:"108px",
height:"34px",
padding:"0px 12px",
border:"1px solid #ccc",
borderRadius:"3px",
position:"relative",
fontSize:"14px",
color:"#666",
userSelect:"none",
})
Utils.insertCss(".guestsNum.focus",{
borderColor:"#ffa800",
boxShadow:"0 0 4px #ffa800"
})
Utils.insertCss(".guestsNum>span",{
lineHeight:"34px"
})
Utils.insertCss(".guestsNum>i",{
display:"inline-block",
width:"16px",
height:"16px",
backgroundImage:"url(./image/user.jpg)",
float:"right",
margin:"8px 0px 0px 10px"
})
Utils.insertCss(".dropDownContainer",{
border: "1px solid #ffa800",
borderRadius: "4px",
boxShadow: "0 0 4px #ffa800",
backgroundColor: "#fff",
padding: "20px 15px",
width: "480px",
fontSize:"12px",
position:"absolute",
left:"0px",
top:"35px",
})
Utils.insertCss(".dropDownItem",{
marginBottom:"12px"
})
Utils.insertCss(".dropDownItem>span",{
display:"block",
width:"60px",
lineHeight:"28px",
float:"left",
})
Utils.insertCss(".dropDownSelect",{
width:"90px",
height:"30px",
marginRight:"10px",
float:"left",
position:"relative"
})
Utils.insertCss(".dropDownCont",{
border:"1px solid #ccc",
borderRadius:"3px",
height:"12px",
padding:"6px 8px 10px",
})
Utils.insertCss(".dropDownCont>span",{
display:"inline-block",
width:"53px",
height:"14px",
lineHeight:"14px",
borderRight:"1px solid #ccc"
})
Utils.insertCss(".dropDownCont>i",{
display:"inline-block",
width:"0px",
height:"0px",
border:"5px solid #c6c6c6",
borderColor:"#c6c6c6 transparent transparent",
margin: "6px 0px 0px 4px",
float: "right"
})
Utils.insertCss(".dropDownList",{
listStyle:"none",
padding:"0px",
margin:"0px",
width:"88px",
maxHeight:"200px",
overflow:"auto",
cursor:"pointer",
border:"1px solid #ccc",
backgroundColor:"#fff",
borderRadius:"4px",
position:"absolute",
left:"0px",
top:"30px",
zIndex:"2",
boxShadow: "1px 1px 3px rgba(0,0,0,.1)",
display:"none"
})
Utils.insertCss(".dropDownList>li",{
lineHeight:"28px",
paddingLeft:"8px",
})
Utils.insertCss(".dropDownList>li:hover",{
background:"#f4f4f4"
})
Utils.insertCss(".dropDownBottom",{
borderTop:"1px solid #ccc",
marginTop:"20px",
paddingTop:"20px"
})
Utils.insertCss(".dropDownTips",{
fontStyle:"normal",
fontSize: "12px",
color: "#ef523d",
lineHeight:"28px"
})
Utils.insertCss(".dropDownBtn",{
textDecoration:"none",
float: "right",
display: "inline-block",
padding: "2px 22px",
backgroundColor: "#ffb200",
borderRadius: "4px",
fontSize: "14px",
lineHeight: "24px",
color: "#fff",
})
Utils.insertCss(".dropDownBtn.disabled",{
backgroundColor: "#efefef",
color: "#999"
})
Utils.insertCss(".clearfix:after",{
content:"\".\"",
display:"block",
overflow:"hidden",
visibility:"hidden",
clear:"both",
height:"0px"
})
Utils.insertCss(".none",{
display:"none"
})
}
}
Utils.js工具类文件代码如下:
export default class Utils{
static createE(elem,style,prep){
elem=document.createElement(elem);
if(style) for(let prop in style) elem.style[prop]=style[prop];
if(prep) for(let prop in prep) elem[prop]=prep[prop];
return elem;
}
static appendTo(elem,parent){
if (parent.constructor === String) parent = document.querySelector(parent);
parent.appendChild(elem);
}
static randomNum(min,max){
return Math.floor(Math.random*(max-min)+min);
}
static randomColor(alpha){
alpha=alpha||Math.random().toFixed(1);
if(isNaN(alpha)) alpha=1;
if(alpha>1) alpha=1;
if(alpha<0) alpha=0;
let col="rgba(";
for(let i=0;i<3;i++){
col+=Utils.randomNum(0,256)+",";
}
col+=alpha+")";
return col;
}
static insertCss(select,styles){
if(document.styleSheets.length===0){
let styleS=Utils.createE("style");
Utils.appendTo(styleS,document.head);
}
let styleSheet=document.styleSheets[document.styleSheets.length-1];
let str=select+"{";
for(var prop in styles){
str+=prop.replace(/[A-Z]/g,function(item){
return "-"+item.toLocaleLowerCase();
})+":"+styles[prop]+";";
}
str+="}"
styleSheet.insertRule(str,styleSheet.cssRules.length);
}
static getIdElem(elem,obj){
if(elem.id) obj[elem.id]=elem;
if(elem.children.length===0) return obj;
for(let i=0;i<elem.children.length;i++){
Utils.getIdElem(elem.children[i],obj);
}
}
static addClass(elem,className){
let arr=(elem.className+" "+className).match(/\S+/g);
arr=arr.filter((item,index)=>arr.indexOf(item,index+1)<0)
elem.className=arr.join(" ");
}
static removeClass(elem,className){
if(!elem.className) return;
let arr=elem.className.match(/\S+/g);
let arr1=className.match(/\S+/g);
arr1.forEach(item=>{
arr=arr.filter(t=>t!==item)
})
elem.className=arr.join(" ");
}
static hasClass(elem,className){
if(!elem.className) return false;
let arr=elem.className.match(/\S+/g);
let arr1=className.match(/\S+/g);
let res;
arr1.forEach(item=>{
res= arr.some(it=>it===item)
})
return res;
}
}
以上即为本文的全部内容,希望这些代码能对读者的学习有所帮助。欢迎继续关注本站以获取更多技术支持。
D3.js 处理大规模数据点的性能优化策略
2026-11-05 15:37:25
JS实现网页摄像头实时扫码:结合getUserMedia与jsQR教程
2026-11-05 15:27:28
D3.js v5 迁移到 v7 的 API 变更与注意事项
2026-11-05 11:57:17
使用 animate.js 实现多元素协同的序列动画
2026-11-04 22:27:53
JS拖拽排序时怎么添加平滑动画效果?FLIP技术实战
2026-11-04 10:12:22
