Kurs Vue.js · Moduł 2: Składnia szablonów
Dyrektywy Warunkowe
W tej lekcji4
Jak systemy NOVA LAB podejmują decyzje oparte na odczytach sensorów, Vue pozwala kontrolować, co pojawi się w interfejsie.
Klasy z poprzedniej lekcji zmieniają wygląd elementu, ale element dalej istnieje. Hologram stacji potrzebuje czasem czegoś mocniejszego: pokazać panel tylko tego systemu, który wybrał operator, a komunikat o awarii wyświetlić wyłącznie podczas awarii. Do tego służą dyrektywy warunkowe - v-if, który decyduje, czy element w ogóle powstanie, oraz v-show, który tylko go zasłania.
v-if, v-else-if, v-else
v-if renderuje element tylko wtedy, gdy wyrażenie jest prawdziwe. v-else-if dokłada kolejne warunki, a v-else bez żadnej wartości łapie resztę. Oba muszą stać bezpośrednio po elemencie z v-if lub v-else-if. Znacznik <template> z v-if pozwala objąć warunkiem całą grupę elementów:
1<template>
2 <div class="system-monitor" style="background: #0a0e27; color: #00ff88;">
3 <div v-if="systemType === 'oxygen'">
4 Generator tlenu - System aktywny
5 </div>
6 <div v-else-if="systemType === 'water'">
7 Recykling wody - System aktywny
8 </div>
9 <div v-else-if="systemType === 'power'">
10 Reaktor plazmowy - System aktywny
11 </div>
12 <div v-else>
13 Nieznany system
14 </div>
15
16 <!-- Na grupie elementów -->
17 <template v-if="showDetails">
18 <h2 style="color: #00b4d8;">{{ module.name }}</h2>
19 <p>{{ module.description }}</p>
20 <span>Status: {{ module.status }}</span>
21 </template>
22 </div>
23</template>Przy systemType równym 'oxygen' w DOM istnieje tylko pierwszy div. Pozostałe nie są ukryte - po prostu ich nie ma. Sam <template> też się nie renderuje: nagłówek, akapit i span trafiają na stronę bez dodatkowego opakowania. Gdyby między v-if a v-else stanął inny element, kompilator zgłosi błąd, że v-else nie ma sąsiedniego v-if.
Warunki czytają zwykłe dane ze skryptu:
1<script setup>
2import { ref, reactive } from 'vue'
3
4const systemType = ref('oxygen')
5const showDetails = ref(true)
6
7const module = reactive({
8 name: 'Life Support Alpha',
9 description: 'Primary oxygen generation system',
10 status: 'ONLINE'
11})
12</script>Ustaw systemType.value = 'water', a Vue usunie element tlenowy i utworzy element wody. Przy każdym renderze Vue buduje nowe drzewo Virtual DOM, czyli lekki opis strony w JavaScripcie. Gdy warunek jest fałszywy, elementu w tym drzewie brakuje. Vue porównuje nowe drzewo z poprzednim i dopiero wtedy dodaje lub usuwa prawdziwy element w przeglądarce. v-if jest przy tym leniwy: jeśli na starcie warunek jest fałszywy, nic się nie renderuje, a przy przełączaniu zawartość bloku jest za każdym razem niszczona i tworzona od nowa.
v-show
v-show działa inaczej: element renderuje się zawsze, a dyrektywa przełącza tylko właściwość CSS display.
1<template>
2 <div class="alert-panel">
3 <!-- v-show - zawsze renderowany, tylko CSS display: none -->
4 <div v-show="isVisible" class="alert-message"
5 style="background: #ff4444; color: white; padding: 1rem;">
6 CRITICAL ALERT: Oxygen levels dropping!
7 </div>
8 </div>
9</template>
10
11<script setup>
12import { ref } from 'vue'
13
14const isVisible = ref(true)
15</script>Przy isVisible równym false element dostanie style="display: none;", ale zostanie w DOM razem ze swoim stanem. v-show nie działa na znaczniku <template> i nie łączy się z v-else. Najczęściej steruje nim przycisk, który odwraca wartość logiczną:
1<template>
2 <button @click="isVisible = !isVisible">Przełącz panel</button>
3 <div v-show="isVisible">Panel diagnostyczny</div>
4</template>Każde kliknięcie zamienia true na false i odwrotnie. To ta sama instrukcja w szablonie, którą poznałeś przy v-on.
v-if vs v-show
Obie dyrektywy dają na ekranie ten sam efekt, ale kosztują co innego:
| Cecha | v-if | v-show |
|---|---|---|
| Renderowanie | Warunkowe | Zawsze |
| Koszt początkowy | Niski | Wyższy |
| Koszt przełączania | Wyższy | Niski |
| Użycie | Rzadkie zmiany | Częste przełączanie (np. alerty) |
Dokumentacja Vue radzi tak samo jak ja: v-show dla rzeczy przełączanych bardzo często, jak alerty czy zakładki, a v-if, gdy warunek rzadko się zmienia lub zawartość jest ciężka i szkoda ją budować na zapas.
Praktyczny Przykład
Panel misji łączy dwa łańcuchy warunków: filtr systemów i stan wczytywania danych. Pierwsza część szablonu zawiera przyciski filtra:
1<template>
2 <div class="mission-dashboard" style="background: #0a0e27; padding: 1rem;">
3 <div class="system-filter">
4 <button @click="filter = 'all'" class="btn-filter">Wszystkie</button>
5 <button @click="filter = 'life-support'" class="btn-filter">Life Support</button>
6 <button @click="filter = 'power'" class="btn-filter">Energetyka</button>
7 </div>
8
9 <div v-if="filter === 'all'" class="systems-grid">
10 Wszystkie systemy...
11 </div>
12 <div v-else-if="filter === 'life-support'" class="systems-grid">
13 Tylko systemy podtrzymywania życia...
14 </div>
15 <div v-else class="systems-grid">
16 Tylko systemy energetyczne...
17 </div>Każdy przycisk przypisuje nową wartość do filter, a łańcuch pokazuje dokładnie jedną siatkę. Druga część tego samego szablonu obsługuje wczytywanie danych z sensorów:
1 <!-- Stan ładowania -->
2 <div v-if="isLoading" class="loader" style="color: #00b4d8;">
3 Ładowanie danych z sensorów...
4 </div>
5 <div v-else-if="error" class="error" style="color: #ff4444;">
6 {{ error }}
7 </div>
8 <div v-else class="content" style="color: #00ff88;">
9 {{ data }}
10 </div>
11 </div>
12</template>Kolejność ma znaczenie: najpierw ładowanie, potem błąd, na końcu dane. Całość napędzają cztery refy:
1<script setup>
2import { ref } from 'vue'
3
4const filter = ref('all')
5const isLoading = ref(false)
6const error = ref(null)
7const data = ref('All systems nominal')
8</script>Ustaw isLoading.value = true, a zamiast danych pojawi się komunikat ładowania. W następnej lekcji zobaczysz v-for i dowiesz się, dlaczego nie łączy się go z v-if na jednym elemencie.
Zapamiętaj: v-if otwiera i zamyka właz modułu, więc element znika z DOM, a v-show tylko zaciąga zasłonę, pod którą element dalej czeka.
Kod do tej lekcji: App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4const oxygenLevel = ref(95)
5const temperature = ref(-55)
6const solarStorm = ref(false)
7const crewStatus = ref('all-safe')
8const showWarnings = ref(true)
9
10const alertType = computed(() => {
11 if (oxygenLevel.value < 50) return 'critical'
12 if (oxygenLevel.value < 80) return 'warning'
13 return 'safe'
14})
15
16function simulateStorm() {
17 solarStorm.value = true
18 oxygenLevel.value = 45
19 temperature.value = -80
20 crewStatus.value = 'emergency'
21}
22
23function normalizeConditions() {
24 solarStorm.value = false
25 oxygenLevel.value = 95
26 temperature.value = -55
27 crewStatus.value = 'all-safe'
28}
29
30function adjustOxygen(value) {
31 oxygenLevel.value = Math.max(0, Math.min(100, oxygenLevel.value + value))
32}
33</script>
34
35<template>
36 <div class="conditions-monitor">
37 <h1>Monitor Warunków - Baza NOVA na Marsie</h1>
38
39 <!-- v-if, v-else-if, v-else -->
40 <div class="alert-section">
41 <div v-if="alertType === 'critical'" class="alert alert-critical">
42 ALARM KRYTYCZNY! Poziom tlenu poniżej bezpiecznego poziomu!
43 </div>
44 <div v-else-if="alertType === 'warning'" class="alert alert-warning">
45 OSTRZEŻENIE: Poziom tlenu spadł poniżej normy.
46 </div>
47 <div v-else class="alert alert-safe">
48 Wszystkie systemy działają prawidłowo.
49 </div>
50 </div>
51
52 <!-- v-show vs v-if -->
53 <div class="info-panel">
54 <h2>Różnica między v-if a v-show</h2>
55 <button @click="showWarnings = !showWarnings" class="btn btn-toggle">
56 Przełącz ostrzeżenia (v-show)
57 </button>
58
59 <!-- v-show: element pozostaje w DOM, tylko ukryty przez CSS -->
60 <div v-show="showWarnings" class="warning-box">
61 <p>To ostrzeżenie jest kontrolowane przez v-show</p>
62 <p>Element pozostaje w DOM, ale jest ukryty przez display: none</p>
63 </div>
64
65 <!-- v-if: element jest usuwany/dodawany z DOM -->
66 <div v-if="showWarnings" class="warning-box">
67 <p>To ostrzeżenie jest kontrolowane przez v-if</p>
68 <p>Element jest całkowicie usuwany/dodawany z DOM</p>
69 </div>
70 </div>
71
72 <div class="status-grid">
73 <!-- Renderowanie warunkowe statusu burzy -->
74 <div class="status-card" :class="{ 'storm-active': solarStorm }">
75 <h3>Burza Słoneczna</h3>
76 <div v-if="solarStorm" class="storm-warning">
77 <p class="storm-icon">!!!</p>
78 <p>AKTYWNA BURZA SŁONECZNA</p>
79 <p>Wszystkie zewnętrzne operacje wstrzymane</p>
80 </div>
81 <div v-else class="storm-calm">
82 <p class="calm-icon">OK</p>
83 <p>Warunki stabilne</p>
84 </div>
85 </div>
86
87 <!-- Status załogi z kilkoma warunkami -->
88 <div class="status-card">
89 <h3>Status Załogi</h3>
90 <div v-if="crewStatus === 'emergency'" class="crew-emergency">
91 SYTUACJA AWARYJNA - Wszyscy w schronie
92 </div>
93 <div v-else-if="crewStatus === 'alert'" class="crew-alert">
94 Stan Podwyższonej Gotowości
95 </div>
96 <div v-else-if="crewStatus === 'all-safe'" class="crew-safe">
97 Cała załoga bezpieczna - Rutynowe operacje
98 </div>
99 <div v-else class="crew-unknown">
100 Status nieznany
101 </div>
102 </div>
103
104 <!-- Kontrola poziomu tlenu -->
105 <div class="status-card">
106 <h3>Poziom Tlenu</h3>
107 <div class="oxygen-display" :class="'oxygen-' + alertType">
108 {{ oxygenLevel }}%
109 </div>
110 <div class="oxygen-controls">
111 <button @click="adjustOxygen(-10)" class="btn btn-small">-10%</button>
112 <button @click="adjustOxygen(10)" class="btn btn-small">+10%</button>
113 </div>
114 </div>
115
116 <!-- Temperatura -->
117 <div class="status-card">
118 <h3>Temperatura</h3>
119 <div class="temp-display">{{ temperature }}°C</div>
120 <div v-if="temperature < -70" class="temp-warning">
121 Ekstremalnie niskie temperatury
122 </div>
123 </div>
124 </div>
125
126 <div class="controls-section">
127 <h2>Symulacja Warunków</h2>
128 <div class="button-group">
129 <button @click="simulateStorm" class="btn btn-danger">
130 Symuluj Burzę Słoneczną
131 </button>
132 <button @click="normalizeConditions" class="btn btn-success">
133 Przywróć Normalne Warunki
134 </button>
135 </div>
136 </div>
137
138 <div class="info-box">
139 <h3>Renderowanie warunkowe w Vue</h3>
140 <ul>
141 <li><strong>v-if</strong> - Renderuje element tylko gdy warunek jest true (usuwa z DOM)</li>
142 <li><strong>v-else-if</strong> - Dodatkowe warunki w łańcuchu</li>
143 <li><strong>v-else</strong> - Domyślny przypadek</li>
144 <li><strong>v-show</strong> - Zawsze w DOM, ukrywa przez CSS (lepsze dla częstych zmian)</li>
145 </ul>
146 </div>
147 </div>
148</template>
149
150<style scoped>
151.conditions-monitor {
152 background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
153 min-height: 100vh;
154 padding: 2rem;
155 color: #ffffff;
156 font-family: 'Courier New', monospace;
157}
158
159h1 {
160 text-align: center;
161 color: #00ff88;
162 text-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
163 margin-bottom: 2rem;
164}
165
166.alert-section {
167 margin-bottom: 2rem;
168}
169
170.alert {
171 padding: 1.5rem;
172 border-radius: 10px;
173 font-size: 1.2rem;
174 font-weight: bold;
175 text-align: center;
176 animation: slideIn 0.5s ease;
177}
178
179@keyframes slideIn {
180 from { transform: translateY(-20px); opacity: 0; }
181 to { transform: translateY(0); opacity: 1; }
182}
183
184.alert-critical {
185 background: rgba(255, 0, 85, 0.2);
186 border: 3px solid #ff0055;
187 animation: pulse 1s infinite;
188}
189
190.alert-warning {
191 background: rgba(255, 180, 0, 0.2);
192 border: 3px solid #ffb400;
193}
194
195.alert-safe {
196 background: rgba(0, 255, 136, 0.2);
197 border: 3px solid #00ff88;
198}
199
200.info-panel {
201 background: rgba(0, 180, 216, 0.1);
202 border: 2px solid #00b4d8;
203 border-radius: 10px;
204 padding: 1.5rem;
205 margin-bottom: 2rem;
206}
207
208.warning-box {
209 background: rgba(255, 180, 0, 0.1);
210 border: 2px solid #ffb400;
211 border-radius: 5px;
212 padding: 1rem;
213 margin-top: 1rem;
214}
215
216.status-grid {
217 display: grid;
218 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
219 gap: 1.5rem;
220 margin-bottom: 2rem;
221}
222
223.status-card {
224 background: rgba(0, 255, 136, 0.1);
225 border: 2px solid #00ff88;
226 border-radius: 10px;
227 padding: 1.5rem;
228 transition: all 0.3s ease;
229}
230
231.status-card.storm-active {
232 border-color: #ff0055;
233 background: rgba(255, 0, 85, 0.2);
234 animation: shake 0.5s infinite;
235}
236
237@keyframes shake {
238 0%, 100% { transform: translateX(0); }
239 25% { transform: translateX(-5px); }
240 75% { transform: translateX(5px); }
241}
242
243.status-card h3 {
244 color: #00b4d8;
245 margin-top: 0;
246}
247
248.storm-warning, .storm-calm {
249 text-align: center;
250}
251
252.storm-icon {
253 font-size: 3rem;
254 margin: 0;
255 animation: flicker 0.3s infinite;
256}
257
258@keyframes flicker {
259 0%, 100% { opacity: 1; }
260 50% { opacity: 0.5; }
261}
262
263.calm-icon {
264 font-size: 3rem;
265 margin: 0;
266}
267
268.crew-emergency {
269 color: #ff0055;
270 font-weight: bold;
271 padding: 1rem;
272 background: rgba(255, 0, 85, 0.2);
273 border-radius: 5px;
274}
275
276.crew-alert {
277 color: #ffb400;
278 font-weight: bold;
279}
280
281.crew-safe {
282 color: #00ff88;
283 font-weight: bold;
284}
285
286.oxygen-display {
287 font-size: 3rem;
288 text-align: center;
289 font-weight: bold;
290 margin: 1rem 0;
291}
292
293.oxygen-critical {
294 color: #ff0055;
295 animation: pulse 1s infinite;
296}
297
298.oxygen-warning {
299 color: #ffb400;
300}
301
302.oxygen-safe {
303 color: #00ff88;
304}
305
306.oxygen-controls {
307 display: flex;
308 gap: 1rem;
309 justify-content: center;
310}
311
312.temp-display {
313 font-size: 2.5rem;
314 text-align: center;
315 color: #00b4d8;
316 margin: 1rem 0;
317}
318
319.temp-warning {
320 color: #00b4d8;
321 text-align: center;
322 font-size: 0.9rem;
323}
324
325.controls-section {
326 background: rgba(255, 255, 255, 0.05);
327 border: 2px solid #00ff88;
328 border-radius: 10px;
329 padding: 1.5rem;
330 margin-bottom: 2rem;
331}
332
333.button-group {
334 display: flex;
335 gap: 1rem;
336 justify-content: center;
337 flex-wrap: wrap;
338}
339
340.btn {
341 padding: 1rem 2rem;
342 border: none;
343 border-radius: 5px;
344 font-size: 1rem;
345 font-weight: bold;
346 cursor: pointer;
347 transition: all 0.3s ease;
348 font-family: 'Courier New', monospace;
349}
350
351.btn-toggle {
352 background: #00b4d8;
353 color: #ffffff;
354}
355
356.btn-danger {
357 background: #ff0055;
358 color: #ffffff;
359}
360
361.btn-success {
362 background: #00ff88;
363 color: #0a0e27;
364}
365
366.btn-small {
367 padding: 0.5rem 1rem;
368 font-size: 0.9rem;
369 background: #00b4d8;
370 color: #ffffff;
371}
372
373.btn:hover {
374 transform: scale(1.05);
375 box-shadow: 0 5px 15px rgba(0, 255, 136, 0.4);
376}
377
378.info-box {
379 background: rgba(0, 255, 136, 0.05);
380 border: 2px solid #00ff88;
381 border-radius: 10px;
382 padding: 1.5rem;
383}
384
385.info-box h3 {
386 color: #00ff88;
387 margin-top: 0;
388}
389
390.info-box ul {
391 list-style: none;
392 padding: 0;
393}
394
395.info-box li {
396 padding: 0.5rem 0;
397 color: #ffffff;
398 border-left: 3px solid #00b4d8;
399 padding-left: 1rem;
400 margin: 0.5rem 0;
401}
402
403.info-box strong {
404 color: #00b4d8;
405}
406</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. Jaka jest różnica między v-if a v-show?
2. Kiedy lepiej używać v-show zamiast v-if?
3. Czy można używać v-else-if w Vue?
4. Który fragment kodu poprawnie używa v-if z v-else?
5. Co dzieje się z elementem DOM, gdy warunek v-if jest ustawiony na false?
Zadania praktyczne w grze
- Edytor kodu
Użyj v-if, v-else-if i v-else do warunkowego renderowania statusów systemu.
- Układanie w pionie
Ułóż poprawny łańcuch warunkowy od v-if do v-else:
- Układanie w pionie
Ułóż etapy działania v-if od sprawdzenia warunku do renderowania:
- Klikanie w kolejności
Ułóż składnię: najpierw przycisk przełączający, a pod nim element z v-show:
- Edytor kodu
Użyj v-show i przycisku przełączającego, aby ukrywać i pokazywać sekcję.