概述
一般后台项目结合分页组件用到这个dropdown组件,用来做显示分页条数,其他用到这个组件的地方倒不是很多,其实现思路和select组件有那么些相似,现记录下这个组件的实现。
最终效果(动图没显示出来,请稍定会儿,可以先看后面)
实现原理
这个组件和select组件记起相似,可以参考我之前的文章【手写vue2select下拉组件】,要做这个组件,需要注意以下几点:
组件分为两部分:
- 供我们点击的文字,按钮,超链接等等(当成插槽供用户提供)
- 下拉菜单项(支持边框,禁用等)
使用该组件应当提供的事件应该是点击item项,然后将对应的item的对应value暴露出来,供用户使用。
组件菜单项的显示隐藏需要过渡动画。
默认菜单项方向向下,当下方可视区的高度不足以容纳下拉菜单的高度的时候,自动让菜单方向向上。
具体实现
目录结构
emitter.js
这个在之前的组件实现过程中介绍过这个文件,主要是为了解决跨多层级父子组件之前数据通信的,本质上实现原理为发布订阅模式。
// 广播通知事件
function _broadcast(componentName, eventName, params) {
// 遍历当前组件的子组件
this.$children.forEach(function (child) {
// 取出componentName,组件options上面可以自己配置
var name = child.$options.componentName;
// 如果找到了需要通知的组件名,触发组件上面的$eimit方法,触发自定义事件
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
// 没找到,递归往下找
_broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
const emiiter = {
methods: {
// 派发事件(通知父组件)
dispatch(componentName, eventName, params) {
var parent = this.$parent || this.$root;
var name = parent.$options.componentName;
// 循环往上层父组件,知道知道组件名和需要触发的组件名相同即可,然后触发对应组件的事件
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
// 广播事件(通知子组件)
broadcast(componentName, eventName, params) {
_broadcast.call(this, componentName, eventName, params);
},
},
};
export default emiiter;
MyDropdown.vue
主要暴露给用户使用的组件
<template>
<div
class="my-dropdown"
@click.stop="trigger == 'click' ? (showMenu = !showMenu) : ''"
@mouseenter="trigger == 'hover' ? (showMenu = true) : ''"
@mouseleave="trigger == 'hover' ? (showMenu = false) : ''"
ref="myDropDdown"
>
<div class="tip-text" ref="tipText">
<slot></slot>
</div>
<slot name="list"></slot>
</div>
</template>
<script>
import emitter from "./emitter";
export default {
name: "MyDropdown",
componentName: "MyDropdown",
mixins: [emitter],
props: {
// 触发显示方式
trigger: {
type: String,
default: "click",
},
// 下来菜单的出现位置(上方,下方)
placement: {
type: String,
default: "bottom",
validator: function (value) {
// 这个值必须匹配下列字符串中的一个
return ["bottom", "top"].includes(value);
},
},
},
data() {
return {
//控制菜单是否显示
showMenu: false,
};
},
mounted() {
//初始化自定义事件
this.initEvent();
},
methods: {
// 初始化
initEvent() {
//订阅当item点击的时候,发布on-click事件,告知外部
this.$on("item-click", (params) => {
this.$emit("on-click", params);
this.showMenu = false;
});
//空白点击要隐藏菜单,需要执行的函数需要绑定this指向
this.handleEmptyDomElementClickBindThis =
this.handleEmptyDomElementClick.bind(this);
window.addEventListener("click", this.handleEmptyDomElementClickBindThis);
},
// 处理空白区域点击,隐藏菜单列表
handleEmptyDomElementClick(e) {
if (!Array.from(this.$refs.myDropDdown.childNodes).includes(e.target)) {
this.showMenu = false;
}
},
},
beforeDestroy() {
// 移除window上面的事件
window.removeEventListener(this.handleEmptyDomElementClickBindThis);
},
watch: {
//变化的时候,通知子组件隐藏菜单列表
showMenu() {
this.broadcast("MyDropdownMenu", "set-menu-status", this.showMenu);
},
},
};
</script>
<style lang="less">
.my-dropdown {
position: relative;
}
</style>
MyDropdownMenu.vue
主要暴露给用户使用的组件,菜单列表组件
<template>
<!-- 涉及到高度,位移,过渡使用js钩子函数的方式比较好处理些 -->
<transition
@before-enter="beforeEnter"
@enter="enter"
@leave="leave"
v-bind:css="false"
>
<div class="my-dropdown-menu" v-if="showMeune" ref="myDroupdownMenu">
<slot></slot>
</div>
</transition>
</template>
<script>
import emitter from "./emitter";
export default {
name: "MyDropdownMenu",
componentName: "MyDropdownMenu",
mixins: [emitter],
data() {
return {
showMeune: false,
timer: null,
};
},
mounted() {
this.$on("set-menu-status", (status) => {
this.showMeune = status;
});
},
methods: {
//进入前,初始化需要过渡的属性
beforeEnter: function (el) {
// 初始化
el.style.opacity = 0;
el.style.transform = "scaleY(0)";
el.style.transformOrigin = "top center";
},
//dom进入
enter: function (el, done) {
//获取文档可视区高度
const htmlClientHeight = document.documentElement.clientHeight;
//菜单列表相对于父元素的top偏移量
const offsetTop = el.offsetTop;
const scrollHeight = el.scrollHeight;
//获取当前元素和可视区的一些长度(top,left,bottom等)
const { bottom } = el.getBoundingClientRect();
// 说明底部高度不够显示菜单了,这时候我们需要调整菜单朝上面显示
if (htmlClientHeight - bottom < scrollHeight) {
el.style.transformOrigin = "bottom center";
el.style.top = -(scrollHeight + 20) + "px";
} else {
//查看是否placement属性,是的话我们主动处理
if (this.$parent.placement == "top") {
el.style.transformOrigin = "bottom center";
el.style.top = -(scrollHeight + 20) + "px";
} else {
el.style.top = offsetTop + "px";
}
}
el.style.transform = "scaleY(1)";
el.style.opacity = 1;
//根据官网事例,必须在enter和leave里面调用done函数,不然过渡动画不生效(切记)
done();
},
//dom元素离开
leave: function (el, done) {
el.style.transform = "scaleY(0)";
el.style.opacity = 0;
clearTimeout(this.timer);
this.timer = setTimeout(() => {
//根据官网事例,必须在enter和leave里面调用done函数,不然过渡动画不生效(切记)
done();
}, 250);
},
},
};
</script>
<style lang="less">
.my-dropdown-menu {
min-width: 100px;
max-height: 200px;
overflow: auto;
margin: 5px 0;
padding: 5px 0;
background-color: #fff;
box-sizing: border-box;
border-radius: 4px;
box-shadow: 0 1px 6px rgb(0 0 0 / 20%);
z-index: 900;
transform-origin: top center;
position: absolute;
transition: transform 0.25s ease, opacity 0.25s ease;
}
</style>
MyDropdownItem.vue
主要暴露给用户使用的组件,菜单列表项组件,组件内容很简单,主要就是展示item数据和绑定点击事件。
<template>
<div
:class="[
'my-dropdownItem',
divided ? 'my-dropdownItem-divided' : '',
disabled ? 'my-dropdownItem-disabled' : '',
]"
@click.stop="handleItemClick"
>
<slot></slot>
</div>
</template>
<script>
import emitter from "./emitter";
export default {
name: "MyDropdownItem",
componentName: "MyDropdownItem",
mixins: [emitter],
props: {
divided: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
name: {
type: String,
default: "",
},
},
data() {
return {};
},
methods: {
handleItemClick() {
if (this.disabled) return;
// item项点击通知dropdown组件派发到外部的自定义事件
this.dispatch("MyDropdown", "item-click", this.name);
},
},
};
</script>
<style lang="less">
.my-dropdownItem {
margin: 0;
line-height: normal;
padding: 7px 16px;
clear: both;
color: #515a6e;
font-size: 14px !important;
white-space: nowrap;
list-style: none;
cursor: pointer;
transition: background 0.2s ease-in-out;
&:hover {
background: #f3f3f3;
}
}
.my-dropdownItem-divided {
border-bottom: 1px solid #eee;
}
.my-dropdownItem-disabled {
color: #cacdd2;
cursor: not-allowed;
&:hover {
background: #fff;
}
}
</style>
总结
类似组件库中的这种经常出现跨多层级组件通信,需要特殊处理,一般emitter.js文件里面的封装我们在开发中一般是用不到的,我们写组件经常也就父子组件之间,很少会跨祖孙级别,但是在组件库中,这种关系就很多,因此需要单独利用发布订阅来处理,这种模式用到实际项目里面也是很管用的。
以上就是手写实现vue2下拉菜单dropdown组件实例的详细内容,更多关于vue 下拉菜单dropdown的资料请关注编程网其它相关文章!