Nuxt 项目实战 - 14:实现两个非常实用的自定义指令

场景

  1. 光标移入一个元素执行执行一段逻辑,比如:按钮颜色要变,其他弹框要显示,光标移出元素时又恢复。如果仅仅是样式不同可直接css,但是要执行js代码就有点麻烦了。可能你立马会想到mouseenter和mouseleave。但是如果有多层元素都需要这个就没有那么优雅了

  2. 点击一个按钮弹出modal对话框,背景是黑色透明,只有中间一部分是显示的内容,点击内容外面就关闭对话框。

实现自定义自定v-hover-el

  • 使用方式

    html 复制代码
    <template>
      <div class="root">
        <button v-hover-el="(v: boolean) => isHoverEl = v">isHoverEl: {{ isHoverEl }}</button>
      </div>
    </template>
    <script setup lang="ts">
    const isHoverEl = ref(false);
    </script>
  • 代码实现

    tsx 复制代码
    import type { ObjectDirective, DirectiveBinding } from 'vue';
    
    type FlushList = Map<
    	HTMLElement,
    	{
    		bindingFn: (isMouseEnter: boolean, event: MouseEvent) => void,
    		enterFn: (event: MouseEvent) => void,
    		leaveFn: (event: MouseEvent) => void,
    	}
    >
    
    const nodeList: FlushList = new Map();
    
    function executeHandler(isMouseEnter: boolean, event: MouseEvent) {
    	let target = event.target as HTMLElement;
    	for (let [key, value] of nodeList.entries()) {
    		//! 说明:如果触发事件target是遍历的元素时就触发调用绑定的方法
    		if (key == target) {
    			value.bindingFn(isMouseEnter, event);
    		}
    	}
    }
    
    function addListener(el: HTMLElement, enterListener: any, leaveListener: any) {
    	el.addEventListener("mouseenter", enterListener);
    	el.addEventListener("mouseleave", leaveListener);
    }
    
    function delListener(el: HTMLElement, enterListener: any, leaveListener: any) {
    	el.removeEventListener("mouseenter", enterListener);
    	el.removeEventListener("mouseleave", leaveListener);
    }
    
    const HoverEl: ObjectDirective = {
    	beforeMount(el: HTMLElement, binding: DirectiveBinding) {
    		let options = {
    			bindingFn: binding.value,
    			enterFn: executeHandler.bind(null, true),
    			leaveFn: executeHandler.bind(null, false)
    		}
    		//! 挂载阶段:
    		//! 1.记录设置了指令的元素
    		nodeList.set(el, options);
    		//! 2.给元素添加移入移出事件
    		addListener(el, options.enterFn, options.leaveFn);
    	},
    	unmounted(el: HTMLElement) {
    		let options = nodeList.get(el);
    		//! 卸载阶段:
    		//! 需要把事件移除掉
    		if (options) {
    			delListener(el, options.enterFn, options.leaveFn);
    		}
    		nodeList.delete(el);
    	}
    }
    
    export { HoverEl }

实现自定义指令v-click-outside

  • 使用方式

    html 复制代码
    <template>
      <div class="root">
        <button @click="isDialogVis = true">open dialog</button>
        <div class="dialog"
             v-if="isDialogVis">
          <div class="wrapper"
               v-click-outside="() => isDialogVis = false">
            This is a dialog
            <div class="tips">
              tips: the dialog will close if you click outside the border.
            </div>
          </div>
        </div>
      </div>
    </template>
    
    <script setup lang="ts">
    const isDialogVis = ref(false);
    </script>
  • 代码实现

    tsx 复制代码
    import type { ObjectDirective, DirectiveBinding } from 'vue';
    import { isClient } from '@vueuse/core'
    
    type DocumentHandler = <T extends MouseEvent>(mouseup: T, mousedown: T) => void
    type FlushList = Map<
    	HTMLElement,
    	{
    		documentHandler: DocumentHandler
    		bindingFn: (...args: unknown[]) => unknown
    	}
    >
    
    const nodeList: FlushList = new Map()
    //! 记录点击的开始事件对象
    let startClick: MouseEvent;
    
    if (isClient) {
    	//! 点击事件绑定在document上
    	document.addEventListener('mousedown', (e: MouseEvent) => (startClick = e))
    	document.addEventListener('mouseup', (e: MouseEvent) => {
    		for (const handler of nodeList.values()) {
    			const { documentHandler } = handler;
    			documentHandler(e as MouseEvent, startClick)
    		}
    	});
    }
    
    function createDocumentHandler(
    	el: HTMLElement,
    	binding: DirectiveBinding
    ): DocumentHandler {
    	let excludes: HTMLElement[] = []
    	if (Array.isArray(binding.arg)) {
    		excludes = binding.arg
    	} else if (isElement(binding.arg)) {
    		excludes.push(binding.arg as unknown as HTMLElement)
    	}
    
    	//! 事件处理
    	return function (mouseup, mousedown) {
    		const mouseUpTarget = mouseup.target as Node
    		const mouseDownTarget = mousedown?.target as Node
    		const isContainedByEl =
    			el.contains(mouseUpTarget) || el.contains(mouseDownTarget);
    		const isSelf = el === mouseUpTarget;
    		//! 如果点击是父容器元素 or 元素本身不触发事件,否则:就是点击元素外面了,需要抛事件
    		if (isContainedByEl || isSelf) {
    			return;
    		}
    		binding.value(mouseup, mousedown);
    	}
    }
    
    const ClickOutside: ObjectDirective = {
    	beforeMount(el: HTMLElement, binding: DirectiveBinding) {
    		//! 挂载阶段:
    		//! 记录设置了指令的元素 && 给元素绑定事件
    		nodeList.set(el, {
    			documentHandler: createDocumentHandler(el, binding),
    			bindingFn: binding.value,
    		});
    	},
    	unmounted(el: HTMLElement) {
    		//! 卸载阶段:
    		//! 需要把元素移除
    		nodeList.delete(el)
    	}
    }
    
    export { ClickOutside }

注册自定义指令

tsx 复制代码
import { defineNuxtPlugin } from '#app'
import { ClickOutside } from '~/composables/directives/click-outside'
import { HoverEl } from '~/composables/directives/hover-el'

export default defineNuxtPlugin((nuxtApp) => {
	nuxtApp.vueApp.directive("click-outside", ClickOutside)
	nuxtApp.vueApp.directive("hover-el", HoverEl)
})

说明:我这个是Nuxt3 中的注册方式,Vue 改成app.directive的方式就可以了

总结

  • 不知到怎么弄就给别人多借鉴借鉴
  • 实现功能只是最基本的完成任务,如果能用更优雅的方式解决那么就发挥你的潜力去实现,或许这是你提升的有效方式

demo 传送门

参考文献

相关推荐
m0_748235616 分钟前
从零开始学前端之HTML(三)
前端·html
一个处女座的程序猿O(∩_∩)O2 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink5 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者7 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-7 小时前
验证码机制
前端·后端
燃先生._.8 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖9 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235249 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_7482402510 小时前
前端如何检测用户登录状态是否过期
前端