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

Obsługa Zdarzeń - Przyciski Kontrolne

5 min czytania
W tej lekcji5

Witaj w Symulatorze Sterowania NOVA LAB! Do tej pory panele stacji tylko wyświetlały dane: liczyły, filtrowały i raportowały. Załoga musi jednak nimi sterować, a przycisk, który nie reaguje na kliknięcie, jest tylko dekoracją. W czystym JavaScripcie każdy przycisk wymagałby odszukania elementu, podpięcia nasłuchu przez addEventListener i pamiętania o jego usunięciu, gdy panel znika. Vue robi to za Ciebie jedną dyrektywą.

Czego się nauczysz

  • obsługi zdarzeń - jak podpiąć procedurę pod przycisk, klawisz albo ruch myszy.
  • modyfikatorów zdarzeń - jak zablokować przeładowanie strony, zatrzymać propagację i reagować na konkretne klawisze.
  • v-model - jak związać pola formularza ze zmiennymi w obie strony.
  • modyfikatorów v-model - jak oczyścić sygnał z pól dzięki .lazy, .number i .trim.
  • zaawansowanych formularzy - wyboru wielu opcji, opcji generowanych z tablicy i walidacji.
  • formularza w praktyce i projektu - pełnej rejestracji modułu i panelu rejestracji misji.

Podstawowa Obsługa Zdarzeń

Dyrektywa v-on nasłuchuje zdarzenia DOM i uruchamia kod, gdy ono nastąpi. Pełny zapis to v-on:click, a skrót używany na co dzień to @click. Wartością może być nazwa funkcji, czyli method handler, albo krótki kod JavaScript, czyli inline handler. Vue rozróżnia je samo: nazwa w rodzaju handleClick albo ścieżka panel.start to metoda, a wszystko inne, na przykład count++ czy initiateSystem('OXYGEN'), to kod inline. Najpierw szablon panelu z pięcioma wariantami przycisku.

1<template>
2  <div class="control-panel" style="background: #0a0e27; color: #00ff88;">
3    <!-- Pełna składnia -->
4    <button v-on:click="handleClick" style="background: #00b4d8; color: #0a0e27;">Aktywuj</button>
5
6    <!-- Skrócona składnia (zalecana) -->
7    <button @click="handleClick" style="background: #00b4d8; color: #0a0e27;">Aktywuj</button>
8
9    <!-- Inline handler -->
10    <button @click="powerLevel++" style="background: #00b4d8; color: #0a0e27;">Moc: {{ powerLevel }}%</button>
11
12    <!-- Z argumentem -->
13    <button @click="initiateSystem('OXYGEN')" style="background: #00b4d8; color: #0a0e27;">Start O2</button>
14
15    <!-- Z event object -->
16    <button @click="logEvent($event)" style="background: #00b4d8; color: #0a0e27;">Log Command</button>
17  </div>
18</template>

Dwa pierwsze przyciski robią dokładnie to samo i różnią się tylko zapisem. Trzeci zwiększa powerLevel bezpośrednio w szablonie. Czwarty wywołuje funkcję z argumentem, a piąty przekazuje jej specjalną zmienną $event, pod którą Vue podstawia natywny obiekt zdarzenia. Teraz skrypt z funkcjami, które te przyciski wywołują.

1<script setup>
2import { ref } from 'vue'
3
4const powerLevel = ref(0)
5
6function handleClick() {
7  console.log('System activated!')
8}
9
10function initiateSystem(systemName) {
11  console.log(`Initializing ${systemName} system...`)
12}
13
14function logEvent(event) {
15  console.log('Event:', event.type, event.target)
16}
17</script>

Zwróć uwagę na handleClick: w szablonie podaliśmy samą nazwę, bez nawiasów, więc Vue wywoła funkcję i przekaże jej obiekt zdarzenia jako pierwszy argument. Tu go nie używamy, ale jest dostępny. Co się nie zmieniło w porównaniu z czystym JavaScriptem: to nadal zwykłe funkcje i zwykłe zdarzenia przeglądarki. Vue dokłada tylko podpięcie nasłuchu i jego automatyczne odpięcie, gdy komponent znika ze strony.

Zdarzenie i własny argument

Czasem procedura potrzebuje zarówno obiektu zdarzenia, jak i własnej informacji, na przykład nazwy systemu. W inline handlerze przekazujesz wtedy $event obok argumentu. Równoważną formą jest funkcja strzałkowa, która dostaje zdarzenie jako parametr.

1<template>
2  <button @click="logAction($event, 'oxygen')">Log O2</button>
3  <button @click="(event) => logAction(event, 'water')">Log H2O</button>
4</template>
5
6<script setup>
7function logAction(event, system) {
8  console.log(system, event.type)
9}
10</script>

Oba przyciski wywołują tę samą funkcję i wypisują nazwę systemu oraz typ zdarzenia, czyli click. Kolejność argumentów ustalasz sam, byle zgadzała się z definicją funkcji. Pamiętaj, że w inline handlerze zdarzenie oznacza tylko $event - zwykłe event czy e są dla szablonu nieznanymi zmiennymi.

Typowe Zdarzenia

Po @ możesz wpisać nazwę dowolnego zdarzenia DOM. Poniższy szablon to katalog najczęstszych: zdarzenia myszy, klawiatury i formularzy, a także przewijania i ładowania obrazka. Funkcje obsługi pominęliśmy, bo liczą się tu same nazwy zdarzeń.

1<template>
2  <!-- Mouse events -->
3  <div @click="onClick" @dblclick="onDoubleClick">Klik</div>
4  <div @mouseenter="onEnter" @mouseleave="onLeave">Hover</div>
5  <div @mousedown="onDown" @mouseup="onUp">Press</div>
6
7  <!-- Keyboard events -->
8  <input @keydown="onKeyDown" @keyup="onKeyUp" @keypress="onKeyPress" />
9
10  <!-- Form events -->
11  <form @submit="onSubmit">
12    <input @input="onInput" @change="onChange" @focus="onFocus" @blur="onBlur" />
13  </form>
14
15  <!-- Other -->
16  <div @scroll="onScroll">Scrollable content</div>
17  <img @load="onLoad" @error="onError" :src="imageUrl" />
18</template>

mouseenter i mouseleave zgłaszają wejście kursora na element i wyjście z niego, a w przeciwieństwie do mouseover nie odpalają się ponownie, gdy kursor przechodzi między elementami w środku. input przychodzi po każdej zmianie treści pola, a change po jej zatwierdzeniu. focus i blur oznaczają wejście do pola i wyjście z niego. Uwaga na keypress: według MDN to zdarzenie jest przestarzałe (deprecated), więc w nowym kodzie używaj keydown. Vue nie wymyśla tu własnych zdarzeń - wszystkie są natywnymi zdarzeniami przeglądarki.

Dostęp do Event Object

Method handler dostaje obiekt zdarzenia automatycznie jako pierwszy parametr. Najczęściej przydają się: target, czyli element, na którym zdarzenie wystąpiło, clientX i clientY z położeniem kursora w oknie, buttons z maską wciśniętych przycisków myszy, a przy klawiaturze key, code oraz flagi ctrlKey i shiftKey.

1<template>
2  <button @click="handleClick">Kliknij</button>
3  <input @keyup="handleKey" />
4</template>
5
6<script setup>
7function handleClick(event) {
8  console.log('Target:', event.target)
9  console.log('Position:', event.clientX, event.clientY)
10  console.log('Buttons:', event.buttons)
11}
12
13function handleKey(event) {
14  console.log('Key:', event.key)
15  console.log('Code:', event.code)
16  console.log('Ctrl:', event.ctrlKey)
17  console.log('Shift:', event.shiftKey)
18}
19</script>

key podaje wartość klawisza, na przykład "Enter" albo "a", a code jego fizyczne miejsce na klawiaturze, na przykład "KeyA", niezależnie od układu. Do skrótów reagujących na znak polecam key, bo na nim opierają się modyfikatory klawiszy z następnej lekcji. code wybieraj przy sterowaniu zależnym od położenia klawiszy, takim jak WASD. Przy buttons pamiętaj, że w zdarzeniu click przycisk bywa już puszczony, a to, który przycisk wywołał zdarzenie, podaje pole button.

Pamiętaj: @ to przewód między przyciskiem na panelu a procedurą w komputerze pokładowym - podajesz nazwę funkcji albo krótką komendę, a Vue sam podłącza i odłącza kabel.

Kod do tej lekcji: App.vue
1<script setup>
2import { ref } from 'vue'
3
4const signalsSent = ref(0)
5const lastSignalType = ref('')
6const emergencyActive = ref(false)
7
8const sendSignal = (type) => {
9  signalsSent.value++
10  lastSignalType.value = type
11}
12
13const activateEmergency = (event) => {
14  event.preventDefault()
15  emergencyActive.value = true
16  setTimeout(() => {
17    emergencyActive.value = false
18  }, 3000)
19}
20
21const sendOnce = () => {
22  console.log('Jednorazowy sygnał wysłany!')
23}
24</script>
25
26<template>
27  <div class="communication-panel">
28    <div class="panel-header">
29      <h2>Panel Komunikacyjny NOVA LAB</h2>
30      <p class="subtitle">Strefa Komunikacji - Obsługa Zdarzeń</p>
31    </div>
32
33    <div class="stats">
34      <div class="stat-item">
35        <span class="label">Wysłane sygnały:</span>
36        <span class="value">{{ signalsSent }}</span>
37      </div>
38      <div class="stat-item">
39        <span class="label">Ostatni typ:</span>
40        <span class="value">{{ lastSignalType || 'Brak' }}</span>
41      </div>
42    </div>
43
44    <div class="signal-buttons">
45      <button @click="sendSignal('Rutynowy')" class="signal-btn routine">
46        Sygnał Rutynowy
47      </button>
48
49      <button @click="sendSignal('Pilny')" class="signal-btn urgent">
50        Sygnał Pilny
51      </button>
52
53      <button @click.once="sendOnce" class="signal-btn once">
54        Jednorazowy (.once)
55      </button>
56    </div>
57
58    <div class="emergency-section">
59      <a href="#" @click.prevent="activateEmergency" class="emergency-link">
60        Aktywuj Alarm (.prevent)
61      </a>
62
63      <div v-if="emergencyActive" class="emergency-alert">
64        ALARM AKTYWNY!
65      </div>
66    </div>
67
68    <div class="event-info">
69      <h3>Modyfikatory zdarzeń:</h3>
70      <ul>
71        <li><code>.prevent</code> - zapobiega domyślnej akcji</li>
72        <li><code>.stop</code> - zatrzymuje propagację wydarzenia</li>
73        <li><code>.once</code> - zdarzenie wykonuje się tylko raz</li>
74      </ul>
75    </div>
76  </div>
77</template>
78
79<style scoped>
80.communication-panel {
81  background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
82  color: #00ff88;
83  padding: 30px;
84  border-radius: 15px;
85  border: 2px solid #00b4d8;
86  font-family: 'Courier New', monospace;
87  max-width: 800px;
88  margin: 0 auto;
89}
90
91.panel-header {
92  text-align: center;
93  margin-bottom: 30px;
94  padding-bottom: 20px;
95  border-bottom: 2px solid #00b4d8;
96}
97
98.panel-header h2 {
99  color: #00ff88;
100  margin: 0 0 10px 0;
101  font-size: 28px;
102  text-shadow: 0 0 10px #00ff88;
103}
104
105.subtitle {
106  color: #00b4d8;
107  margin: 0;
108  font-size: 14px;
109}
110
111.stats {
112  display: grid;
113  grid-template-columns: 1fr 1fr;
114  gap: 15px;
115  margin-bottom: 30px;
116}
117
118.stat-item {
119  background: rgba(0, 180, 216, 0.1);
120  padding: 15px;
121  border-radius: 8px;
122  border: 1px solid #00b4d8;
123  display: flex;
124  justify-content: space-between;
125  align-items: center;
126}
127
128.label {
129  color: #00b4d8;
130  font-size: 14px;
131}
132
133.value {
134  color: #00ff88;
135  font-size: 20px;
136  font-weight: bold;
137}
138
139.signal-buttons {
140  display: grid;
141  gap: 15px;
142  margin-bottom: 30px;
143}
144
145.signal-btn {
146  background: linear-gradient(135deg, #00b4d8 0%, #0077b6 100%);
147  color: white;
148  border: none;
149  padding: 15px 30px;
150  border-radius: 8px;
151  font-size: 16px;
152  font-weight: bold;
153  cursor: pointer;
154  transition: all 0.3s ease;
155  font-family: 'Courier New', monospace;
156}
157
158.signal-btn:hover {
159  transform: translateY(-2px);
160  box-shadow: 0 5px 15px rgba(0, 180, 216, 0.4);
161}
162
163.signal-btn.urgent {
164  background: linear-gradient(135deg, #ff6b35 0%, #d64545 100%);
165}
166
167.signal-btn.once {
168  background: linear-gradient(135deg, #9d4edd 0%, #7209b7 100%);
169}
170
171.emergency-section {
172  text-align: center;
173  margin-bottom: 30px;
174  padding: 20px;
175  background: rgba(255, 107, 53, 0.1);
176  border-radius: 8px;
177  border: 1px solid #ff6b35;
178}
179
180.emergency-link {
181  color: #ff6b35;
182  font-size: 18px;
183  text-decoration: none;
184  font-weight: bold;
185  padding: 10px 20px;
186  display: inline-block;
187  border: 2px solid #ff6b35;
188  border-radius: 8px;
189  transition: all 0.3s ease;
190}
191
192.emergency-link:hover {
193  background: rgba(255, 107, 53, 0.2);
194  transform: scale(1.05);
195}
196
197.emergency-alert {
198  margin-top: 15px;
199  padding: 15px;
200  background: #ff6b35;
201  color: white;
202  border-radius: 8px;
203  font-weight: bold;
204  animation: pulse 1s infinite;
205}
206
207@keyframes pulse {
208  0%, 100% { opacity: 1; }
209  50% { opacity: 0.7; }
210}
211
212.event-info {
213  background: rgba(0, 255, 136, 0.05);
214  padding: 20px;
215  border-radius: 8px;
216  border: 1px solid #00ff88;
217}
218
219.event-info h3 {
220  color: #00ff88;
221  margin-top: 0;
222  font-size: 18px;
223}
224
225.event-info ul {
226  margin: 0;
227  padding-left: 20px;
228}
229
230.event-info li {
231  color: #00b4d8;
232  margin: 8px 0;
233}
234
235.event-info code {
236  background: rgba(0, 255, 136, 0.2);
237  padding: 2px 6px;
238  border-radius: 4px;
239  color: #00ff88;
240  font-weight: bold;
241}
242</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. Jak dodać event listener dla kliknięcia w Vue.js?

  2. 2. Jaki symbol jest skrótem dla dyrektywy v-on?

To 2 z 4 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Dodaj @click handler zwiększający licznik i wyświetl wartość.

  • Klikanie w kolejności

    Ułóż składnię dodania click handlera w Vue:

  • Edytor kodu

    Dodaj obsługę @mouseenter, @mouseleave i @dblclick na elemencie div.

  • Układanie w poziomie

    Ułóż poprawną składnię handlera przekazującego event i argument:

Przydatne artykuły