Kurs Vue.js · Moduł 5: Zdarzenia i formularze
Modyfikatory Zdarzeń
W tej lekcji4
Pierwszy test Symulatora Sterowania skończył się awarią. Operator wysłał formularz z komendą, a przeglądarka przeładowała stronę i wyzerowała cały stan symulatora. Chwilę później kliknął przycisk wewnątrz panelu, który sam też reaguje na kliknięcia, i wykonały się dwie procedury naraz. W czystym JavaScripcie leczy się to wywołaniami event.preventDefault() i event.stopPropagation() na początku funkcji. Vue ma na to krótszy zapis: modyfikatory, czyli kropkę i nazwę dopisane do nazwy zdarzenia.
Jak zdarzenie wędruje po DOM
Żeby zrozumieć modyfikatory, trzeba wiedzieć, jak podróżuje zdarzenie. Kliknięcie przycisku przechodzi przez trzy fazy. W fazie przechwytywania (capture) zdarzenie schodzi od okna przeglądarki i document w dół drzewa, aż do klikniętego elementu. W fazie celu (target) dociera do samego przycisku. W fazie bąbelkowania (bubble) wraca w górę: od przycisku do rodzica <div>, potem do <body>, <html> i document. Nasłuch podpięty przez @click działa domyślnie w fazie bąbelkowania, dlatego kliknięcie przycisku uruchamia także procedurę rodzica.
Modyfikatory Event
.prevent wywołuje event.preventDefault(), czyli blokuje domyślną akcję przeglądarki, na przykład przeładowanie strony po wysłaniu formularza. .stop wywołuje event.stopPropagation() i zatrzymuje bąbelkowanie do rodziców. .once pozwala procedurze zadziałać tylko przy pierwszym zdarzeniu. Pierwsza część szablonu pokazuje te trzy modyfikatory.
1<template>
2 <!-- .prevent - zapobiega domyślnej akcji -->
3 <form @submit.prevent="onSubmit">
4 <button type="submit">Wyślij</button>
5 </form>
6
7 <!-- .stop - zatrzymuje propagację -->
8 <div @click="outer">
9 <button @click.stop="inner">Tylko inner</button>
10 </div>
11
12 <!-- .once - tylko raz -->
13 <button @click.once="runOnce">Tylko raz</button>Formularz wyśle się bez przeładowania strony i wywoła onSubmit. Kliknięcie przycisku wewnątrz <div> wypisze tylko inner, bo .stop nie wypuści zdarzenia do rodzica. Przycisk z .once zareaguje raz, a kolejne kliknięcia przejdą bez echa. Druga część szablonu pokazuje modyfikatory związane z fazami zdarzenia.
1 <!-- .self - tylko na tym elemencie -->
2 <div @click.self="onlyThis">
3 <button>Klik na button nie wywoła onlyThis</button>
4 </div>
5
6 <!-- .capture - tryb capture -->
7 <div @click.capture="captureFirst">
8 <button @click="child">Child</button>
9 </div>
10
11 <!-- .passive - dla scroll performance -->
12 <div @scroll.passive="onScroll">Scroll content</div>
13
14 <!-- Kombinacje -->
15 <a @click.stop.prevent="handleLink">Link</a>
16</template>.self uruchamia procedurę tylko wtedy, gdy zdarzenie wystąpiło na samym elemencie, a nie na jego dziecku. .capture podpina nasłuch w fazie przechwytywania, więc captureFirst wykona się przed procedurą dziecka. .passive mówi przeglądarce, że procedura nie zablokuje domyślnej akcji, więc przewijanie może ruszyć od razu, bez czekania na nią. Najczęściej dopisuje się go do zdarzeń dotyku i kółka myszy. Nie łącz go z .prevent, bo te dwa sobie przeczą. Modyfikatory można łączyć w łańcuch, jak @click.stop.prevent, ale kolejność ma znaczenie: @click.prevent.self blokuje domyślną akcję także dla kliknięć w dzieci, a @click.self.prevent tylko dla kliknięć w sam element. Na koniec skrypt z funkcjami.
1<script setup>
2function onSubmit() {
3 console.log('Form submitted without page reload')
4}
5
6function outer() { console.log('outer') }
7function inner() { console.log('inner') }
8function runOnce() { console.log('This runs only once') }
9function onlyThis() { console.log('Clicked on div itself') }
10</script>Funkcje są proste, bo cała logika zdarzeń siedzi w szablonie, a pozostałe procedury, takie jak captureFirst, pominęliśmy dla zwięzłości. Polecam tę zasadę: modyfikator zamiast ręcznego preventDefault w funkcji. Procedura zostaje czystą logiką misji, a zachowanie przeglądarki widać od razu w szablonie.
Modyfikatory Klawiszy
Przy zdarzeniach klawiatury, keyup i keydown, modyfikator może wskazać konkretny klawisz. Vue ma aliasy .enter, .tab, .delete (łapie Delete i Backspace), .esc, .space oraz strzałki .up, .down, .left i .right. Działa też każda nazwa z event.key zapisana małymi literami z myślnikami, na przykład .page-down.
1<template>
2 <!-- Konkretne klawisze -->
3 <input @keyup.enter="submit" placeholder="Enter to submit" />
4 <input @keyup.esc="cancel" placeholder="Esc to cancel" />
5
6 <!-- Strzałki -->
7 <div tabindex="0" @keyup.up="moveUp" @keyup.down="moveDown"
8 @keyup.left="moveLeft" @keyup.right="moveRight">Panel sterowania</div>
9
10 <!-- Funkcyjne -->
11 <input @keyup.delete="deleteItem" />
12 <input @keyup.space="togglePlay" />
13 <input @keyup.tab="nextField" />Panel ze strzałkami ma tabindex="0", bo zwykły <div> nie przyjmuje fokusu, a zdarzenia klawiatury trafiają do elementu z fokusem. Uwaga na @keyup.tab: Tab przenosi fokus do następnego pola, więc zdarzenie keyup trafia już tam, a wyjście z pola łapiesz przez @keydown.tab. Klawisze systemowe .ctrl, .alt, .shift i .meta (Cmd na Macu, klawisz Windows na PC) łączysz z innymi w kombinacje.
1 <!-- Kombinacje z modyfikatorami -->
2 <input @keyup.ctrl.enter="submitWithCtrl" />
3 <input @keyup.shift.delete="forceDelete" />
4 <input @keyup.alt.s="saveShortcut" />
5 <input @keyup.meta.s="macSave" /> <!-- Cmd na Mac -->
6
7 <!-- Dokładny modyfikator -->
8 <input @keyup.ctrl.exact="onlyCtrl" /> <!-- Tylko Ctrl, nie Ctrl+Shift -->
9</template>@keyup.ctrl.enter zadziała, gdy puścisz Enter, trzymając Ctrl, bo przy keyup klawisz systemowy musi być wciśnięty w chwili zdarzenia. .exact wymaga dokładnie tego zestawu klawiszy systemowych: @keyup.ctrl.exact nie zareaguje, gdy razem z Ctrl trzymasz Shift.
Modyfikatory Myszy
Modyfikatory .left, .right i .middle ograniczają procedurę do przycisku głównego, pomocniczego i środkowego. Nazwy zakładają mysz dla praworęcznych, ale oznaczają role przycisków, a nie ich fizyczne położenie.
1<template>
2 <!-- Przyciski myszy -->
3 <div @click.left="leftClick">Left click</div>
4 <div @click.right="rightClick">Right click</div>
5 <div @click.middle="middleClick">Middle click</div>
6
7 <!-- Menu kontekstowe -->
8 <div @contextmenu.prevent="showCustomMenu">
9 Prawy przycisk = custom menu
10 </div>
11</template>
12
13<script setup>
14function leftClick() { console.log('Left') }
15function rightClick() { console.log('Right') }
16function middleClick() { console.log('Middle') }
17function showCustomMenu(e) {
18 console.log('Custom menu at:', e.clientX, e.clientY)
19}
20</script>Przeglądarki wysyłają click tylko dla przycisku głównego, dlatego Vue kompiluje @click.right do zdarzenia contextmenu, a @click.middle do mouseup. Stąd praktyczny wniosek: samo .right nie ukryje menu przeglądarki. Własne menu kontekstowe budujesz przez @contextmenu.prevent, jak w ostatnim elemencie przykładu, a e.clientX i e.clientY mówią, gdzie je otworzyć. W następnej lekcji przejdziesz do formularzy i dyrektywy v-model.
Pamiętaj: modyfikator to filtr na linii sygnałowej - przepuszcza do procedury tylko właściwe komendy i gasi te, które przeglądarka wykonałaby na własną rękę.
Kod do tej lekcji: App.vue
1<script setup>
2import { ref } from 'vue'
3
4const commandLog = ref([])
5const secretCode = ref('')
6const mouseAction = ref('')
7
8const addLog = (message) => {
9 commandLog.value.unshift({
10 id: Date.now(),
11 message,
12 time: new Date().toLocaleTimeString()
13 })
14 if (commandLog.value.length > 5) {
15 commandLog.value.pop()
16 }
17}
18
19const handleEnter = () => {
20 addLog('⏎ Enter: Komenda wykonana!')
21 if (secretCode.value === 'MARS') {
22 addLog('Kod dostępu zaakceptowany!')
23 }
24}
25
26const handleEscape = () => {
27 addLog('⎋ Escape: Operacja anulowana')
28 secretCode.value = ''
29}
30
31const handleDelete = () => {
32 addLog('⌫ Delete: Dane usunięte')
33}
34
35const handleLeftClick = () => {
36 mouseAction.value = 'Lewy przycisk: Skanowanie lewego sektora'
37 addLog('Lewy przycisk myszy')
38}
39
40const handleRightClick = (event) => {
41 event.preventDefault()
42 mouseAction.value = 'Prawy przycisk: Skanowanie prawego sektora'
43 addLog('Prawy przycisk myszy')
44}
45
46const handleMiddleClick = () => {
47 mouseAction.value = 'Środkowy przycisk: Skanowanie centralnego sektora'
48 addLog('Środkowy przycisk myszy')
49}
50</script>
51
52<template>
53 <div class="command-center">
54 <div class="center-header">
55 <h2>⌨Centrum Komend NOVA LAB</h2>
56 <p class="subtitle">Modyfikatory Klawiszy i Myszy</p>
57 </div>
58
59 <div class="input-section">
60 <div class="input-group">
61 <label>Kod dostępu (wpisz MARS i naciśnij Enter):</label>
62 <input
63 v-model="secretCode"
64 @keyup.enter="handleEnter"
65 @keyup.esc="handleEscape"
66 @keyup.delete="handleDelete"
67 type="text"
68 placeholder="Wprowadź kod..."
69 class="command-input"
70 />
71 </div>
72
73 <div class="key-hints">
74 <span class="hint">Enter - Wykonaj</span>
75 <span class="hint">Esc - Anuluj</span>
76 <span class="hint">Delete - Usuń</span>
77 </div>
78 </div>
79
80 <div class="mouse-section">
81 <h3>Kontrola Skanera Myszy</h3>
82 <div class="scanner-area">
83 <div
84 class="scan-zone"
85 @click.left="handleLeftClick"
86 @click.right="handleRightClick"
87 @click.middle="handleMiddleClick"
88 >
89 <p>Kliknij różnymi przyciskami myszy</p>
90 <p class="action-display">{{ mouseAction || 'Czekam na komendę...' }}</p>
91 </div>
92 </div>
93 </div>
94
95 <div class="log-section">
96 <h3>Log Komend</h3>
97 <div class="log-container">
98 <div
99 v-for="log in commandLog"
100 :key="log.id"
101 class="log-entry"
102 >
103 <span class="log-time">{{ log.time }}</span>
104 <span class="log-message">{{ log.message }}</span>
105 </div>
106 <div v-if="commandLog.length === 0" class="log-empty">
107 Brak aktywności...
108 </div>
109 </div>
110 </div>
111
112 <div class="modifiers-info">
113 <h3>Dostępne modyfikatory:</h3>
114 <div class="modifier-grid">
115 <div class="modifier-item">
116 <strong>Klawisze:</strong>
117 <code>.enter</code>
118 <code>.esc</code>
119 <code>.delete</code>
120 <code>.tab</code>
121 <code>.space</code>
122 </div>
123 <div class="modifier-item">
124 <strong>Mysz:</strong>
125 <code>.left</code>
126 <code>.right</code>
127 <code>.middle</code>
128 </div>
129 </div>
130 </div>
131 </div>
132</template>
133
134<style scoped>
135.command-center {
136 background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
137 color: #00ff88;
138 padding: 30px;
139 border-radius: 15px;
140 border: 2px solid #00b4d8;
141 font-family: 'Courier New', monospace;
142 max-width: 900px;
143 margin: 0 auto;
144}
145
146.center-header {
147 text-align: center;
148 margin-bottom: 30px;
149 padding-bottom: 20px;
150 border-bottom: 2px solid #00b4d8;
151}
152
153.center-header h2 {
154 color: #00ff88;
155 margin: 0 0 10px 0;
156 font-size: 28px;
157 text-shadow: 0 0 10px #00ff88;
158}
159
160.subtitle {
161 color: #00b4d8;
162 margin: 0;
163 font-size: 14px;
164}
165
166.input-section {
167 margin-bottom: 30px;
168}
169
170.input-group {
171 margin-bottom: 15px;
172}
173
174.input-group label {
175 display: block;
176 color: #00b4d8;
177 margin-bottom: 10px;
178 font-size: 14px;
179}
180
181.command-input {
182 width: 100%;
183 background: rgba(0, 180, 216, 0.1);
184 border: 2px solid #00b4d8;
185 border-radius: 8px;
186 padding: 15px;
187 color: #00ff88;
188 font-size: 16px;
189 font-family: 'Courier New', monospace;
190 box-sizing: border-box;
191}
192
193.command-input:focus {
194 outline: none;
195 border-color: #00ff88;
196 box-shadow: 0 0 15px rgba(0, 255, 136, 0.3);
197}
198
199.key-hints {
200 display: flex;
201 gap: 10px;
202 flex-wrap: wrap;
203}
204
205.hint {
206 background: rgba(0, 255, 136, 0.1);
207 border: 1px solid #00ff88;
208 padding: 5px 12px;
209 border-radius: 20px;
210 font-size: 12px;
211 color: #00ff88;
212}
213
214.mouse-section {
215 margin-bottom: 30px;
216}
217
218.mouse-section h3 {
219 color: #00ff88;
220 margin-bottom: 15px;
221}
222
223.scanner-area {
224 background: rgba(0, 180, 216, 0.05);
225 padding: 20px;
226 border-radius: 8px;
227 border: 2px dashed #00b4d8;
228}
229
230.scan-zone {
231 background: linear-gradient(135deg, rgba(0, 180, 216, 0.2) 0%, rgba(0, 255, 136, 0.1) 100%);
232 padding: 40px;
233 border-radius: 8px;
234 text-align: center;
235 cursor: pointer;
236 transition: all 0.3s ease;
237}
238
239.scan-zone:hover {
240 background: linear-gradient(135deg, rgba(0, 180, 216, 0.3) 0%, rgba(0, 255, 136, 0.2) 100%);
241 transform: scale(1.02);
242}
243
244.scan-zone p {
245 margin: 0 0 10px 0;
246 color: #00b4d8;
247}
248
249.action-display {
250 color: #00ff88 !important;
251 font-weight: bold;
252 font-size: 18px;
253}
254
255.log-section {
256 margin-bottom: 30px;
257}
258
259.log-section h3 {
260 color: #00ff88;
261 margin-bottom: 15px;
262}
263
264.log-container {
265 background: rgba(0, 0, 0, 0.3);
266 border: 1px solid #00b4d8;
267 border-radius: 8px;
268 padding: 15px;
269 max-height: 200px;
270 overflow-y: auto;
271}
272
273.log-entry {
274 padding: 8px;
275 margin-bottom: 5px;
276 background: rgba(0, 180, 216, 0.1);
277 border-radius: 4px;
278 display: flex;
279 gap: 15px;
280}
281
282.log-time {
283 color: #00b4d8;
284 font-size: 12px;
285}
286
287.log-message {
288 color: #00ff88;
289 flex: 1;
290}
291
292.log-empty {
293 color: #00b4d8;
294 text-align: center;
295 opacity: 0.5;
296}
297
298.modifiers-info {
299 background: rgba(0, 255, 136, 0.05);
300 padding: 20px;
301 border-radius: 8px;
302 border: 1px solid #00ff88;
303}
304
305.modifiers-info h3 {
306 color: #00ff88;
307 margin-top: 0;
308}
309
310.modifier-grid {
311 display: grid;
312 grid-template-columns: 1fr 1fr;
313 gap: 20px;
314}
315
316.modifier-item {
317 color: #00b4d8;
318}
319
320.modifier-item strong {
321 display: block;
322 color: #00ff88;
323 margin-bottom: 10px;
324}
325
326.modifier-item code {
327 background: rgba(0, 255, 136, 0.2);
328 padding: 4px 8px;
329 border-radius: 4px;
330 color: #00ff88;
331 font-size: 12px;
332 margin-right: 5px;
333 display: inline-block;
334 margin-bottom: 5px;
335}
336</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. Co robi modyfikator .prevent w @submit.prevent?
2. Kiedy używamy modyfikatora .stop?
To 2 z 7 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Stwórz formularz z @submit.prevent i przycisk z @click.once.
- Układanie w poziomie
Ułóż składnię formularza z modyfikatorem .prevent:
- Układanie w pionie
Ułóż fazy propagacji zdarzeń DOM w kolejności:
- Klikanie w kolejności
Kliknij w odpowiedniej kolejności, aby stworzyć @click.stop.prevent:
- Edytor kodu
Stwórz input reagujący na @keyup.enter i @keyup.esc.
- Układanie w pionie
Ułóż kolejność propagacji zdarzeń od dołu do góry (bubbling):