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

Dyrektywy Vue

5 min czytania
W tej lekcji4

Dyrektywy to specjalne atrybuty zaczynające się od v-. Każda dyrektywa to jak instrukcja dla systemu kontrolnego bazy na Marsie.

Wąsy z poprzedniej lekcji mają dwie luki. Nie sięgają do atrybutów, więc adres obrazka czujnika czy blokada przycisku nie mogą zależeć od danych. I nie słyszą operatora: kliknięcie w hologram niczego nie uruchamia. Dyrektywy łatają obie luki. Ich budowa jest zawsze taka sama: nazwa po przedrostku v-, argument po dwukropku, modyfikatory po kropkach i wyrażenie w cudzysłowie. W zapisie v-on:submit.prevent="onSubmit" nazwą jest on, argumentem submit, modyfikatorem prevent, a wartością funkcja onSubmit.

v-bind - Wiązanie Atrybutów

v-bind utrzymuje atrybut w zgodzie z danymi: po dwukropku podajesz nazwę atrybutu, a w cudzysłowie wyrażenie. Ponieważ ta dyrektywa pojawia się bez przerwy, ma skrót - sam dwukropek. Oto szablon panelu z czterema typowymi przypadkami:

1<template>
2  <div class="control-panel" style="background: #0a0e27; padding: 1rem;">
3    <!-- Pełna składnia -->
4    <img v-bind:src="sensorImage" v-bind:alt="sensorName">
5
6    <!-- Skrócona składnia (zalecana) -->
7    <img :src="sensorImage" :alt="sensorName">
8
9    <!-- Dynamiczne atrybuty -->
10    <a :href="documentationUrl" :class="linkClass">Dokumentacja</a>
11
12    <!-- Atrybuty logiczne -->
13    <button :disabled="isProcessing" class="btn-neon">Uruchom</button>
14  </div>
15</template>

Oba obrazki dają identyczny HTML - skrót niczego nie zmienia poza długością zapisu. W cudzysłowie stoi wyrażenie, a nie tekst: :alt="sensorName" wstawi wartość zmiennej, a zwykłe alt="sensorName" wpisałoby dosłownie słowo sensorName. Atrybut logiczny disabled pojawia się, gdy wartość jest prawdziwa, i znika dla false. Gdy wiązanie da null lub undefined, Vue usunie atrybut całkiem.

Dane do tych wiązań to zwykłe refy z sekcji skryptu - nic nowego w porównaniu z modułem 1:

1<script setup>
2import { ref } from 'vue'
3
4const sensorImage = ref('/images/oxygen-sensor.png')
5const sensorName = ref('Oxygen Sensor MK-7')
6const documentationUrl = ref('https://nova-lab.mars/docs')
7const linkClass = ref('neon-link')
8const isProcessing = ref(false)
9</script>

Wystarczy ustawić isProcessing.value = true, a przycisk dostanie atrybut disabled - szablonu nie dotykasz. Wygląd przycisku zapewnia zwykły CSS, bo v-bind nie zmienia zasad działania stylów:

1<style>
2.btn-neon {
3  background: transparent;
4  border: 2px solid #00ff88;
5  color: #00ff88;
6  padding: 0.5rem 1rem;
7  cursor: pointer;
8}
9.btn-neon:hover {
10  background: #00ff8822;
11  box-shadow: 0 0 10px #00ff88;
12}
13</style>

W sekcji stylów nie ma ani jednego elementu Vue. Polecam zawsze pisać skrót : - w gęstym szablonie od razu widać, które atrybuty żyją. Od Vue 3.4 działa nawet krótszy zapis: gdy atrybut i zmienna mają tę samą nazwę, :id znaczy to samo co :id="id".

v-on - Obsługa Zdarzeń

v-on podpina funkcję pod zdarzenie przeglądarki; skrótem jest małpa @. Wartością może być nazwa funkcji albo krótka instrukcja, na przykład wywołanie z argumentem. Specjalna zmienna $event przekazuje obiekt zdarzenia, gdy wywołujesz funkcję sam:

1<template>
2  <div class="reactor-controls" style="background: #0a0e27; color: #00b4d8;">
3    <!-- Pełna składnia -->
4    <button v-on:click="handleActivation">Aktywuj</button>
5
6    <!-- Skrócona składnia (zalecana) -->
7    <button @click="handleActivation" class="btn-reactor">Aktywuj Reaktor</button>
8
9    <!-- Instrukcja w szablonie -->
10    <button @click="powerLevel++">Zwiększ Moc</button>
11
12    <!-- Z argumentem -->
13    <button @click="sendSignal('ALPHA-BASE')">Wyślij Sygnał</button>
14
15    <!-- Z obiektem zdarzenia -->
16    <button @click="handleActivation($event)">Event Info</button>
17  </div>
18</template>

Kompilator rozpoznaje, co podałeś: sama nazwa handleActivation oznacza, że Vue wywoła funkcję i przekaże jej natywne zdarzenie jako jedyny argument. Zapis z nawiasami to instrukcja, więc zdarzenie trzeba podać jawnie przez $event. W powerLevel++ nie ma .value, bo w szablonie refy są rozpakowane.

Funkcje obsługi mieszkają w skrypcie, gdzie obowiązuje już .value:

1<script setup>
2import { ref } from 'vue'
3
4const powerLevel = ref(0)
5
6function handleActivation(event) {
7  console.log('Reaktor aktywowany!', event)
8}
9
10function sendSignal(baseCode) {
11  alert(`Wysyłanie sygnału do: ${baseCode}`)
12}
13</script>

Przycisk pozostał zwykłym <button> - v-on tylko dodaje do niego nasłuchiwanie zdarzenia, tak jak zrobiłby to addEventListener.

Modyfikatory Zdarzeń

Wywołania event.preventDefault() czy event.stopPropagation() zaśmiecają funkcje szczegółami DOM. Modyfikator dopisany po kropce załatwia to w szablonie. Poniższa ściąga pokazuje same znaczniki otwierające:

1<template>
2  <div class="mission-control">
3    <!-- Bez domyślnej akcji -->
4    <form @submit.prevent="onSubmit" class="mission-form">
5
6    <!-- Bez propagacji -->
7    <div @click.stop="onClick" class="stop-zone">
8
9    <!-- Tylko raz -->
10    <button @click.once="launchSequence" class="btn-launch">Uruchom Sekwencję</button>
11
12    <!-- Faza przechwytywania -->
13    <div @click.capture="onCapture" class="capture-zone">
14
15    <!-- Tylko sam element -->
16    <div @click.self="onSelf" class="self-zone">
17
18    <!-- Kombinacja -->
19    <a @click.stop.prevent="onClick" class="safe-link">
20  </div>
21</template>

.prevent wywołuje event.preventDefault(), więc formularz nie przeładuje strony. .stop wywołuje event.stopPropagation() i zdarzenie nie dotrze do rodziców. .once pozwala zadziałać najwyżej raz, .capture obsługuje zdarzenie w fazie przechwytywania, zanim trafi do dziecka, a .self reaguje tylko wtedy, gdy kliknięto sam element. Gdy zdarzenie nadejdzie, Vue najpierw stosuje modyfikatory, a dopiero potem woła Twoją funkcję. W łańcuchu liczy się kolejność: @click.prevent.self blokuje domyślną akcję także dla dzieci, a @click.self.prevent tylko dla samego elementu.

Modyfikatory Klawiszy

Zdarzenia keyup i keydown też mają modyfikatory. Vue zna aliasy .enter, .tab, .delete (Delete i Backspace), .esc, .space i strzałki, a klawisze systemowe to .ctrl, .alt, .shift i .meta. Funkcje z przykładu są zdefiniowane w skrypcie:

1<template>
2  <input @keyup.enter="sendCommand" placeholder="Komenda dla stacji">
3  <input @keyup.esc="clearCommand" placeholder="Esc czyści pole">
4  <button @click.ctrl="emergencyStop">Ctrl + klik: stop awaryjny</button>
5</template>

sendCommand ruszy tylko po Enterze, clearCommand tylko po Escape, a stop awaryjny wymaga wciśniętego Ctrl. Inne klawisze nie wywołają niczego - bez ani jednego if sprawdzającego event.key. W następnej lekcji zobaczysz, że :class i :style to specjalne odmiany v-bind, które przyjmują obiekty i tablice.

Zapamiętaj: dwukropek łączy atrybut z danymi, małpa łączy zdarzenie z funkcją, a kropka dopisuje do rozkazu protokół bezpieczeństwa.

Kod do tej lekcji: App.vue
1<script setup>
2import { ref } from 'vue'
3
4const stationStatus = ref('operational')
5const powerLevel = ref(85)
6const alertLevel = ref('low')
7const showDetails = ref(false)
8const logoUrl = ref('https://placehold.co/100/00ff88/0a0e27?text=NOVA')
9
10const systemMessages = ref([
11  { id: 1, text: 'Systemy podtrzymywania życia: ONLINE', type: 'success' },
12  { id: 2, text: 'Poziom energii: STABILNY', type: 'info' },
13  { id: 3, text: 'Komunikacja z Ziemią: AKTYWNA', type: 'success' }
14])
15
16function toggleDetails() {
17  showDetails.value = !showDetails.value
18}
19
20function increasePower() {
21  if (powerLevel.value < 100) {
22    powerLevel.value += 5
23  }
24}
25
26function decreasePower() {
27  if (powerLevel.value > 0) {
28    powerLevel.value -= 5
29  }
30}
31</script>
32
33<template>
34  <div class="control-panel">
35    <div class="station-header">
36      <!-- v-bind dla atrybutów src i alt -->
37      <img :src="logoUrl" :alt="'NOVA LAB Logo'" class="logo">
38      <h1>Stacja Badawcza NOVA</h1>
39    </div>
40
41    <div class="power-control">
42      <h2>Kontrola Energii</h2>
43      <!-- v-bind dla class i style -->
44      <div
45        class="power-bar"
46        :class="{ 'critical': powerLevel < 30, 'optimal': powerLevel >= 70 }"
47        :style="{ width: powerLevel + '%' }"
48      >
49        {{ powerLevel }}%
50      </div>
51
52      <!-- v-on (@) dla obsługi zdarzeń -->
53      <div class="controls">
54        <button @click="decreasePower" class="btn btn-danger">-5%</button>
55        <button @click="increasePower" class="btn btn-success">+5%</button>
56      </div>
57    </div>
58
59    <div class="status-section">
60      <h2>Status Systemów</h2>
61      <button @click="toggleDetails" class="btn btn-info">
62        {{ showDetails ? 'Ukryj szczegóły' : 'Pokaż szczegóły' }}
63      </button>
64
65      <div v-show="showDetails" class="messages">
66        <div
67          v-for="msg in systemMessages"
68          :key="msg.id"
69          :class="['message', 'message-' + msg.type]"
70        >
71          {{ msg.text }}
72        </div>
73      </div>
74    </div>
75
76    <div class="info-box">
77      <h3>Dyrektywy Vue.js</h3>
78      <ul>
79        <li><strong>v-bind</strong> (skrót :) - wiązanie atrybutów</li>
80        <li><strong>v-on</strong> (skrót @) - obsługa zdarzeń</li>
81        <li><strong>:class</strong> - dynamiczne klasy CSS</li>
82        <li><strong>:style</strong> - dynamiczne style inline</li>
83      </ul>
84    </div>
85  </div>
86</template>
87
88<style scoped>
89.control-panel {
90  background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
91  min-height: 100vh;
92  padding: 2rem;
93  color: #ffffff;
94  font-family: 'Courier New', monospace;
95}
96
97.station-header {
98  text-align: center;
99  margin-bottom: 2rem;
100}
101
102.logo {
103  width: 100px;
104  height: 100px;
105  border-radius: 50%;
106  border: 3px solid #00ff88;
107  box-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
108}
109
110h1, h2, h3 {
111  color: #00ff88;
112  text-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
113}
114
115.power-control {
116  background: rgba(0, 255, 136, 0.1);
117  border: 2px solid #00ff88;
118  border-radius: 10px;
119  padding: 1.5rem;
120  margin-bottom: 2rem;
121}
122
123.power-bar {
124  height: 40px;
125  background: linear-gradient(90deg, #00ff88, #00b4d8);
126  border-radius: 20px;
127  display: flex;
128  align-items: center;
129  justify-content: center;
130  font-weight: bold;
131  transition: all 0.3s ease;
132  margin: 1rem 0;
133}
134
135.power-bar.critical {
136  background: linear-gradient(90deg, #ff0055, #ff6600);
137  animation: pulse 1s infinite;
138}
139
140.power-bar.optimal {
141  box-shadow: 0 0 20px rgba(0, 255, 136, 0.8);
142}
143
144@keyframes pulse {
145  0%, 100% { opacity: 1; }
146  50% { opacity: 0.7; }
147}
148
149.controls {
150  display: flex;
151  gap: 1rem;
152  justify-content: center;
153  margin-top: 1rem;
154}
155
156.btn {
157  padding: 0.75rem 1.5rem;
158  border: none;
159  border-radius: 5px;
160  font-size: 1rem;
161  font-weight: bold;
162  cursor: pointer;
163  transition: all 0.3s ease;
164  font-family: 'Courier New', monospace;
165}
166
167.btn-success {
168  background: #00ff88;
169  color: #0a0e27;
170}
171
172.btn-danger {
173  background: #ff0055;
174  color: #ffffff;
175}
176
177.btn-info {
178  background: #00b4d8;
179  color: #ffffff;
180}
181
182.btn:hover {
183  transform: scale(1.05);
184  box-shadow: 0 5px 15px rgba(0, 255, 136, 0.4);
185}
186
187.status-section {
188  background: rgba(0, 180, 216, 0.1);
189  border: 2px solid #00b4d8;
190  border-radius: 10px;
191  padding: 1.5rem;
192  margin-bottom: 2rem;
193}
194
195.messages {
196  margin-top: 1rem;
197}
198
199.message {
200  padding: 1rem;
201  margin: 0.5rem 0;
202  border-radius: 5px;
203  border-left: 4px solid;
204}
205
206.message-success {
207  background: rgba(0, 255, 136, 0.2);
208  border-color: #00ff88;
209}
210
211.message-info {
212  background: rgba(0, 180, 216, 0.2);
213  border-color: #00b4d8;
214}
215
216.info-box {
217  background: rgba(255, 255, 255, 0.05);
218  border: 2px solid #00ff88;
219  border-radius: 10px;
220  padding: 1.5rem;
221}
222
223.info-box ul {
224  list-style: none;
225  padding: 0;
226}
227
228.info-box li {
229  padding: 0.5rem 0;
230  color: #ffffff;
231}
232
233.info-box strong {
234  color: #00b4d8;
235}
236</style>

Drugi edytor to laboratorium modyfikatorów: wyślij w nim formularz z .prevent, kliknij przyciski z .stop i .once, a w polach tekstowych wypróbuj Enter, Escape i skrót Ctrl+S.

Kod do tej lekcji: App.vue
1<script setup>
2import { ref } from 'vue'
3
4const commandInput = ref('')
5const searchQuery = ref('')
6const formData = ref({
7  mission: '',
8  priority: 'medium',
9  description: ''
10})
11
12const logs = ref([])
13const systemMessages = ref([
14  { id: 1, text: 'System uruchomiony' },
15  { id: 2, text: 'Połączono z bazą na Marsie' },
16  { id: 3, text: 'Wszystkie systemy sprawne' }
17])
18let nextMessageId = 4
19
20function handleCommand(event) {
21  event.preventDefault()
22  if (commandInput.value.trim()) {
23    logs.value.unshift({
24      id: Date.now(),
25      command: commandInput.value,
26      timestamp: new Date().toLocaleTimeString()
27    })
28    commandInput.value = ''
29  }
30}
31
32function preventRefresh(event) {
33  event.preventDefault()
34  alert('Formularz został zatrzymany przez event.preventDefault()')
35}
36
37function stopPropagation(event) {
38  event.stopPropagation()
39  systemMessages.value.unshift({ id: nextMessageId++, text: 'Kliknięto przycisk - propagacja zatrzymana' })
40}
41
42function handleContainerClick() {
43  systemMessages.value.unshift({ id: nextMessageId++, text: 'Kliknięto kontener' })
44}
45
46function runOnce() {
47  systemMessages.value.unshift({ id: nextMessageId++, text: 'Ta funkcja zadziała tylko raz!' })
48}
49
50function handleEnter() {
51  systemMessages.value.unshift({ id: nextMessageId++, text: `Wyszukano: ${searchQuery.value}` })
52  searchQuery.value = ''
53}
54
55function handleEscape() {
56  searchQuery.value = ''
57  systemMessages.value.unshift({ id: nextMessageId++, text: 'Wyszukiwanie wyczyszczone klawiszem ESC' })
58}
59
60function handleCtrlS(event) {
61  event.preventDefault()
62  systemMessages.value.unshift({ id: nextMessageId++, text: 'Misja zapisana (Ctrl+S)' })
63}
64
65function submitMission(event) {
66  event.preventDefault()
67  logs.value.unshift({
68    id: Date.now(),
69    command: `Misja: ${formData.value.mission} | Priorytet: ${formData.value.priority}`,
70    timestamp: new Date().toLocaleTimeString()
71  })
72  formData.value.mission = ''
73  formData.value.description = ''
74}
75</script>
76
77<template>
78  <div class="modifiers-lab">
79    <h1>Laboratorium Modyfikatorów - NOVA LAB</h1>
80
81    <!-- Sekcja modyfikatorów zdarzeń -->
82    <section class="lab-section">
83      <h2>Modyfikatory zdarzeń</h2>
84
85      <!-- Modyfikator .prevent -->
86      <div class="demo-card">
87        <h3>.prevent - Zatrzymanie domyślnej akcji</h3>
88        <form @submit.prevent="preventRefresh" class="demo-form">
89          <input
90            type="text"
91            placeholder="Ten formularz nie odświeży strony"
92            class="input-field"
93          >
94          <button type="submit" class="btn btn-primary">
95            Wyślij (preventDefault)
96          </button>
97        </form>
98        <code class="code-hint">@submit.prevent="preventRefresh"</code>
99      </div>
100
101      <!-- Modyfikator .stop -->
102      <div class="demo-card">
103        <h3>.stop - Zatrzymanie propagacji zdarzenia</h3>
104        <div @click="handleContainerClick" class="click-container">
105          Kliknij w kontener (zdarzenie się propaguje)
106          <button @click.stop="stopPropagation" class="btn btn-warning">
107            Kliknij w przycisk (propagacja zatrzymana)
108          </button>
109        </div>
110        <code class="code-hint">@click.stop="stopPropagation"</code>
111      </div>
112
113      <!-- Modyfikator .once -->
114      <div class="demo-card">
115        <h3>.once - Funkcja obsługi działa tylko raz</h3>
116        <button @click.once="runOnce" class="btn btn-success">
117          Kliknij mnie (działa tylko raz)
118        </button>
119        <code class="code-hint">@click.once="runOnce"</code>
120      </div>
121
122      <!-- Kilka modyfikatorów naraz -->
123      <div class="demo-card">
124        <h3>Łączenie modyfikatorów</h3>
125        <form @submit.prevent.stop="handleCommand" class="demo-form">
126          <input
127            v-model="commandInput"
128            type="text"
129            placeholder="Wpisz komendę..."
130            class="input-field"
131          >
132          <button type="submit" class="btn btn-primary">
133            Wykonaj komendę
134          </button>
135        </form>
136        <code class="code-hint">@submit.prevent.stop="handleCommand"</code>
137      </div>
138    </section>
139
140    <!-- Sekcja modyfikatorów klawiszy -->
141    <section class="lab-section">
142      <h2>Modyfikatory klawiszy</h2>
143
144      <!-- Modyfikator .enter -->
145      <div class="demo-card">
146        <h3>.enter - Wykrywa klawisz Enter</h3>
147        <input
148          v-model="searchQuery"
149          @keyup.enter="handleEnter"
150          type="text"
151          placeholder="Wpisz i naciśnij Enter..."
152          class="input-field"
153        >
154        <code class="code-hint">@keyup.enter="handleEnter"</code>
155      </div>
156
157      <!-- Modyfikator .esc -->
158      <div class="demo-card">
159        <h3>.esc - Wykrywa klawisz Escape</h3>
160        <input
161          v-model="searchQuery"
162          @keyup.esc="handleEscape"
163          type="text"
164          placeholder="Wpisz i naciśnij ESC aby wyczyścić..."
165          class="input-field"
166        >
167        <code class="code-hint">@keyup.esc="handleEscape"</code>
168      </div>
169
170      <!-- Klawisze systemowe -->
171      <div class="demo-card">
172        <h3>.ctrl + klawisz - Kombinacje klawiszy</h3>
173        <textarea
174          v-model="formData.description"
175          @keydown.ctrl.s="handleCtrlS"
176          placeholder="Naciśnij Ctrl+S aby zapisać..."
177          class="textarea-field"
178          rows="4"
179        ></textarea>
180        <code class="code-hint">@keydown.ctrl.s="handleCtrlS"</code>
181      </div>
182
183      <!-- Kilka modyfikatorów klawiszy -->
184      <div class="demo-card">
185        <h3>Kompleksowy formularz z modyfikatorami</h3>
186        <form @submit.prevent="submitMission" class="mission-form">
187          <input
188            v-model="formData.mission"
189            type="text"
190            placeholder="Nazwa misji (Enter wysyła)..."
191            class="input-field"
192          >
193          <select v-model="formData.priority" class="select-field">
194            <option value="low">Niski priorytet</option>
195            <option value="medium">Średni priorytet</option>
196            <option value="high">Wysoki priorytet</option>
197          </select>
198          <button type="submit" class="btn btn-primary">
199            Wyślij misję
200          </button>
201        </form>
202      </div>
203    </section>
204
205    <!-- Komunikaty systemu -->
206    <section class="lab-section">
207      <h2>Komunikaty systemu</h2>
208      <div class="messages-container">
209        <div
210          v-for="msg in systemMessages.slice(0, 10)"
211          :key="msg.id"
212          class="message-item"
213        >
214          <span class="message-time">{{ new Date().toLocaleTimeString() }}</span>
215          <span class="message-text">{{ msg.text }}</span>
216        </div>
217      </div>
218    </section>
219
220    <!-- Dziennik komend -->
221    <section class="lab-section" v-if="logs.length > 0">
222      <h2>Dziennik komend</h2>
223      <div class="logs-container">
224        <div
225          v-for="log in logs"
226          :key="log.id"
227          class="log-item"
228        >
229          <span class="log-time">{{ log.timestamp }}</span>
230          <span class="log-command">{{ log.command }}</span>
231        </div>
232      </div>
233    </section>
234
235    <!-- Ramka z podsumowaniem -->
236    <div class="info-box">
237      <h3>Modyfikatory zdarzeń i klawiszy w Vue</h3>
238      <div class="info-grid">
239        <div>
240          <h4>Modyfikatory zdarzeń:</h4>
241          <ul>
242            <li><strong>.prevent</strong> - event.preventDefault()</li>
243            <li><strong>.stop</strong> - event.stopPropagation()</li>
244            <li><strong>.once</strong> - funkcja obsługi tylko raz</li>
245            <li><strong>.capture</strong> - faza przechwytywania</li>
246            <li><strong>.self</strong> - tylko jeśli target === currentTarget</li>
247            <li><strong>.passive</strong> - { passive: true }</li>
248          </ul>
249        </div>
250        <div>
251          <h4>Modyfikatory klawiszy:</h4>
252          <ul>
253            <li><strong>.enter</strong> - klawisz Enter</li>
254            <li><strong>.tab</strong> - klawisz Tab</li>
255            <li><strong>.delete</strong> - Delete/Backspace</li>
256            <li><strong>.esc</strong> - klawisz Escape</li>
257            <li><strong>.space</strong> - spacja</li>
258            <li><strong>.up/.down/.left/.right</strong> - strzałki</li>
259            <li><strong>.ctrl/.alt/.shift/.meta</strong> - klawisze systemowe</li>
260          </ul>
261        </div>
262      </div>
263    </div>
264  </div>
265</template>
266
267<style scoped>
268.modifiers-lab {
269  background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
270  min-height: 100vh;
271  padding: 2rem;
272  color: #ffffff;
273  font-family: 'Courier New', monospace;
274}
275
276h1 {
277  text-align: center;
278  color: #00ff88;
279  text-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
280  margin-bottom: 3rem;
281}
282
283h2 {
284  color: #00b4d8;
285  margin-bottom: 1.5rem;
286  border-bottom: 2px solid #00b4d8;
287  padding-bottom: 0.5rem;
288}
289
290h3 {
291  color: #00ff88;
292  font-size: 1.1rem;
293  margin-bottom: 1rem;
294}
295
296.lab-section {
297  background: rgba(0, 255, 136, 0.05);
298  border: 2px solid #00ff88;
299  border-radius: 10px;
300  padding: 2rem;
301  margin-bottom: 2rem;
302}
303
304.demo-card {
305  background: rgba(0, 180, 216, 0.1);
306  border: 2px solid #00b4d8;
307  border-radius: 8px;
308  padding: 1.5rem;
309  margin-bottom: 1.5rem;
310}
311
312.demo-form {
313  display: flex;
314  gap: 1rem;
315  margin-bottom: 1rem;
316  flex-wrap: wrap;
317}
318
319.mission-form {
320  display: flex;
321  flex-direction: column;
322  gap: 1rem;
323}
324
325.input-field, .select-field, .textarea-field {
326  background: rgba(0, 0, 0, 0.3);
327  border: 2px solid #00b4d8;
328  border-radius: 5px;
329  padding: 0.75rem;
330  color: #ffffff;
331  font-family: 'Courier New', monospace;
332  font-size: 1rem;
333  flex: 1;
334  min-width: 200px;
335}
336
337.input-field:focus, .select-field:focus, .textarea-field:focus {
338  outline: none;
339  border-color: #00ff88;
340  box-shadow: 0 0 10px rgba(0, 255, 136, 0.3);
341}
342
343.input-field::placeholder, .textarea-field::placeholder {
344  color: rgba(255, 255, 255, 0.5);
345}
346
347.btn {
348  padding: 0.75rem 1.5rem;
349  border: none;
350  border-radius: 5px;
351  font-size: 1rem;
352  font-weight: bold;
353  cursor: pointer;
354  transition: all 0.3s ease;
355  font-family: 'Courier New', monospace;
356}
357
358.btn-primary {
359  background: #00ff88;
360  color: #0a0e27;
361}
362
363.btn-warning {
364  background: #ffb400;
365  color: #0a0e27;
366}
367
368.btn-success {
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.click-container {
379  background: rgba(255, 180, 0, 0.1);
380  border: 2px dashed #ffb400;
381  border-radius: 5px;
382  padding: 2rem;
383  text-align: center;
384  cursor: pointer;
385  transition: all 0.3s ease;
386  margin-bottom: 1rem;
387}
388
389.click-container:hover {
390  background: rgba(255, 180, 0, 0.2);
391}
392
393.code-hint {
394  display: block;
395  background: rgba(0, 0, 0, 0.5);
396  border-left: 4px solid #00ff88;
397  padding: 0.75rem;
398  border-radius: 5px;
399  color: #00ff88;
400  font-family: 'Courier New', monospace;
401  font-size: 0.9rem;
402}
403
404.messages-container, .logs-container {
405  background: rgba(0, 0, 0, 0.3);
406  border-radius: 8px;
407  padding: 1rem;
408  max-height: 300px;
409  overflow-y: auto;
410}
411
412.message-item, .log-item {
413  padding: 0.75rem;
414  background: rgba(0, 180, 216, 0.1);
415  border-left: 4px solid #00b4d8;
416  border-radius: 5px;
417  margin-bottom: 0.5rem;
418  display: flex;
419  gap: 1rem;
420  align-items: center;
421}
422
423.message-time, .log-time {
424  color: #ffb400;
425  font-size: 0.9rem;
426  min-width: 100px;
427}
428
429.message-text, .log-command {
430  color: #ffffff;
431  flex: 1;
432}
433
434.info-box {
435  background: rgba(0, 255, 136, 0.05);
436  border: 2px solid #00ff88;
437  border-radius: 10px;
438  padding: 2rem;
439}
440
441.info-box h3 {
442  margin-top: 0;
443  margin-bottom: 1.5rem;
444}
445
446.info-grid {
447  display: grid;
448  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
449  gap: 2rem;
450}
451
452.info-box h4 {
453  color: #00b4d8;
454  margin-bottom: 1rem;
455}
456
457.info-box ul {
458  list-style: none;
459  padding: 0;
460}
461
462.info-box li {
463  padding: 0.5rem 0;
464  color: #ffffff;
465  border-left: 3px solid #00b4d8;
466  padding-left: 1rem;
467  margin: 0.5rem 0;
468}
469
470.info-box strong {
471  color: #00ff88;
472}
473</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. Do czego służy dyrektywa v-bind?

  2. 2. Jaki jest skrót dla v-bind:href?

  3. 3. Do czego służy v-on?

  4. 4. Co oznacza <img :src> bez wartości, gdy w <script setup> istnieje zmienna src?

  5. 5. Co robi .prevent w @submit.prevent?

  6. 6. Co robi modyfikator .stop w v-on?

Zadania praktyczne w grze

  • Edytor kodu

    Użyj v-bind (skrót :) do przypisania dynamicznego src, alt, href i disabled.

  • Klikanie w kolejności

    Ułóż składnię obsługi kliknięcia z argumentem:

  • Edytor kodu

    Użyj v-on (lub skrótu @), aby obsłużyć zdarzenia click, submit i keyup.

  • Układanie w poziomie

    Ułóż poprawną składnię v-bind dla dynamicznego atrybutu src:

  • Układanie w poziomie

    Ułóż poprawną składnię v-on dla obsługi kliknięcia:

  • Układanie w pionie

    Uporządkuj kroki obsługi zdarzenia click w Vue:

  • Edytor kodu

    Użyj modyfikatorów .prevent, .stop i .once w obsłudze zdarzeń.

Przydatne artykuły