Kurs Vue.js · Moduł 5: Zdarzenia i formularze
Formularze w Praktyce - Rejestracja Modułu
Dział inżynierii NOVA LAB dostał zlecenie: jeden formularz, w którym zarejestruje się każdy nowy moduł stacji, od śluzy po laboratorium. Moduł ma nazwę, typ, rok i status, parametry techniczne, kategorie, opis i opcje serwisowe. Kilkanaście pól wrzuconych jedno pod drugim to ściana, której nikt nie przeczyta. Zbudujemy więc formularz podzielony na grupy, z polami, które pojawiają się tylko wtedy, gdy mają sens, z resetem i z podglądem danych.
Grupy pól: fieldset i legend
Element <fieldset> grupuje powiązane pola, a <legend> jest podpisem tej grupy. Czytniki ekranu odczytują go przed polami, więc operator korzystający z czytnika wie, w której sekcji jest. Etykieta <label> z atrybutem for wskazuje id pola, dzięki czemu kliknięcie w napis ustawia kursor w polu. Pierwsza grupa zaczyna się od nazwy i typu modułu.
1<template>
2 <form @submit.prevent="submitModule" class="module-form" style="background: #0a0e27; color: #00ff88;">
3 <h2>Rejestracja modułu misji</h2>
4
5 <!-- Podstawowe informacje -->
6 <fieldset style="border-color: #00b4d8;">
7 <legend>Informacje podstawowe</legend>
8
9 <div class="form-group">
10 <label for="name">Nazwa modułu *</label>
11 <input
12 id="name"
13 v-model.trim="module.name"
14 required
15 minlength="3"
16 style="background: #1a1e37; color: #00ff88;"
17 />
18 </div>
19
20 <div class="form-group">
21 <label for="type">Typ *</label>
22 <input id="type" v-model.trim="module.type" required style="background: #1a1e37; color: #00ff88;" />
23 </div>Nazwa ma atrybuty required i minlength="3", czyli wbudowaną walidację HTML. Przeglądarka sprawdza takie atrybuty, zanim w ogóle wyśle zdarzenie submit, i przy błędzie pokazuje własny komunikat - @submit.prevent niczego tu nie wyłącza. v-model.trim usuwa przy tym zbędne spacje. Dalej rząd z rokiem i statusem.
1 <div class="form-row">
2 <div class="form-group">
3 <label for="year">Rok wdrożenia</label>
4 <input
5 id="year"
6 v-model.number="module.year"
7 type="number"
8 min="2020"
9 max="2100"
10 style="background: #1a1e37; color: #00ff88;"
11 />
12 </div>
13
14 <div class="form-group">
15 <label for="status">Status</label>
16 <select id="status" v-model="module.status" style="background: #1a1e37; color: #00ff88;">
17 <option value="">Wybierz...</option>
18 <option value="ACTIVE">Aktywny</option>
19 <option value="STANDBY">Gotowość</option>
20 <option value="MAINTENANCE">Konserwacja</option>
21 <option value="OFFLINE">Offline</option>
22 </select>
23 </div>
24 </div>
25 </fieldset>Rok ma min i max dla przeglądarki oraz .number dla Vue. Computed isValid roku nie sprawdza, więc wpis 1999 zatrzyma dopiero przeglądarka po kliknięciu przycisku wysyłania. Lista statusów zaczyna się od pustej opcji zachęcającej do wyboru. Druga grupa pokazuje, że v-model działa też na zagnieżdżonych ścieżkach, takich jak module.params.maxPower.
1 <!-- Parametry -->
2 <fieldset style="border-color: #00b4d8;">
3 <legend>Parametry</legend>
4 <div class="form-row">
5 <div class="form-group">
6 <label>Max moc (MW)</label>
7 <input v-model.number="module.params.maxPower" type="number" style="background: #1a1e37; color: #00ff88;" />
8 </div>
9 <div class="form-group">
10 <label>Temperatura (K)</label>
11 <input v-model.number="module.params.temperature" type="number" style="background: #1a1e37; color: #00ff88;" />
12 </div>
13 <div class="form-group" v-if="needsPressure">
14 <label>Ciśnienie (kPa)</label>
15 <input v-model.number="module.params.pressure" type="number" style="background: #1a1e37; color: #00ff88;" />
16 </div>
17 </div>
18 </fieldset>Pole ciśnienia ma v-if="needsPressure", więc istnieje tylko wtedy, gdy computed zwraca true. Przy wartości false Vue usuwa je z DOM, a nie tylko ukrywa. Usunięcie pola nie czyści jednak danych: wpisane wcześniej ciśnienie zostaje w module.params.pressure. Kategorie w trzeciej grupie generuje v-for z tablicy.
1 <!-- Kategorie -->
2 <fieldset>
3 <legend>Kategorie</legend>
4 <div class="checkbox-group">
5 <label v-for="cat in availableCategories" :key="cat.value">
6 <input
7 type="checkbox"
8 v-model="module.categories"
9 :value="cat.value"
10 />
11 {{ cat.label }}
12 </label>
13 </div>
14 </fieldset>Każdy checkbox bierze swoją wartość z :value="cat.value", a wszystkie zapisują do jednej tablicy module.categories, dokładnie jak grupa checkboxów z lekcji o v-model. Czwarta grupa to opis w obszarze tekstowym.
1 <!-- Opis -->
2 <fieldset>
3 <legend>Opis</legend>
4 <textarea
5 v-model="module.description"
6 rows="4"
7 placeholder="Opisz moduł misji..."
8 ></textarea>
9 <p class="char-count">
10 {{ module.description.length }} / 500 znaków
11 </p>
12 </fieldset>Atrybut rows="4" ustala widoczną wysokość pola. Licznik pod spodem tylko informuje o długości opisu i niczego nie ogranicza: twardy limit dałby dopiero atrybut maxlength="500". Piąta grupa zawiera opcje i pole warunkowe.
1 <!-- Opcje -->
2 <fieldset>
3 <legend>Opcje</legend>
4 <label class="checkbox-label">
5 <input type="checkbox" v-model="module.isActive" />
6 Moduł aktywny
7 </label>
8 <label class="checkbox-label">
9 <input type="checkbox" v-model="module.needsMaintenance" />
10 Wymaga konserwacji
11 </label>
12
13 <div v-if="module.needsMaintenance" class="form-group">
14 <label>Budżet konserwacji (USD)</label>
15 <input v-model.number="module.maintenanceCost" type="number" min="0" />
16 </div>
17 </fieldset>To wzorzec pola warunkowego: checkbox z v-model steruje zmienną, a v-if na tej zmiennej pokazuje dodatkowe pole z budżetem konserwacji. isActive startuje od true, więc pole jest zaznaczone od początku, bo v-model bierze stan z JavaScriptu. Na końcu szablonu są przyciski i podgląd danych.
1 <!-- Przyciski -->
2 <div class="form-actions">
3 <button type="button" @click="resetForm">Wyczyść</button>
4 <button type="submit" :disabled="!isValid">Zarejestruj moduł</button>
5 </div>
6
7 <!-- Podgląd -->
8 <details>
9 <summary>Podgląd danych</summary>
10 <pre>{{ JSON.stringify(module, null, 2) }}</pre>
11 </details>
12 </form>
13</template>type="button" przy przycisku czyszczenia jest kluczowy, bo przycisk w formularzu domyślnie ma typ submit i bez tego atrybutu czyszczenie wysłałoby formularz. <details> z <summary> tworzy rozwijany podgląd, a JSON.stringify(module, null, 2) formatuje dane z wcięciem o dwie spacje. Przejdźmy do skryptu.
1<script setup>
2import { reactive, computed } from 'vue'
3
4const initialState = {
5 name: '',
6 type: '',
7 year: null,
8 status: '',
9 params: { maxPower: null, temperature: null, pressure: null },
10 categories: [],
11 description: '',
12 isActive: true,
13 needsMaintenance: false,
14 maintenanceCost: null
15}
16
17const module = reactive({
18 ...initialState,
19 params: { ...initialState.params },
20 categories: []
21})initialState opisuje pusty formularz, a reactive tworzy z niego stan. Uwaga na pułapkę: rozwinięcie ...initialState kopiuje tylko pierwszy poziom obiektu. Gdyby params nie dostało osobnej kopii, wpisanie mocy zmieniłoby także initialState.params, a reset przywróciłby wpisane liczby zamiast pustych pól. Dlatego params i categories dostają własne kopie. Dalej lista kategorii i dwa computed.
1const availableCategories = [
2 { value: 'lifesupport', label: 'Podtrzymywanie życia' },
3 { value: 'energy', label: 'Energetyczny' },
4 { value: 'research', label: 'Badawczy' },
5 { value: 'communication', label: 'Komunikacyjny' },
6 { value: 'navigation', label: 'Nawigacyjny' }
7]
8
9const needsPressure = computed(() => {
10 return ['lifesupport', 'research'].some(c => module.categories.includes(c))
11})
12
13const isValid = computed(() => {
14 return module.name.length >= 3 && module.type
15})availableCategories to zwykła tablica, bo nigdy się nie zmienia i nie potrzebuje reaktywności. needsPressure używa metody some, która zwraca true, gdy choć jeden element spełnia warunek. isValid pełni rolę isFormValid z poprzedniej lekcji: wymaga nazwy z co najmniej trzema znakami i niepustego typu. Zostały reset i wysyłka.
1function resetForm() {
2 Object.assign(module, {
3 ...initialState,
4 params: { ...initialState.params },
5 categories: []
6 })
7}
8
9function submitModule() {
10 if (isValid.value) {
11 console.log('Rejestruję:', JSON.parse(JSON.stringify(module)))
12 alert('Moduł został zarejestrowany!')
13 }
14}
15</script>Object.assign wpisuje wartości początkowe do istniejącego obiektu module, więc szablon pozostaje podpięty do tego samego stanu. Podmiana całego obiektu na nowy odcięłaby szablon od danych. params i categories znów dostają świeże kopie. JSON.parse(JSON.stringify(module)) daje w konsoli zwykłą migawkę danych zamiast obiektu reaktywnego. Polecam pójść o krok dalej i zamienić initialState na funkcję zwracającą nowy obiekt przy każdym wywołaniu - pułapka płytkiej kopii znika wtedy całkowicie. W projekcie modułu wykorzystasz ten sam porządek: stan, v-model, walidacja na @blur, computed isFormValid i @submit.prevent.
Pamiętaj: dobry formularz rejestracji działa jak dobrze zaprojektowany moduł stacji - każda grupa ma swój przedział, a reset przywraca fabryczne ustawienia bez zostawiania śladów.
Kod do tej lekcji: App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4const missionForm = ref({
5 commander: '',
6 destination: '',
7 duration: '',
8 crewSize: '',
9 missionType: 'exploration',
10 equipment: [],
11 emergencyProtocol: false,
12 launchDate: '',
13 objectives: ''
14})
15
16const errors = ref({})
17const submitted = ref(false)
18const submittedData = ref(null)
19
20const missionTypes = [
21 { value: 'exploration', label: 'Eksploracja' },
22 { value: 'research', label: 'Badania' },
23 { value: 'construction', label: 'Budowa' },
24 { value: 'rescue', label: 'Ratunkowa' }
25]
26
27const equipmentOptions = [
28 { id: 'rover', name: 'Łazik' },
29 { id: 'drill', name: 'Wiertło' },
30 { id: 'lab', name: 'Laboratorium' },
31 { id: 'satellite', name: 'Satelita' },
32 { id: 'drone', name: 'Dron' }
33]
34
35const validateForm = () => {
36 errors.value = {}
37
38 if (!missionForm.value.commander.trim()) {
39 errors.value.commander = 'Dowódca jest wymagany'
40 }
41
42 if (!missionForm.value.destination.trim()) {
43 errors.value.destination = 'Miejsce docelowe jest wymagane'
44 }
45
46 const duration = Number(missionForm.value.duration)
47 if (!duration || duration < 1 || duration > 365) {
48 errors.value.duration = 'Czas trwania: 1-365 dni'
49 }
50
51 const crew = Number(missionForm.value.crewSize)
52 if (!crew || crew < 1 || crew > 50) {
53 errors.value.crewSize = 'Załoga: 1-50 osób'
54 }
55
56 if (!missionForm.value.launchDate) {
57 errors.value.launchDate = 'Data startu jest wymagana'
58 }
59
60 if (missionForm.value.objectives.trim().length < 20) {
61 errors.value.objectives = 'Cele muszą mieć minimum 20 znaków'
62 }
63
64 return Object.keys(errors.value).length === 0
65}
66
67const handleSubmit = () => {
68 if (validateForm()) {
69 submitted.value = true
70 submittedData.value = {
71 ...missionForm.value,
72 submittedAt: new Date().toLocaleString('pl-PL')
73 }
74
75 // Symulacja wysłania
76 setTimeout(() => {
77 resetForm()
78 }, 5000)
79 }
80}
81
82const resetForm = () => {
83 missionForm.value = {
84 commander: '',
85 destination: '',
86 duration: '',
87 crewSize: '',
88 missionType: 'exploration',
89 equipment: [],
90 emergencyProtocol: false,
91 launchDate: '',
92 objectives: ''
93 }
94 errors.value = {}
95 submitted.value = false
96 submittedData.value = null
97}
98
99const isFormValid = computed(() => {
100 return missionForm.value.commander.trim() &&
101 missionForm.value.destination.trim() &&
102 missionForm.value.duration &&
103 missionForm.value.crewSize &&
104 missionForm.value.launchDate &&
105 missionForm.value.objectives.trim().length >= 20
106})
107</script>
108
109<template>
110 <div class="mission-control">
111 <div class="control-header">
112 <h2>Centrum Kontroli Misji NOVA LAB</h2>
113 <p class="subtitle">Kompletny System Zgłaszania Misji</p>
114 </div>
115
116 <div v-if="submitted" class="success-panel">
117 <div class="success-message">
118 <h3>Misja Zarejestrowana Pomyślnie!</h3>
119 <p>Dane zostały przesłane do Centrum Dowodzenia NOVA LAB</p>
120 </div>
121
122 <div class="submitted-data">
123 <h4>Szczegóły Misji:</h4>
124 <div class="data-grid">
125 <div><strong>Dowódca:</strong> {{ submittedData.commander }}</div>
126 <div><strong>Cel:</strong> {{ submittedData.destination }}</div>
127 <div><strong>Czas trwania:</strong> {{ submittedData.duration }} dni</div>
128 <div><strong>Załoga:</strong> {{ submittedData.crewSize }} osób</div>
129 <div><strong>Typ:</strong> {{ missionTypes.find(t => t.value === submittedData.missionType)?.label }}</div>
130 <div><strong>Sprzęt:</strong> {{ submittedData.equipment.length }} elementów</div>
131 <div><strong>Data startu:</strong> {{ submittedData.launchDate }}</div>
132 <div><strong>Wysłano:</strong> {{ submittedData.submittedAt }}</div>
133 </div>
134 </div>
135
136 <button @click="resetForm" class="reset-btn">
137 Nowa Misja
138 </button>
139 </div>
140
141 <form v-else @submit.prevent="handleSubmit" class="mission-form">
142 <div class="form-section">
143 <h3>Podstawowe Informacje</h3>
144
145 <div class="form-field">
146 <label for="commander">Dowódca Misji:</label>
147 <input
148 id="commander"
149 v-model.trim="missionForm.commander"
150 type="text"
151 placeholder="Imię i nazwisko"
152 class="form-input"
153 :class="{ error: errors.commander }"
154 />
155 <span v-if="errors.commander" class="error-text">{{ errors.commander }}</span>
156 </div>
157
158 <div class="form-field">
159 <label for="destination">Miejsce Docelowe:</label>
160 <input
161 id="destination"
162 v-model.trim="missionForm.destination"
163 type="text"
164 placeholder="np. Krater Gale, Mars"
165 class="form-input"
166 :class="{ error: errors.destination }"
167 />
168 <span v-if="errors.destination" class="error-text">{{ errors.destination }}</span>
169 </div>
170
171 <div class="form-row">
172 <div class="form-field">
173 <label for="duration">Czas trwania (dni):</label>
174 <input
175 id="duration"
176 v-model.number="missionForm.duration"
177 type="number"
178 placeholder="1-365"
179 class="form-input"
180 :class="{ error: errors.duration }"
181 />
182 <span v-if="errors.duration" class="error-text">{{ errors.duration }}</span>
183 </div>
184
185 <div class="form-field">
186 <label for="crew">Wielkość załogi:</label>
187 <input
188 id="crew"
189 v-model.number="missionForm.crewSize"
190 type="number"
191 placeholder="1-50"
192 class="form-input"
193 :class="{ error: errors.crewSize }"
194 />
195 <span v-if="errors.crewSize" class="error-text">{{ errors.crewSize }}</span>
196 </div>
197 </div>
198
199 <div class="form-field">
200 <label for="launch">Data startu:</label>
201 <input
202 id="launch"
203 v-model="missionForm.launchDate"
204 type="date"
205 class="form-input"
206 :class="{ error: errors.launchDate }"
207 />
208 <span v-if="errors.launchDate" class="error-text">{{ errors.launchDate }}</span>
209 </div>
210 </div>
211
212 <div class="form-section">
213 <h3>Konfiguracja Misji</h3>
214
215 <div class="form-field">
216 <label>Typ misji:</label>
217 <div class="radio-group">
218 <label
219 v-for="type in missionTypes"
220 :key="type.value"
221 class="radio-label"
222 >
223 <input
224 v-model="missionForm.missionType"
225 type="radio"
226 :value="type.value"
227 />
228 {{ type.label }}
229 </label>
230 </div>
231 </div>
232
233 <div class="form-field">
234 <label>Wyposażenie:</label>
235 <div class="checkbox-group">
236 <label
237 v-for="item in equipmentOptions"
238 :key="item.id"
239 class="checkbox-label"
240 >
241 <input
242 v-model="missionForm.equipment"
243 type="checkbox"
244 :value="item.id"
245 />
246 {{ item.name }}
247 </label>
248 </div>
249 </div>
250
251 <div class="form-field">
252 <label class="checkbox-label emergency">
253 <input
254 v-model="missionForm.emergencyProtocol"
255 type="checkbox"
256 />
257 Aktywuj Protokół Ratunkowy
258 </label>
259 </div>
260 </div>
261
262 <div class="form-section">
263 <h3>Cele Misji</h3>
264
265 <div class="form-field">
266 <label for="objectives">Szczegółowy opis celów:</label>
267 <textarea
268 id="objectives"
269 v-model.trim="missionForm.objectives"
270 placeholder="Opisz główne cele i zadania misji (minimum 20 znaków)..."
271 class="form-textarea"
272 :class="{ error: errors.objectives }"
273 rows="6"
274 ></textarea>
275 <div class="char-counter">
276 Znaki: {{ missionForm.objectives.length }}
277 </div>
278 <span v-if="errors.objectives" class="error-text">{{ errors.objectives }}</span>
279 </div>
280 </div>
281
282 <div class="form-actions">
283 <button
284 type="submit"
285 class="submit-btn"
286 :class="{ disabled: !isFormValid }"
287 :disabled="!isFormValid"
288 >
289 Zarejestruj Misję
290 </button>
291
292 <button
293 type="button"
294 @click="resetForm"
295 class="cancel-btn"
296 >
297 Anuluj
298 </button>
299 </div>
300
301 <div class="form-status" :class="{ valid: isFormValid }">
302 {{ isFormValid ? 'Formularz gotowy do wysłania' : 'Uzupełnij wszystkie wymagane pola' }}
303 </div>
304 </form>
305 </div>
306</template>
307
308<style scoped>
309.mission-control {
310 background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
311 color: #00ff88;
312 padding: 30px;
313 border-radius: 15px;
314 border: 2px solid #00b4d8;
315 font-family: 'Courier New', monospace;
316 max-width: 1000px;
317 margin: 0 auto;
318}
319
320.control-header {
321 text-align: center;
322 margin-bottom: 30px;
323 padding-bottom: 20px;
324 border-bottom: 2px solid #00b4d8;
325}
326
327.control-header h2 {
328 color: #00ff88;
329 margin: 0 0 10px 0;
330 font-size: 28px;
331 text-shadow: 0 0 10px #00ff88;
332}
333
334.subtitle {
335 color: #00b4d8;
336 margin: 0;
337 font-size: 14px;
338}
339
340.success-panel {
341 text-align: center;
342}
343
344.success-message {
345 background: rgba(0, 255, 136, 0.1);
346 border: 2px solid #00ff88;
347 border-radius: 12px;
348 padding: 30px;
349 margin-bottom: 30px;
350}
351
352.success-message h3 {
353 color: #00ff88;
354 margin: 0 0 10px 0;
355 font-size: 24px;
356}
357
358.success-message p {
359 color: #00b4d8;
360 margin: 0;
361}
362
363.submitted-data {
364 background: rgba(0, 180, 216, 0.05);
365 border: 1px solid #00b4d8;
366 border-radius: 8px;
367 padding: 20px;
368 margin-bottom: 20px;
369 text-align: left;
370}
371
372.submitted-data h4 {
373 color: #00ff88;
374 margin-top: 0;
375}
376
377.data-grid {
378 display: grid;
379 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
380 gap: 15px;
381 color: #00b4d8;
382}
383
384.data-grid strong {
385 color: #00ff88;
386}
387
388.reset-btn {
389 background: linear-gradient(135deg, #00b4d8 0%, #0077b6 100%);
390 color: white;
391 border: none;
392 padding: 15px 40px;
393 border-radius: 8px;
394 font-size: 16px;
395 font-weight: bold;
396 cursor: pointer;
397 font-family: 'Courier New', monospace;
398}
399
400.mission-form {
401 display: flex;
402 flex-direction: column;
403 gap: 30px;
404}
405
406.form-section {
407 background: rgba(0, 180, 216, 0.05);
408 padding: 25px;
409 border-radius: 12px;
410 border: 1px solid #00b4d8;
411}
412
413.form-section h3 {
414 color: #00ff88;
415 margin: 0 0 20px 0;
416 font-size: 20px;
417}
418
419.form-field {
420 margin-bottom: 20px;
421}
422
423.form-field:last-child {
424 margin-bottom: 0;
425}
426
427.form-field > label {
428 display: block;
429 color: #00b4d8;
430 margin-bottom: 8px;
431 font-size: 14px;
432 font-weight: bold;
433}
434
435.form-input,
436.form-textarea {
437 width: 100%;
438 background: rgba(0, 180, 216, 0.1);
439 border: 2px solid #00b4d8;
440 border-radius: 8px;
441 padding: 12px 15px;
442 color: #00ff88;
443 font-size: 16px;
444 font-family: 'Courier New', monospace;
445 box-sizing: border-box;
446}
447
448.form-input:focus,
449.form-textarea:focus {
450 outline: none;
451 border-color: #00ff88;
452 box-shadow: 0 0 15px rgba(0, 255, 136, 0.3);
453}
454
455.form-input.error,
456.form-textarea.error {
457 border-color: #ff6b35;
458}
459
460.form-textarea {
461 resize: vertical;
462}
463
464.form-row {
465 display: grid;
466 grid-template-columns: 1fr 1fr;
467 gap: 15px;
468}
469
470.error-text {
471 display: block;
472 margin-top: 5px;
473 color: #ff6b35;
474 font-size: 12px;
475}
476
477.char-counter {
478 margin-top: 5px;
479 color: #00b4d8;
480 font-size: 12px;
481}
482
483.radio-group,
484.checkbox-group {
485 display: flex;
486 flex-direction: column;
487 gap: 10px;
488}
489
490.radio-label,
491.checkbox-label {
492 display: flex;
493 align-items: center;
494 gap: 10px;
495 padding: 10px 15px;
496 background: rgba(0, 180, 216, 0.05);
497 border: 1px solid #00b4d8;
498 border-radius: 6px;
499 cursor: pointer;
500 color: #00ff88;
501}
502
503.radio-label:hover,
504.checkbox-label:hover {
505 background: rgba(0, 180, 216, 0.15);
506}
507
508.checkbox-label.emergency {
509 background: rgba(255, 107, 53, 0.1);
510 border-color: #ff6b35;
511 color: #ff6b35;
512 font-weight: bold;
513}
514
515.form-actions {
516 display: flex;
517 gap: 15px;
518 justify-content: center;
519}
520
521.submit-btn,
522.cancel-btn {
523 padding: 15px 40px;
524 border: none;
525 border-radius: 8px;
526 font-size: 16px;
527 font-weight: bold;
528 cursor: pointer;
529 font-family: 'Courier New', monospace;
530 transition: all 0.3s ease;
531}
532
533.submit-btn {
534 background: linear-gradient(135deg, #00ff88 0%, #00b368 100%);
535 color: #0a0e27;
536}
537
538.submit-btn:hover:not(.disabled) {
539 transform: translateY(-2px);
540 box-shadow: 0 5px 20px rgba(0, 255, 136, 0.4);
541}
542
543.submit-btn.disabled {
544 opacity: 0.5;
545 cursor: not-allowed;
546}
547
548.cancel-btn {
549 background: linear-gradient(135deg, #ff6b35 0%, #d64545 100%);
550 color: white;
551}
552
553.cancel-btn:hover {
554 transform: translateY(-2px);
555 box-shadow: 0 5px 20px rgba(255, 107, 53, 0.4);
556}
557
558.form-status {
559 text-align: center;
560 padding: 15px;
561 background: rgba(255, 107, 53, 0.1);
562 border: 2px solid #ff6b35;
563 border-radius: 8px;
564 color: #ff6b35;
565 font-weight: bold;
566}
567
568.form-status.valid {
569 background: rgba(0, 255, 136, 0.1);
570 border-color: #00ff88;
571 color: #00ff88;
572}
573</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. Jak najlepiej zresetować formularz w Vue?
2. Jak pokazać dodatkowe pole tylko gdy checkbox jest zaznaczony?
To 2 z 4 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Układanie w poziomie
Ułóż składnię warunkowego wyświetlenia pola z v-if:
- Edytor kodu
Zbuduj formularz z fieldset, walidacją, reset i submit.
- Układanie w poziomie
Ułóż poprawną składnię fieldset z legend:
- Klikanie w kolejności
Ułóż składnię formularza z handlerem submit:
- Edytor kodu
Zbuduj formularz z walidacją @blur, przyciskiem reset i disabled submit.
- Układanie w pionie
Ułóż kroki tworzenia reaktywnego formularza w Vue:
- Klikanie w kolejności
Ułóż definicję computed sprawdzającego formularz:
- Układanie w poziomie
Ułóż poprawną składnię textarea z v-model od góry do dołu: