Kurs Vue.js · Moduł 14: Dyrektywy i pluginy

Zaawansowane dyrektywy

2 min czytania
W tej lekcji4

W NOVA LAB zaawansowane systemy interfejsów wymagają wyrafinowanych dyrektyw. Poznajmy trzy popularne wzorce: v-click-outside, v-tooltip i v-intersection.

v-click-outside

Wykrywanie kliknięcia poza elementem - idealne do zamykania menu, dropdownów i modali:

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}

Użycie:

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>Opcja 1</li>
26      <li>Opcja 2</li>
27    </ul>
28  </div>
29</template>

Ważne: W hooku unmounted usuwamy listener, aby uniknąć wycieku pamięci. Przechowujemy referencję handlera na elemencie (el._clickOutside).

v-tooltip

Prosty tooltip wyświetlany po najechaniu myszką:

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)

Wykrywanie, kiedy element pojawia się w widoku - idealne do lazy loadingu i animacji wejścia:

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}

Użycie:

1<template>
2  <div v-intersection.once="onVisible">
3    Pokaże się, gdy element będzie widoczny
4  </div>
5</template>

Cleanup - złota zasada dyrektyw

Każdy listener, observer, timer dodany w mounted MUSI być usunięty w unmounted. Inaczej powstają wycieki pamięci.

Wzorzec:

  1. W mounted - dodaj handler i zapisz referencję na el
  2. W unmounted - usuń handler używając zapisanej referencji
Kod do tej lekcji: 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: 'Panel nawigacji' },
46  { id: 2, visible: false, label: 'Panel diagnostyki' },
47  { id: 3, visible: false, label: 'Panel komunikacji' }
48])
49</script>
50
51<template>
52  <div class="nova-lab">
53    <h1>Zaawansowane dyrektywy</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 ? 'Zamknij menu' : 'Otworz menu' }}
60        </button>
61        <ul v-if="menuOpen" class="dropdown">
62          <li>Nawigacja</li>
63          <li>Diagnostyka</li>
64          <li>Komunikacja</li>
65          <li>Systemy</li>
66        </ul>
67      </div>
68      <p class="hint">Kliknij poza menu, aby je zamknac</p>
69    </section>
70
71    <section class="section">
72      <h2>v-tooltip</h2>
73      <div class="tooltip-row">
74        <button v-tooltip="'Otworz panel nawigacji'" class="btn">NAV</button>
75        <button v-tooltip="'Diagnostyka systemow'" class="btn cyan">DIAG</button>
76        <button v-tooltip="'System komunikacji'" class="btn red">COM</button>
77      </div>
78      <p class="hint">Najedz myszka na przyciski</p>
79    </section>
80
81    <section class="section">
82      <h2>Cleanup - zlota zasada</h2>
83      <p>Kazdy listener dodany w mounted() MUSI byc usuniety w unmounted().</p>
84      <p>Wzorzec: zapisz referencje na el (np. el._handler), usun w 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>

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Gdzie należy dodać event listener w dyrektywie v-click-outside?

  2. 2. Dlaczego należy usuwać event listenery w hooku unmounted?

Zadania praktyczne w grze

  • Edytor kodu

    Dodaj listener na document w mounted, sprawdzaj el.contains(event.target), usuń w unmounted.

  • Układanie w pionie

    Ułóż kroki wzorca cleanup dyrektywy w poprawnej kolejności:

  • Klikanie w kolejności

    Ułóż warunek sprawdzający, czy kliknięto poza elementem:

  • Edytor kodu

    Stwórz dyrektywę, która dodaje tooltip do elementu po najechaniu myszką.

Przydatne artykuły