vue3+ts实现自定义指令v-click-outside

1.介绍clickoutside

当我们开发一个dropdown组件或者有这么一个场景,点击一个button,打开一个浮层来展示一些不常用信息,当我们想关闭它的时候,我们不希望只能通过点击那个button触发,而是希望点击除了这个浮层以外的区域都能把它关闭,这样就能大大提高用户的体验感。

本例中,使用vue3全局注册自定义指令来实现这个功能,方便全局使用,效果如下:

clickoutside.gif

2.介绍自定义指令

Vue3允许注册自定义指令。当你需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。下面引入一下vue官网对自定义指令的介绍。

vue3自定义指令-directive

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事件(别人有用mouseupmousedown事件来实现的,可能会更好,我这里先简单用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
喜欢就支持一下吧
点赞0 分享