这篇文章我们一起来实现一个vue的下拉菜单组件。
像这种基本UI组件,网上已经有很多了,为什么要自己实现呢?其实并不是有意重复造*,而是想通过这个过程回顾一下vue组件开发的一些细节和注意事项。
为什么选择下拉菜单组件?
因为:麻雀虽小五脏俱全,这个小小的组件涉及到了不少vue组件开发的知识点。
好了,那就开始吧!
首先创建一个vue-cli的项目,笔者用的是vue-cli3,创建过程略,然后创建一个vue组件:DropDownList.vue
在编写模板之前,我们来分析一下这个组件的视图结构和功能。
下拉菜单组件应该由两部分组成:
- 选中项的文本
- 待选菜单(默认隐藏)
它的主要功能包括:
- 鼠标经过下拉菜单组件,显示待选菜单
- 鼠标滑出下拉菜单组件,隐藏待选菜单
- 鼠标点击待选菜单中的条目,选中项文本更新,组件派发change事件
我们编写如下这样的模板:
<template> <div class="zq-drop-list" @mouseover="onDplOver($event)" @mouseout="onDplOut($event)"> <span>选中项的文本<i></i></span> <ul> <li>北京</li> <li>上海</li> <li>广州</li> </ul> </div> </template>
选中项文本右侧的i标签,用来实现下拉菜单的三角形图标,在下文的css中我们用背景图来实现。
我们给根元素div已经添加了鼠标经过和滑出的回调函数,具体实现见下文。
接下来我们为这个下拉菜单编写样式,在模板下方添加style标签,为了防止和其他组件的样式发生冲突,笔者建议大家在开发组件时,都给style加上scoped属性。另外,笔者在这里用到了scss,具体代码如下:
<style scoped lang="scss"> .zq-drop-list{ display: inline-block; min-width: 100px; position: relative; span{ display: block; height: 30px; line-height: 30px; background: #f1f1f1; font-size: 14px; text-align: center; color: #333333; border-radius: 4px; i{ background: url(https://www.easyicon.net/api/resizeApi.php?id=1189852&size=16) no-repeat center center; margin-left: 6px; display: inline-block; } } ul{ position: absolute; top: 30px; left: 0; width: 100%; margin: 0; padding: 0; border: solid 1px #f1f1f1; border-radius: 4px; overflow: hidden; li{ list-style: none; height: 30px; line-height: 30px; font-size: 14px; border-bottom: solid 1px #f1f1f1; background: #ffffff; } li:last-child{ border-bottom: none; } li:hover{ background: #f6f6f6; } } } </style>
关于样式,这里就不详细展开了,只说其中几个需要注意的点:
- 那个i元素的样式,我用到了一个网络图片,大家可以自行更换
- 待选菜单ul在css里并没有让它隐藏,因为我们要通过js来控制,具体原因见下文
- 待选菜单ul使用了绝对定位,因为当它展开的时候,不应该影响页面上其他元素的布局
现在这个组件大概长这个样子:
1.png
我们继续为这个组件定义属性,很显然,待选菜单应该作为属性传进来,一定不能是内部写死的,属性定义如下:
<script> export default { name: "DropDownList", props:{ dataList:{ type:Array, default(){ return [ {name: "选项一"}, {name: "选项二"} ] } }, labelProperty:{ type:String, default(){ return "name" } } }, data(){ return { activeIndex:0 } }, }
其中dataList就是待选菜单的数据源属性,这里我们给这个属性定义了默认值,这也是笔者建议大家养成的一个习惯,作为一个组件,最好有默认值,因为当别人使用你的组件时,可以先不设置相关属性,就能看到一个成品的效果,也能快速查看你这个组件所需属性的数据细节。
另外一个属性是labelProperty,这个属性的作用是什么?我们实际项目中的数据源,并不一定都含有name这个字段,因此就可能导致下拉菜单无法渲染数据的文本,于是我们定义了这个属性用来指定实际数据源渲染文本的字段,这个字段必须是字符串。这个属性的默认值是name,因为它需要和默认数据源保持一致。相信你还看到了一个组件内部数据,activeIndex,这个是用来表示当前选中项的索引的,我们后面会用到。
现在我们就可以在其他地方引入并使用这个组件了,虽然它还没有完成,但我们不妨先让它显示在界面上吧:
<template> <div class="home"> <DropList :dataList="dplist" labelProperty="city" @change="onDpChange($event)"></DropList> <p>其他文本内容</p> </div> </template> <script> import DropList from '@/components/DropDownList.vue' //其他代码略 </script>
这个页面引入并使用了我们的DropDownList组件,:dataList="dplist" 绑定了当前页面的dplist数组到组件的dataList属性上,这个数组中的对象有一个city字段,我们希望此字段显示在下拉菜单上,因此我们设置组件的labelProperty为city,我们还给这个组件注册了change事件,这个组件内部需要派发这个事件,见下文。
现在我们回到组件的模板部分,发现它都还是静态内容,我们把这些静态内容修改为通过属性渲染。
<template> <div class="zq-drop-list" @mouseover="onDplOver($event)" @mouseout="onDplOut($event)"> <span>{{dplLable}}<i></i></span> <ul> <li v-for="(item, index) in dataList" :key="index" @click="onLiClick(index, $event)">{{item[labelProperty]}}</li> </ul> </div> </template>
其中待选菜单li的文本是 item[labelProperty] 这样就能正确的显示开发者指定的字段了。
我们看看选中项的文本表达式:dplLabel,我们并没有定义这个属性,也没有定义这个内部数据,它是哪儿来的?选中项的文本应该是 dataList[activeIndex][labelProperty] (这个很好理解吧,有问题请留言),但这个表达式太长了,写在模板里不利于维护,我们就把它写到计算属性里吧。
computed:{ dplLable(){ return this.dataList[this.activeIndex][this.labelProperty] } }
于是才有了上面的dplLabel,计算属性真的很好用呢。
现在下拉菜单的视图和数据关联部分我们已经写完了,接下来我们要实现它的功能。
第一步是先让待选菜单默认隐藏起来,这里我们为什么不直接用css的display:none呢,然后鼠标经过的时候display:block不就可以了吗?因为这样的话,我们无法实现点击待选菜单条目的时候让它隐藏,体验不好。我们用js来控制,但vue对直接访问dom元素支持的并不好,我们要想在组件初始化的时候访问dom元素,有一个最方便的做法,那就是:自定义指令。
我们为下拉菜单组件添加局部自定义指令,代码如下:
directives:{ dpl:{ bind(el){ el.style.display = "none"; } } },
这个dpl就是自定义指令啦,请忽略我笨拙的命名哈!然后我们在自定义指令的钩子函数bind方法中,访问el元素,控制它的style属性display:none; 最后,把这个自定义指令加到模板里面的ul标签上。别忘了要加v-,现在看看效果,待选菜单已经隐藏了。
<ul v-dpl>
我们利用自定义指令钩子函数访问dom元素,实现了对dom的控制,这一点非常实用!
让我们继续实现最开始为下拉菜单定义的鼠标经过和鼠标滑出的监听,实现待选菜单的显示与隐藏。
onDplOver(event){ let ul = event.currentTarget.childNodes[1]; ul.style.display = "block"; }, onDplOut(event){ let ul = event.currentTarget.childNodes[1]; ul.style.display = "none"; },
我们在鼠标事件中,访问event的currentTarget对象,为什么不是target?因为下拉菜单的子元素也会触发这个事件,如果访问target,可能不会是我们预期的顶层元素。
最后一步,我们实现待选菜单条目的点击事件,点击后,待选菜单隐藏,修改内部状态,派发change事件。
onLiClick(index){ let path = event.path || (event.composedPath && event.composedPath()) //兼容火狐和safari path[1].style.display = "none"; this.activeIndex = index; this.$emit("change", { index:index, value:this.dataList[index] }) }
这里有一个细节需要注意,我们要通过li元素找到外层ul元素,但path不支持火狐和safari,好在这两个浏览器支持composedPath,因此才有了第一行代码的兼容写法。然后通过修改内部数据activeIndex实现选中项文本的更新,最后调用emit方法向父元素派发change事件,别忘了把事件对象封装好传出去。
完整的代码如下:
<template> <div class="zq-drop-list" @mouseover="onDplOver($event)" @mouseout="onDplOut($event)"> <span>{{dplLable}}<i></i></span> <ul v-dpl> <li v-for="(item, index) in dataList" :key="index" @click="onLiClick(index, $event)">{{item[labelProperty]}}</li> </ul> </div> </template> <script> export default { name: "DropDownList", data(){ return { activeIndex:0 } }, props:{ dataList:{ type:Array, default(){ return [ {name: "选项一"}, {name: "选项二"} ] } }, labelProperty:{ type:String, default(){ return "name" } } }, directives:{ dpl:{ bind(el){ el.style.display = "none"; } } }, methods:{ onDplOver(event){ let ul = event.currentTarget.childNodes[1]; ul.style.display = "block"; }, onDplOut(event){ let ul = event.currentTarget.childNodes[1]; ul.style.display = "none"; }, onLiClick(index){ let path = event.path || (event.composedPath && event.composedPath()) //兼容火狐和safari path[1].style.display = "none"; this.activeIndex = index; this.$emit("change", { index:index, value:this.dataList[index] }) } }, computed:{ dplLable(){ return this.dataList[this.activeIndex][this.labelProperty] } } } </script> <style scoped lang="scss"> .zq-drop-list{ display: inline-block; min-width: 100px; position: relative; span{ display: block; height: 30px; line-height: 30px; background: #f1f1f1; font-size: 14px; text-align: center; color: #333333; border-radius: 4px; i{ background: url(https://www.easyicon.net/api/resizeApi.php?id=1189852&size=16) no-repeat center center; margin-left: 6px; display: inline-block; } } ul{ position: absolute; top: 30px; left: 0; width: 100%; margin: 0; padding: 0; border: solid 1px #f1f1f1; border-radius: 4px; overflow: hidden; li{ list-style: none; height: 30px; line-height: 30px; font-size: 14px; border-bottom: solid 1px #f1f1f1; background: #ffffff; } li:last-child{ border-bottom: none; } li:hover{ background: #f6f6f6; } } } </style>
以上为大家展示了vue如何实现一个下拉菜单组件,虽然比较简单,但也基本涉及到了组件开发常用的一些特性。