Kurs Vue.js · Moduł 14: Dyrektywy i pluginy
Zaawansowane dyrektywy
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:
- W
mounted- dodaj handler i zapisz referencję nael - 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. Gdzie należy dodać event listener w dyrektywie v-click-outside?
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ą.