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

Formularze - v-model dla Danych Misji

5 min czytania
W tej lekcji4

Panel rejestracji załogi ma pole z nazwą i akapit, który tę nazwę powtarza. Żeby to działało, potrzebne są dwa połączenia: zmienna musi trafiać do pola, a to, co operator wpisze, musi wracać do zmiennej. Pisanie tego ręcznie przy każdym z dziesięciu pól to dwadzieścia wiązań i dwadzieścia okazji do pomyłki. Vue zamyka oba kierunki w jednej dyrektywie: v-model, czyli dwukierunkowym wiązaniu danych między polem formularza a zmienną.

Co v-model robi pod spodem

v-model to skrót. Na polu tekstowym rozwija się do dwóch rzeczy, które już znasz: wiązania :value, które wpisuje zmienną do pola, oraz nasłuchu @input, który po każdej zmianie odczytuje $event.target.value i zapisuje wynik do zmiennej.

1<template>
2  <input :value="crewName" @input="crewName = $event.target.value" />
3  <input v-model="crewName" />
4</template>
5
6<script setup>
7import { ref } from 'vue'
8
9const crewName = ref('')
10</script>

Oba pola działają identycznie i są ze sobą zsynchronizowane: pisz w jednym, a tekst pojawi się w drugim, bo oba czytają i zapisują ten sam ref. crewName pozostaje zwykłym ref, więc v-model nie potrzebuje żadnego specjalnego stanu. Zapamiętaj jedną zasadę: v-model ignoruje atrybuty value, checked i selected zapisane w HTML. Źródłem prawdy jest zawsze zmienna, dlatego wartość początkową ustawiasz w JavaScripcie.

Podstawy v-model

Ta sama dyrektywa działa na każdym polu formularza, ale pod spodem dobiera właściwość i zdarzenie do typu pola. Pola tekstowe i <textarea> używają value i zdarzenia input, checkboxy i radio używają checked i change, a <select> używa value i change. Przejdźmy przez formularz misji etapami, zaczynając od tekstu.

1<template>
2  <div class="mission-form" style="background: #0a0e27; color: #00ff88;">
3    <!-- Text input -->
4    <input v-model="crewName" placeholder="Nazwa załogi" style="background: #1a1e37; color: #00ff88;" />
5    <p>Załoga: {{ crewName }}</p>
6
7    <!-- Textarea -->
8    <textarea v-model="missionNotes" placeholder="Notatki misji" style="background: #1a1e37; color: #00ff88;"></textarea>
9    <p>Notatki: {{ missionNotes }}</p>

Pole i obszar tekstowy zachowują się tak samo: akapit pod nimi zmienia się z każdą literą. W <textarea> nie wpisuj {{ missionNotes }} między znacznikami, bo interpolacja w tym miejscu nie zadziała. Do obszaru tekstu zawsze podpinaj v-model. Teraz checkboxy.

1    <!-- Checkbox - boolean -->
2    <label style="color: #00ff88;">
3      <input type="checkbox" v-model="isAutomated" />
4      Automatyczny tryb
5    </label>
6    <p>Automatyczny: {{ isAutomated }}</p>
7
8    <!-- Checkbox - array -->
9    <label style="color: #00ff88;">
10      <input type="checkbox" v-model="activeSystems" value="oxygen" />
11      Generator tlenu
12    </label>
13    <label style="color: #00ff88;">
14      <input type="checkbox" v-model="activeSystems" value="water" />
15      Recykler wody
16    </label>
17    <p>Aktywne systemy: {{ activeSystems }}</p>

Pojedynczy checkbox związany z isAutomated daje wartość logiczną: true po zaznaczeniu i false po odznaczeniu. Gdy kilka checkboxów dzieli jedną tablicę, każdy ma własny atrybut value, a w activeSystems lądują wartości zaznaczonych pól, w kolejności zaznaczania. Czas na przełączniki radio i listę rozwijaną.

1    <!-- Radio -->
2    <label style="color: #00ff88;">
3      <input type="radio" v-model="powerMode" value="eco" />
4      Eco
5    </label>
6    <label style="color: #00ff88;">
7      <input type="radio" v-model="powerMode" value="performance" />
8      Performance
9    </label>
10    <p>Tryb mocy: {{ powerMode }}</p>
11
12    <!-- Select -->
13    <select v-model="selectedModule" style="background: #1a1e37; color: #00ff88;">
14      <option disabled value="">Wybierz moduł</option>
15      <option value="alpha">Oxygen Alpha</option>
16      <option value="beta">Plasma Beta</option>
17      <option value="gamma">Water Gamma</option>
18    </select>
19    <p>Moduł: {{ selectedModule }}</p>
20  </div>
21</template>

Grupa radio zapisuje do powerMode wartość value wybranego przycisku, więc zmienna ma zawsze jedną wartość. <select> robi to samo z wybraną opcją. Pierwsza opcja z disabled i pustą wartością to zalecany wzorzec: gdy zmienna startuje od pustego tekstu, lista pokazuje zachętę do wyboru, a na iOS nie blokuje wyboru pierwszej prawdziwej opcji. Został jeszcze skrypt.

1<script setup>
2import { ref } from 'vue'
3
4const crewName = ref('')
5const missionNotes = ref('')
6const isAutomated = ref(false)
7const activeSystems = ref([])
8const powerMode = ref('')
9const selectedModule = ref('')
10</script>

Każde pole dostaje wartość startową pasującą do typu: pusty tekst dla pól tekstowych, radia i listy, false dla pojedynczego checkboxa i pustą tablicę dla grupy checkboxów. Pomyłka typu, na przykład ref('') dla grupy checkboxów, to najczęstsza przyczyna dziwnego zachowania formularza.

Opcje selecta z tablicy

Katalog modułów stacji rośnie, więc opcji nie wpisuje się ręcznie. Generujesz je dyrektywą v-for z tablicy obiektów, a wartość opcji podajesz przez wiązanie :value. Jak przy każdej liście z v-for, każda opcja dostaje też unikalny :key.

1<template>
2  <select v-model="selectedModuleId">
3    <option disabled value="">Wybierz moduł</option>
4    <option v-for="mod in modules" :key="mod.id" :value="mod.id">
5      {{ mod.name }}
6    </option>
7  </select>
8  <p>Wybrane id: {{ selectedModuleId }}</p>
9</template>
10
11<script setup>
12import { ref } from 'vue'
13
14const modules = ref([
15  { id: 1, name: 'Oxygen Alpha' },
16  { id: 2, name: 'Plasma Beta' }
17])
18const selectedModuleId = ref('')
19</script>

Po wybraniu Plasma Beta akapit pokaże 2. Zwróć uwagę na typ: dzięki :value zmienna dostaje liczbę 2, dokładnie tę z tablicy, a zwykły atrybut value="2" zawsze dałby tekst. Wiązanie :value przyjmuje dowolną wartość, nawet cały obiekt.

Od danych do wysłania

Każdy formularz buduje się w tej samej kolejności: najpierw stan w ref albo reactive, potem wiązanie pól przez v-model, następnie @submit.prevent na formularzu, a na końcu walidacja przed wysłaniem. Trzy pierwsze kroki już znasz, walidację dopiszemy w lekcji o zaawansowanych formularzach. Polecam nadawać zmiennym nazwy mówiące, co przechowują, a nie jakim są polem: activeSystems przetrwa zamianę checkboxów na listę wielokrotnego wyboru, a checkboxValues już nie. W następnej lekcji dostroisz sygnał z pól modyfikatorami .lazy, .number i .trim.

Pamiętaj: v-model to dwukierunkowy kanał łączności - pole na panelu i zmienna w komputerze misji zawsze pokazują tę samą wartość, niezależnie od tego, która strona ją zmieniła.

Kod do tej lekcji: App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4const astronautName = ref('')
5const missionCode = ref('')
6const missionDescription = ref('')
7const statusReport = ref('')
8
9const characterCount = computed(() => missionDescription.value.length)
10const wordCount = computed(() => {
11  return missionDescription.value.trim().split(/\s+/).filter(word => word.length > 0).length
12})
13
14const isFormValid = computed(() => {
15  return astronautName.value.length >= 3 &&
16         missionCode.value.length >= 4 &&
17         missionDescription.value.length >= 10
18})
19</script>
20
21<template>
22  <div class="data-transmission">
23    <div class="transmission-header">
24      <h2>System Transmisji Danych</h2>
25      <p class="subtitle">v-model - Dwukierunkowe Wiązanie Danych</p>
26    </div>
27
28    <div class="form-container">
29      <div class="form-group">
30        <label for="astronaut">Imię Astronauty:</label>
31        <input
32          id="astronaut"
33          v-model="astronautName"
34          type="text"
35          placeholder="Wprowadź imię..."
36          class="data-input"
37        />
38        <div class="input-feedback">
39          Wprowadzone: <strong>{{ astronautName || '(puste)' }}</strong>
40        </div>
41      </div>
42
43      <div class="form-group">
44        <label for="mission-code">Kod Misji:</label>
45        <input
46          id="mission-code"
47          v-model="missionCode"
48          type="text"
49          placeholder="np. MARS-2087-A1"
50          class="data-input"
51        />
52        <div class="input-feedback">
53          Kod: <strong>{{ missionCode || '(puste)' }}</strong>
54        </div>
55      </div>
56
57      <div class="form-group">
58        <label for="description">Opis Misji:</label>
59        <textarea
60          id="description"
61          v-model="missionDescription"
62          placeholder="Opisz szczegóły misji..."
63          class="data-textarea"
64          rows="5"
65        ></textarea>
66        <div class="textarea-stats">
67          <span>Znaki: {{ characterCount }}</span>
68          <span>Słowa: {{ wordCount }}</span>
69        </div>
70      </div>
71
72      <div class="form-group">
73        <label for="status">Raport Statusu (opcjonalne):</label>
74        <textarea
75          id="status"
76          v-model="statusReport"
77          placeholder="Dodatkowe informacje..."
78          class="data-textarea"
79          rows="3"
80        ></textarea>
81      </div>
82
83      <div class="validation-status" :class="{ valid: isFormValid }">
84        <span v-if="isFormValid">Formularz gotowy do transmisji</span>
85        <span v-else>Uzupełnij wszystkie wymagane pola</span>
86      </div>
87    </div>
88
89    <div class="data-preview">
90      <h3>Podgląd Transmitowanych Danych</h3>
91      <div class="preview-box">
92        <pre>{{ {
93  astronaut: astronautName || null,
94  missionCode: missionCode || null,
95  description: missionDescription || null,
96  status: statusReport || null,
97  valid: isFormValid
98} }}</pre>
99      </div>
100    </div>
101
102    <div class="vmodel-info">
103      <h3>Jak działa v-model:</h3>
104      <div class="info-content">
105        <p><code>v-model</code> to skrót dla:</p>
106        <code class="code-example">:value="variable" @input="variable = $event.target.value"</code>
107        <p>Automatycznie synchronizuje dane między inputem a zmienną!</p>
108      </div>
109    </div>
110  </div>
111</template>
112
113<style scoped>
114.data-transmission {
115  background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
116  color: #00ff88;
117  padding: 30px;
118  border-radius: 15px;
119  border: 2px solid #00b4d8;
120  font-family: 'Courier New', monospace;
121  max-width: 900px;
122  margin: 0 auto;
123}
124
125.transmission-header {
126  text-align: center;
127  margin-bottom: 30px;
128  padding-bottom: 20px;
129  border-bottom: 2px solid #00b4d8;
130}
131
132.transmission-header h2 {
133  color: #00ff88;
134  margin: 0 0 10px 0;
135  font-size: 28px;
136  text-shadow: 0 0 10px #00ff88;
137}
138
139.subtitle {
140  color: #00b4d8;
141  margin: 0;
142  font-size: 14px;
143}
144
145.form-container {
146  margin-bottom: 30px;
147}
148
149.form-group {
150  margin-bottom: 25px;
151}
152
153.form-group label {
154  display: block;
155  color: #00b4d8;
156  margin-bottom: 10px;
157  font-size: 14px;
158  font-weight: bold;
159}
160
161.data-input,
162.data-textarea {
163  width: 100%;
164  background: rgba(0, 180, 216, 0.1);
165  border: 2px solid #00b4d8;
166  border-radius: 8px;
167  padding: 12px 15px;
168  color: #00ff88;
169  font-size: 16px;
170  font-family: 'Courier New', monospace;
171  box-sizing: border-box;
172  transition: all 0.3s ease;
173}
174
175.data-input:focus,
176.data-textarea:focus {
177  outline: none;
178  border-color: #00ff88;
179  box-shadow: 0 0 15px rgba(0, 255, 136, 0.3);
180  background: rgba(0, 180, 216, 0.15);
181}
182
183.data-textarea {
184  resize: vertical;
185}
186
187.input-feedback {
188  margin-top: 8px;
189  color: #00b4d8;
190  font-size: 12px;
191  padding: 8px 12px;
192  background: rgba(0, 180, 216, 0.05);
193  border-radius: 4px;
194}
195
196.input-feedback strong {
197  color: #00ff88;
198}
199
200.textarea-stats {
201  margin-top: 8px;
202  display: flex;
203  gap: 20px;
204  color: #00b4d8;
205  font-size: 12px;
206}
207
208.validation-status {
209  padding: 15px;
210  background: rgba(255, 107, 53, 0.1);
211  border: 2px solid #ff6b35;
212  border-radius: 8px;
213  text-align: center;
214  font-weight: bold;
215  color: #ff6b35;
216  transition: all 0.3s ease;
217}
218
219.validation-status.valid {
220  background: rgba(0, 255, 136, 0.1);
221  border-color: #00ff88;
222  color: #00ff88;
223}
224
225.data-preview {
226  margin-bottom: 30px;
227}
228
229.data-preview h3 {
230  color: #00ff88;
231  margin-bottom: 15px;
232}
233
234.preview-box {
235  background: rgba(0, 0, 0, 0.4);
236  border: 1px solid #00b4d8;
237  border-radius: 8px;
238  padding: 20px;
239  overflow-x: auto;
240}
241
242.preview-box pre {
243  margin: 0;
244  color: #00ff88;
245  font-size: 14px;
246  line-height: 1.6;
247}
248
249.vmodel-info {
250  background: rgba(0, 255, 136, 0.05);
251  padding: 20px;
252  border-radius: 8px;
253  border: 1px solid #00ff88;
254}
255
256.vmodel-info h3 {
257  color: #00ff88;
258  margin-top: 0;
259}
260
261.info-content {
262  color: #00b4d8;
263}
264
265.info-content p {
266  margin: 10px 0;
267}
268
269.info-content code {
270  background: rgba(0, 255, 136, 0.2);
271  padding: 2px 6px;
272  border-radius: 4px;
273  color: #00ff88;
274  font-size: 13px;
275}
276
277.code-example {
278  display: block;
279  background: rgba(0, 180, 216, 0.2);
280  padding: 10px 15px;
281  border-radius: 6px;
282  margin: 10px 0;
283  font-size: 12px;
284}
285</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. Czym jest v-model w Vue.js?

  2. 2. v-model jest skrótem dla jakiej kombinacji?

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

Zadania praktyczne w grze

  • Edytor kodu

    Użyj v-model z input text i wyświetl wpisaną wartość w paragrafie.

  • Układanie w poziomie

    Ułóż poprawną składnię input z v-model:

  • Klikanie w kolejności

    Ułóż składnię checkboxa z v-model:

  • Edytor kodu

    Użyj checkbox (array) do wyboru equipment i radio do wyboru typu misji.

  • Układanie w pionie

    Ułóż kroki tworzenia reaktywnego formularza Vue od danych do walidacji:

  • Układanie w poziomie

    Ułóż poprawną składnię opcji selecta z v-for:

  • Edytor kodu

    Zbuduj select z v-for i :value dla listy obiektów.

Przydatne artykuły