Vue.js course Β· Module 14: Directives & Plugins
Advanced Directives
In this lesson4
In NOVA LAB, advanced interface systems require sophisticated directives. Let's explore three popular patterns: v-click-outside, v-tooltip, and v-intersection.
v-click-outside
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).
v-tooltip
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}v-intersection (Intersection Observer)
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>Cleanup - The Golden Rule of Directives
Every listener, observer, or timer added in mounted MUST be removed in unmounted. Otherwise, memory leaks will occur.
Pattern:
- In
mounted- add the handler and store the reference onel - In
unmounted- remove the handler using the stored reference
Code for this lesson: App.vue
1<script setup>
2import { ref } from 'vue'
3
4// v-click-outside
5const vClickOutside = {
6 mounted(el, binding) {
7 el._clickOutside = (event) => {
8 if (!el.contains(event.target)) {
9 binding.value(event)
10 }
11 }
12 document.addEventListener('click', el._clickOutside)
13 },
14 unmounted(el) {
15 document.removeEventListener('click', el._clickOutside)
16 }
17}
18
19// v-tooltip (uproszczony)
20const vTooltip = {
21 mounted(el, binding) {
22 el.style.position = 'relative'
23 el.style.cursor = 'pointer'
24 const tip = document.createElement('span')
25 tip.textContent = binding.value
26 tip.style.cssText =
27 'position:absolute;bottom:110%;left:50%;transform:translateX(-50%);' +
28 'background:#333;color:#00ff88;padding:4px 10px;border-radius:4px;' +
29 'font-size:12px;white-space:nowrap;opacity:0;transition:opacity 0.2s;' +
30 'pointer-events:none;z-index:100;'
31 el.appendChild(tip)
32 el._tipShow = () => { tip.style.opacity = '1' }
33 el._tipHide = () => { tip.style.opacity = '0' }
34 el.addEventListener('mouseenter', el._tipShow)
35 el.addEventListener('mouseleave', el._tipHide)
36 },
37 unmounted(el) {
38 el.removeEventListener('mouseenter', el._tipShow)
39 el.removeEventListener('mouseleave', el._tipHide)
40 }
41}
42
43const menuOpen = ref(false)
44const items = ref([
45 { id: 1, visible: false, label: 'Navigation panel' },
46 { id: 2, visible: false, label: 'Diagnostics panel' },
47 { id: 3, visible: false, label: 'Communications panel' }
48])
49</script>
50
51<template>
52 <div class="nova-lab">
53 <h1>Advanced directives</h1>
54
55 <section class="section">
56 <h2>v-click-outside</h2>
57 <div v-click-outside="() => menuOpen = false" class="menu-wrapper">
58 <button @click.stop="menuOpen = !menuOpen" class="btn">
59 {{ menuOpen ? 'Close menu' : 'Open menu' }}
60 </button>
61 <ul v-if="menuOpen" class="dropdown">
62 <li>Navigation</li>
63 <li>Diagnostics</li>
64 <li>Communication</li>
65 <li>Systems</li>
66 </ul>
67 </div>
68 <p class="hint">Click outside the menu to close it</p>
69 </section>
70
71 <section class="section">
72 <h2>v-tooltip</h2>
73 <div class="tooltip-row">
74 <button v-tooltip="'Open navigation panel'" class="btn">NAV</button>
75 <button v-tooltip="'Systems diagnostics'" class="btn cyan">DIAG</button>
76 <button v-tooltip="'Communication system'" class="btn red">COM</button>
77 </div>
78 <p class="hint">Hover over the buttons</p>
79 </section>
80
81 <section class="section">
82 <h2>Cleanup - the golden rule</h2>
83 <p>Every listener added in mounted() MUST be removed in unmounted().</p>
84 <p>Pattern: save a reference on el (e.g. el._handler), remove it in unmounted.</p>
85 </section>
86 </div>
87</template>
88
89<style scoped>
90.nova-lab {
91 background: #0a0e27;
92 color: #c0c0c0;
93 min-height: 100vh;
94 padding: 20px;
95 font-family: monospace;
96}
97h1 { color: #00ff88; margin-bottom: 20px; }
98h2 { color: #00b4d8; font-size: 16px; margin-bottom: 10px; }
99.section {
100 background: rgba(0, 180, 216, 0.05);
101 border: 1px solid rgba(0, 180, 216, 0.2);
102 padding: 15px;
103 border-radius: 8px;
104 margin-bottom: 15px;
105}
106.btn {
107 background: rgba(0, 255, 136, 0.15);
108 color: #00ff88;
109 border: 1px solid #00ff88;
110 padding: 8px 16px;
111 border-radius: 6px;
112 cursor: pointer;
113 font-family: monospace;
114}
115.btn.cyan { color: #00b4d8; border-color: #00b4d8; background: rgba(0,180,216,0.15); }
116.btn.red { color: #e74c3c; border-color: #e74c3c; background: rgba(231,76,60,0.15); }
117.menu-wrapper { position: relative; display: inline-block; }
118.dropdown {
119 position: absolute; top: 100%; left: 0;
120 background: #1a1e37; border: 1px solid #00ff88;
121 border-radius: 6px; list-style: none;
122 padding: 0; margin: 4px 0; min-width: 160px; z-index: 10;
123}
124.dropdown li {
125 padding: 8px 16px; cursor: pointer;
126}
127.dropdown li:hover { background: rgba(0,255,136,0.1); }
128.tooltip-row { display: flex; gap: 12px; }
129.hint { color: #666; font-size: 12px; margin-top: 8px; }
130</style>Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Where should the event listener be added in the v-click-outside directive?
2. Why should event listeners be removed in the unmounted hook?
Hands-on tasks in the game
- Code editor
Add a listener on document in mounted, check el.contains(event.target), remove it in unmounted.
- Vertical ordering
Arrange the steps of the directive cleanup pattern in the correct order:
- Click in order
Arrange the condition that checks whether a click occurred outside the element:
- Code editor
Create a directive that adds a tooltip to an element on mouse hover.