Kurs Vue.js · Moduł 2: Składnia szablonów

Dyrektywy Warunkowe

5 min czytania
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:

Cechav-ifv-show
RenderowanieWarunkoweZawsze
Koszt początkowyNiskiWyższy
Koszt przełączaniaWyższyNiski
UżycieRzadkie zmianyCzę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. 1. Jaka jest różnica między v-if a v-show?

  2. 2. Kiedy lepiej używać v-show zamiast v-if?

  3. 3. Czy można używać v-else-if w Vue?

  4. 4. Który fragment kodu poprawnie używa v-if z v-else?

  5. 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ę.

Przydatne artykuły