In NOVA LAB, advanced interface systems require sophisticated directives. Let's explore three popular patterns: v-click-outside, v-tooltip, and v-intersection.
Detecting clicks outside an element — ideal for closing menus, dropdowns, and modals:
1const vClickOutside = {
2 mounted(el, binding) {
3 el._clickOutside = (event) => {
4 if (!el.contains(event.target)) {
5 binding.value(event)
6 }
7 }
8 document.addEventListener('click', el._clickOutside)
9 },
10 unmounted(el) {
11 document.removeEventListener('click', el._clickOutside)
12 }
13}Usage:
1<script setup>
2import { ref } from 'vue'
3
4const menuOpen = ref(false)
5
6const vClickOutside = {
7 mounted(el, binding) {
8 el._clickOutside = (event) => {
9 if (!el.contains(event.target)) {
10 binding.value(event)
11 }
12 }
13 document.addEventListener('click', el._clickOutside)
14 },
15 unmounted(el) {
16 document.removeEventListener('click', el._clickOutside)
17 }
18}
19</script>
20
21<template>
22 <div v-click-outside="() => menuOpen = false">
23 <button @click="menuOpen = !menuOpen">Menu</button>
24 <ul v-if="menuOpen">
25 <li>Option 1</li>
26 <li>Option 2</li>
27 </ul>
28 </div>
29</template>Important: In the
unmounted hook, we remove the listener to avoid memory leaks. We store the handler reference on the element (el._clickOutside).A simple tooltip displayed on mouse hover:
1const vTooltip = {
2 mounted(el, binding) {
3 const tooltip = document.createElement('div')
4 tooltip.className = 'v-tooltip'
5 tooltip.textContent = binding.value
6 tooltip.style.cssText = `
7 position: absolute; background: #333; color: white;
8 padding: 6px 12px; border-radius: 6px; font-size: 12px;
9 pointer-events: none; opacity: 0; transition: opacity 0.2s;
10 z-index: 9999;
11 `
12
13 el.style.position = 'relative'
14 el.appendChild(tooltip)
15
16 el._showTooltip = () => { tooltip.style.opacity = '1' }
17 el._hideTooltip = () => { tooltip.style.opacity = '0' }
18
19 el.addEventListener('mouseenter', el._showTooltip)
20 el.addEventListener('mouseleave', el._hideTooltip)
21 },
22 updated(el, binding) {
23 const tooltip = el.querySelector('.v-tooltip')
24 if (tooltip) tooltip.textContent = binding.value
25 },
26 unmounted(el) {
27 el.removeEventListener('mouseenter', el._showTooltip)
28 el.removeEventListener('mouseleave', el._hideTooltip)
29 }
30}Detecting when an element appears in the viewport — ideal for lazy loading and entrance animations:
1const vIntersection = {
2 mounted(el, binding) {
3 const observer = new IntersectionObserver(
4 (entries) => {
5 entries.forEach(entry => {
6 if (entry.isIntersecting) {
7 binding.value(entry)
8 if (binding.modifiers.once) {
9 observer.unobserve(el)
10 }
11 }
12 })
13 },
14 { threshold: binding.arg ? parseFloat(binding.arg) : 0.1 }
15 )
16
17 el._observer = observer
18 observer.observe(el)
19 },
20 unmounted(el) {
21 if (el._observer) {
22 el._observer.disconnect()
23 }
24 }
25}Usage:
1<template>
2 <div v-intersection.once="onVisible">
3 Will appear when the element is visible
4 </div>
5</template>Every listener, observer, or timer added in
MUST be removed in mounted
. Otherwise, memory leaks will occur.unmounted
Pattern:
mounted — add the handler and store the reference on elunmounted — remove the handler using the stored reference