1.介绍clickoutside
当我们开发一个dropdown组件或者有这么一个场景,点击一个button,打开一个浮层来展示一些不常用信息,当我们想关闭它的时候,我们不希望只能通过点击那个button触发,而是希望点击除了这个浮层以外的区域都能把它关闭,这样就能大大提高用户的体验感。
本例中,使用vue3全局注册自定义指令来实现这个功能,方便全局使用,效果如下:
2.介绍自定义指令
Vue3允许注册自定义指令。当你需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。下面引入一下vue官网对自定义指令的介绍。
1.生命周期钩子
- created — 在绑定元素的 attribute 或事件监听器被应用之前调用
- beforeMount — 在绑定元素的父组件挂载之前调用
- mounted — 绑定元素的父组件被挂载时调用
- beforeUpdate — 在包含组件的 VNode 更新之前调用
- updated — 在包含组件的 VNode 及其子组件的 VNode 更新之后调用
- beforeUnmount — 在绑定元素的父组件卸载之前调用
- unmounted — 卸载绑定元素的父组件时调用
2.指令钩子参数
以下主要介绍指令钩子常用参数,详细请移步官网。
-
el
指令绑定到的元素,可用于直接操作 DOM。
-
binding
包含以下 property 的对象。
- instance:使用指令的组件实例。
- value:传递给指令的值。例如,在 v-my-directive=”1 + 1″ 中,该值为 2。
- oldValue:先前的值,仅在 beforeUpdate 和 updated 中可用。值是否已更改都可用。
- arg:参数传递给指令 (如果有)。例如在 v-my-directive:foo 中,arg 为 “foo”。
- modifiers:包含修饰符 (如果有) 的对象。例如在 v-my-directive.foo.bar 中,修饰符对象为 {foo: true,bar: true}。
- dir:一个对象,在注册指令时作为参数传递。
3.思路
在vue指令中可以获得指令所绑定的元素el
,然后监听浏览器的click
事件(别人有用mouseup
和mousedown
事件来实现的,可能会更好,我这里先简单用click
事件),判断事件中鼠标位置对应的 dom
是否属于 el
,是的话说明点击区域在el
内部,此时不做任何处理,否的话说明点击了el
的外部,即 clickoutside,然后进行后续逻辑处理,关闭浮层等
4.代码实战
element-plus源码中有一个关于clickoutside指令的实现,采用的是mouseup和mousedown事件,支持绑定多个元素作为一个inside,更加严谨强大,感兴趣的同学可以去学习一下。
// directives/clickoutside.ts
import { DirectiveBinding, ObjectDirective } from 'vue'
type DocumentHandler = <T extends MouseEvent>(e:T) => void
interface ListProps {
documentHandler?: DocumentHandler
}
let nodeList: ListProps = {}
function createDocumentHandler(
el: HTMLElement,
binding: DirectiveBinding
): DocumentHandler {
return function (e: MouseEvent) {
const target = e.target as HTMLElement
if (el.contains(target)) {
return false
}
if (binding.arg) {
binding.value(e)
}
}
}
const handler = (e: MouseEvent) => {
const { documentHandler } = nodeList
if (documentHandler) {
documentHandler(e)
}
}
window.addEventListener('click', handler)
const ClickOutSide: ObjectDirective = {
beforeMount(el, binding) {
nodeList = {
documentHandler: createDocumentHandler(el, binding)
}
},
updated(el, binding) {
nodeList = {
documentHandler: createDocumentHandler(el, binding)
}
},
unmounted() {
window.removeEventListener('click', handler)
}
}
export default ClickOutSide
复制代码
注册全局指令
//main.ts
...
import ClickOutside from './directives/clickoutside'
...
const app = createApp(App)
app.directive('click-outside', ClickOutside)
...
复制代码
使用示例
// Dropdown.ts
<template>
<div class="dropdown" ref="dropdownRef" v-click-outside:[dropdownRef]="handleClickOutside">
<a href="#" class="btn btn-outline-light my-2 dropdown-toggle text-primary" @click.prevent="toggleOpen">
{{title}}
</a>
<ul class="dropdown-menu" v-if="isOpen" style="display:block" >
<slot name="dropdown"></slot>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from 'vue'
export default defineComponent({
name: 'Dropdown',
props: {
title: {
type: String,
default: ''
}
},
setup() {
const isOpen = ref(false)
const dropdownRef = ref < null | HTMLElement >(null)
const toggleOpen = () => {
isOpen.value = !isOpen.value
}
const handleClickOutside = () => {
console.log('clickoutside---点击了外部')
if (isOpen.value) {
isOpen.value = false
}
}
return {
isOpen,
toggleOpen,
dropdownRef,
handleClickOutside
}
}
})
</script>
<style scoped>
.dropdown-menu{
min-width: unset;
}
</style>
复制代码
如有发现bug或可以优化的地方,还望大佬不吝赐教
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END