Kurs Vue.js · Moduł 5: Zdarzenia i formularze

Modyfikatory Zdarzeń

5 min czytania
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. 1. Co robi modyfikator .prevent w @submit.prevent?

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

Przydatne artykuły