Vue.js course Β· Module 14: Directives & Plugins

Advanced Directives

2 min read
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:

  1. In mounted - add the handler and store the reference on el
  2. 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. 1. Where should the event listener be added in the v-click-outside directive?

  2. 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.

Useful articles