Kurs Vue.js · Moduł 2: Składnia szablonów
Renderowanie List z v-for
W tej lekcji5
Jak baza NOVA wyświetla dane z wielu sensorów jednocześnie, v-for renderuje listę elementów.
Stacja ma dziesiątki czujników i ta liczba ciągle się zmienia: technicy montują nowe, zdejmują uszkodzone. Kopiowanie znacznika <li> dla każdego czujnika ręcznie nie wchodzi w grę, bo po każdej zmianie sprzętu trzeba by poprawiać szablon. Dyrektywa v-for powtarza element dla każdej pozycji z danych, a gdy dane się zmienią, lista na hologramie zmieni się razem z nimi.
Podstawowe v-for
Składnia przypomina pętlę z JavaScriptu: w v-for="sensor in sensors" słowo sensor to alias bieżącej pozycji, a sensors to tablica źródłowa. Zapis (sensor, index) daje też numer pozycji liczony od zera. Atrybut :key omówimy za chwilę - na razie wystarczy, że to unikalny identyfikator:
1<template>
2 <div class="sensors-list" style="background: #0a0e27; padding: 1rem;">
3 <!-- Iteracja po tablicy -->
4 <ul style="list-style: none; color: #00ff88;">
5 <li v-for="sensor in sensors" :key="sensor.id"
6 style="border-left: 2px solid #00b4d8; padding: 0.5rem; margin: 0.5rem 0;">
7 {{ sensor.name }} - {{ sensor.value }}{{ sensor.unit }}
8 </li>
9 </ul>
10
11 <!-- Z indeksem -->
12 <div v-for="(sensor, index) in sensors" :key="sensor.id"
13 style="color: #00b4d8;">
14 [{{ index + 1 }}] {{ sensor.name }}: {{ sensor.value }}
15 </div>
16 </div>
17</template>Na ekranie pojawią się trzy elementy li i trzy div z numerami od 1, bo do indeksu dodajemy jedynkę dla ludzi. Alias sensor istnieje tylko wewnątrz elementu z v-for. Zamiast in możesz też napisać of, bliżej składni iteratorów JavaScriptu.
Dane to tablica obiektów w refie:
1<script setup>
2import { ref } from 'vue'
3
4const sensors = ref([
5 { id: 1, name: 'Oxygen Level', value: 21.5, unit: '%' },
6 { id: 2, name: 'Temperature', value: 22, unit: '°C' },
7 { id: 3, name: 'Pressure', value: 101.3, unit: 'kPa' }
8])
9</script>Vue wykrywa metody zmieniające tablicę, takie jak push(), splice() czy sort(), więc nowy czujnik dodany przez push od razu pojawi się na liście. Metody filter() czy slice() zwracają nową tablicę i nie ruszają starej - wynik trzeba przypisać do sensors.value.
Iteracja po Obiektach
v-for przechodzi też po właściwościach obiektu. Aliasy to kolejno wartość, klucz i indeks, a kolejność odpowiada wynikowi Object.values():
1<template>
2 <div class="module-specs" style="background: #0a0e27; padding: 1rem;">
3 <!-- Wartości -->
4 <div v-for="value in moduleDetails" :key="value" style="color: #00ff88;">
5 {{ value }}
6 </div>
7
8 <!-- Klucz i wartość -->
9 <div v-for="(value, key) in moduleDetails" :key="key"
10 style="color: #00b4d8; border-bottom: 1px solid #00ff8833; padding: 0.25rem;">
11 <strong>{{ key }}:</strong> {{ value }}
12 </div>
13
14 <!-- Klucz, wartość i indeks -->
15 <div v-for="(value, key, index) in moduleDetails" :key="key">
16 {{ index }}. {{ key }}: {{ value }}
17 </div>
18 </div>
19</template>Ostatnia pętla wypisze na przykład 0. name: Reactor Core Alpha. W pierwszej wartość służy za klucz tylko dlatego, że wszystkie wartości są tu różne - bezpieczniej użyć nazwy właściwości. Sam obiekt jest zwykłym obiektem reaktywnym:
1<script setup>
2import { reactive } from 'vue'
3
4const moduleDetails = reactive({
5 name: 'Reactor Core Alpha',
6 type: 'Plasma Fusion',
7 output: '500 MW',
8 location: 'Central Hub, Mars Base'
9})
10</script>Dlaczego :key jest ważny?
Bez klucza Vue aktualizuje listę strategią w miejscu: gdy kolejność danych się zmieni, nie przesuwa elementów, tylko poprawia zawartość każdego na jego pozycji. Dla samego tekstu to wystarcza, ale pole formularza albo komponent ze stanem zostaną wtedy przy złej pozycji. Unikalny :key nadaje każdemu elementowi tożsamość. Porównaj trzy warianty, z których każdy pokazuje sam znacznik otwierający:
1<template>
2 <!-- DOBRZE - unikalne ID -->
3 <div v-for="item in items" :key="item.id">
4
5 <!-- ŹLE - indeks jako key (problemy przy sortowaniu/filtrowaniu) -->
6 <div v-for="(item, index) in items" :key="index">
7
8 <!-- ŹLE - brak key -->
9 <div v-for="item in items">
10</template>Indeks jako klucz działa jak brak klucza, bo po sortowaniu ten sam numer dostaje inny czujnik. Brak klucza nie wywoła w Vue błędu ani ostrzeżenia w konsoli - wychwyci go dopiero linter (reguła vue/require-v-for-key z eslint-plugin-vue). Przy powtórzonych kluczach Vue w trybie deweloperskim ostrzega podczas aktualizacji listy. Klucz powinien być napisem lub liczbą, najlepiej identyfikatorem z danych.
v-for z v-if
Gdy chcesz pokazać tylko aktywne czujniki, kusi dopisanie v-if do elementu z v-for. W Vue 3 v-if ma jednak wyższy priorytet, więc nie widzi zmiennej sensor i render kończy się błędem. Są dwa poprawne wyjścia:
1<template>
2 <div class="active-sensors">
3 <!-- DOBRZE - template wrapper -->
4 <template v-for="sensor in sensors" :key="sensor.id">
5 <div v-if="sensor.isActive" style="color: #00ff88;">
6 {{ sensor.name }}
7 </div>
8 </template>
9
10 <!-- LUB - filtr w computed -->
11 <div v-for="sensor in activeSensors" :key="sensor.id"
12 style="color: #00ff88; border-left: 2px solid #00ff88; padding: 0.5rem;">
13 {{ sensor.name }}
14 </div>
15 </div>
16</template>Obie wersje pokażą te same nazwy. Przy <template v-for> klucz stoi na znaczniku <template>, a nie na dziecku - inaczej kompilator zgłosi błąd. Druga wersja korzysta z computed:
1<script setup>
2import { ref, computed } from 'vue'
3
4const sensors = ref([
5 { id: 1, name: 'Oxygen Level', isActive: true },
6 { id: 2, name: 'Dust Filter', isActive: false },
7 { id: 3, name: 'Pressure', isActive: true }
8])
9
10const activeSensors = computed(() =>
11 sensors.value.filter(s => s.isActive)
12)
13</script>Oryginalna tablica się nie zmienia - computed zwraca nową, przefiltrowaną. Dokumentacja Vue zaleca właśnie ten sposób i ja też go wybieram: szablon zostaje prosty, a filtr ma nazwę.
Zakres Liczb
v-for przyjmuje też liczbę całkowitą i powtarza element tyle razy. Uwaga: n zaczyna się od 1, nie od 0:
1<template>
2 <div class="power-bars">
3 <!-- Renderuj 10 razy -->
4 <span v-for="n in 10" :key="n"
5 style="display: inline-block; width: 20px; height: 20px; background: #00ff88; margin: 2px;">
6 </span>
7
8 <!-- Wynik: 10 zielonych kwadratów -->
9 </div>
10</template>Dziesięć kwadratów tworzy wskaźnik mocy, a z :class mógłbyś zapalać tylko te, dla których n nie przekracza poziomu energii. W następnej lekcji sięgniesz bezpośrednio po element takiej listy przez template refs.
Zapamiętaj: v-for to linia montażowa czujników, a :key to numer seryjny, po którym Vue rozpoznaje każdy egzemplarz.
Kod do tej lekcji: App.vue
1<script setup>
2import { ref } from 'vue'
3
4const crewMembers = ref([
5 { id: 1, name: 'Dr. Elena Rodriguez', role: 'Dowódca', specialization: 'Astrobiologia', status: 'active' },
6 { id: 2, name: 'James Chen', role: 'Inżynier', specialization: 'Systemy mechaniczne', status: 'active' },
7 { id: 3, name: 'Sarah Johnson', role: 'Naukowiec', specialization: 'Geologia', status: 'on-mission' },
8 { id: 4, name: 'Marcus Williams', role: 'Pilot', specialization: 'Nawigacja', status: 'active' },
9 { id: 5, name: 'Dr. Yuki Tanaka', role: 'Lekarz pokładowy', specialization: 'Medycyna kosmiczna', status: 'active' }
10])
11
12const missionLogs = ref([
13 { id: 101, sol: 42, type: 'discovery', message: 'Znaleziono próbki lodowego rdzenia' },
14 { id: 102, sol: 43, type: 'maintenance', message: 'Serwis paneli słonecznych zakończony' },
15 { id: 103, sol: 44, type: 'research', message: 'Analiza składu mineralnego skał' },
16 { id: 104, sol: 45, type: 'communication', message: 'Kontakt z Centrum Kontroli Misji' }
17])
18
19const equipment = ref([
20 { id: 'eq1', name: 'Rover Explorer-1', status: 'operational', battery: 85 },
21 { id: 'eq2', name: 'System wiertniczy', status: 'operational', battery: 92 },
22 { id: 'eq3', name: 'Stacja meteorologiczna', status: 'maintenance', battery: 45 },
23 { id: 'eq4', name: 'Zespół anten łączności', status: 'operational', battery: 100 }
24])
25
26const modules = ref([
27 {
28 id: 'm1',
29 name: 'Moduł mieszkalny',
30 sections: ['Kwatery załogi', 'Laboratorium', 'Szklarnia']
31 },
32 {
33 id: 'm2',
34 name: 'Moduł badawczy',
35 sections: ['Laboratorium chemiczne', 'Laboratorium biologiczne', 'Laboratorium geologiczne']
36 },
37 {
38 id: 'm3',
39 name: 'Moduł magazynowy',
40 sections: ['Magazyn żywności', 'Magazyn sprzętu', 'Zbiorniki wody']
41 }
42])
43
44function addCrewMember() {
45 const newId = Math.max(...crewMembers.value.map(m => m.id)) + 1
46 crewMembers.value.push({
47 id: newId,
48 name: 'Nowy rekrut',
49 role: 'Kadet',
50 specialization: 'Ogólna',
51 status: 'training'
52 })
53}
54
55function removeCrewMember(id) {
56 crewMembers.value = crewMembers.value.filter(member => member.id !== id)
57}
58
59function addLog() {
60 const newId = Math.max(...missionLogs.value.map(l => l.id)) + 1
61 const currentSol = Math.max(...missionLogs.value.map(l => l.sol)) + 1
62 missionLogs.value.unshift({
63 id: newId,
64 sol: currentSol,
65 type: 'info',
66 message: 'Nowy wpis w dzienniku misji'
67 })
68}
69</script>
70
71<template>
72 <div class="mission-base">
73 <h1>Baza NOVA - Lista Zasobów</h1>
74
75 <!-- v-for z tablicą obiektów + :key -->
76 <section class="crew-section">
77 <div class="section-header">
78 <h2>Załoga ({{ crewMembers.length }} osób)</h2>
79 <button @click="addCrewMember" class="btn btn-add">+ Dodaj członka</button>
80 </div>
81
82 <div class="crew-grid">
83 <div
84 v-for="member in crewMembers"
85 :key="member.id"
86 class="crew-card"
87 :class="'status-' + member.status"
88 >
89 <div class="crew-header">
90 <h3>{{ member.name }}</h3>
91 <span class="role-badge">{{ member.role }}</span>
92 </div>
93 <p class="specialization">{{ member.specialization }}</p>
94 <div class="crew-footer">
95 <span class="status-badge" :class="member.status">
96 {{ member.status }}
97 </span>
98 <button @click="removeCrewMember(member.id)" class="btn-remove">×</button>
99 </div>
100 </div>
101 </div>
102 </section>
103
104 <!-- v-for z indeksem -->
105 <section class="logs-section">
106 <div class="section-header">
107 <h2>Dziennik Misji</h2>
108 <button @click="addLog" class="btn btn-add">+ Nowy wpis</button>
109 </div>
110
111 <div class="logs-list">
112 <div
113 v-for="(log, index) in missionLogs"
114 :key="log.id"
115 class="log-entry"
116 :class="'log-' + log.type"
117 >
118 <span class="log-number">#{{ index + 1 }}</span>
119 <span class="log-sol">Sol {{ log.sol }}</span>
120 <span class="log-type">{{ log.type }}</span>
121 <span class="log-message">{{ log.message }}</span>
122 </div>
123 </div>
124 </section>
125
126 <!-- v-for z zagnieżdżonymi pętlami -->
127 <section class="modules-section">
128 <h2>Moduły Bazy</h2>
129 <div class="modules-grid">
130 <div
131 v-for="module in modules"
132 :key="module.id"
133 class="module-card"
134 >
135 <h3>{{ module.name }}</h3>
136 <ul class="sections-list">
137 <li
138 v-for="(section, sectionIndex) in module.sections"
139 :key="module.id + '-' + sectionIndex"
140 class="section-item"
141 >
142 <span class="section-number">{{ sectionIndex + 1 }}.</span>
143 {{ section }}
144 </li>
145 </ul>
146 </div>
147 </div>
148 </section>
149
150 <!-- v-for po sprzęcie ze stylami zależnymi od stanu -->
151 <section class="equipment-section">
152 <h2>Sprzęt</h2>
153 <div class="equipment-list">
154 <div
155 v-for="item in equipment"
156 :key="item.id"
157 class="equipment-card"
158 >
159 <div class="equipment-name">{{ item.name }}</div>
160 <div class="equipment-status" :class="item.status">
161 {{ item.status }}
162 </div>
163 <div class="battery-container">
164 <div class="battery-label">Bateria:</div>
165 <div class="battery-bar">
166 <div
167 class="battery-fill"
168 :style="{ width: item.battery + '%' }"
169 :class="{
170 'battery-low': item.battery < 50,
171 'battery-medium': item.battery >= 50 && item.battery < 80,
172 'battery-high': item.battery >= 80
173 }"
174 >
175 {{ item.battery }}%
176 </div>
177 </div>
178 </div>
179 </div>
180 </div>
181 </section>
182
183 <div class="info-box">
184 <h3>v-for - Renderowanie List</h3>
185 <ul>
186 <li><strong>v-for="item in items"</strong> - iteracja po tablicy</li>
187 <li><strong>v-for="(item, index) in items"</strong> - z indeksem</li>
188 <li><strong>:key="item.id"</strong> - ZAWSZE używaj unikalnego klucza</li>
189 <li><strong>Zagnieżdżone pętle</strong> - możesz zagnieżdżać v-for</li>
190 <li><strong>v-for z :class/:style</strong> - dynamiczne stylowanie elementów listy</li>
191 </ul>
192 </div>
193 </div>
194</template>
195
196<style scoped>
197.mission-base {
198 background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
199 min-height: 100vh;
200 padding: 2rem;
201 color: #ffffff;
202 font-family: 'Courier New', monospace;
203}
204
205h1 {
206 text-align: center;
207 color: #00ff88;
208 text-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
209 margin-bottom: 3rem;
210}
211
212h2 {
213 color: #00b4d8;
214 margin-bottom: 1.5rem;
215}
216
217section {
218 background: rgba(0, 255, 136, 0.05);
219 border: 2px solid #00ff88;
220 border-radius: 10px;
221 padding: 1.5rem;
222 margin-bottom: 2rem;
223}
224
225.section-header {
226 display: flex;
227 justify-content: space-between;
228 align-items: center;
229 margin-bottom: 1.5rem;
230 flex-wrap: wrap;
231 gap: 1rem;
232}
233
234.btn-add {
235 background: #00ff88;
236 color: #0a0e27;
237 border: none;
238 padding: 0.75rem 1.5rem;
239 border-radius: 5px;
240 font-weight: bold;
241 cursor: pointer;
242 font-family: 'Courier New', monospace;
243 transition: all 0.3s ease;
244}
245
246.btn-add:hover {
247 transform: scale(1.05);
248 box-shadow: 0 5px 15px rgba(0, 255, 136, 0.4);
249}
250
251.crew-grid {
252 display: grid;
253 grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
254 gap: 1.5rem;
255}
256
257.crew-card {
258 background: rgba(0, 180, 216, 0.1);
259 border: 2px solid #00b4d8;
260 border-radius: 8px;
261 padding: 1.5rem;
262 transition: all 0.3s ease;
263}
264
265.crew-card:hover {
266 transform: translateY(-5px);
267 box-shadow: 0 10px 30px rgba(0, 180, 216, 0.3);
268}
269
270.crew-card.status-on-mission {
271 border-color: #ffb400;
272 background: rgba(255, 180, 0, 0.1);
273}
274
275.crew-header {
276 display: flex;
277 justify-content: space-between;
278 align-items: start;
279 margin-bottom: 0.5rem;
280}
281
282.crew-header h3 {
283 color: #00ff88;
284 margin: 0;
285 font-size: 1.1rem;
286}
287
288.role-badge {
289 background: #00b4d8;
290 color: #ffffff;
291 padding: 0.25rem 0.75rem;
292 border-radius: 12px;
293 font-size: 0.8rem;
294 font-weight: bold;
295}
296
297.specialization {
298 color: #ffffff;
299 margin: 0.5rem 0;
300 font-size: 0.9rem;
301}
302
303.crew-footer {
304 display: flex;
305 justify-content: space-between;
306 align-items: center;
307 margin-top: 1rem;
308}
309
310.status-badge {
311 padding: 0.25rem 0.75rem;
312 border-radius: 12px;
313 font-size: 0.8rem;
314 font-weight: bold;
315 text-transform: uppercase;
316}
317
318.status-badge.active {
319 background: rgba(0, 255, 136, 0.3);
320 color: #00ff88;
321}
322
323.status-badge.on-mission {
324 background: rgba(255, 180, 0, 0.3);
325 color: #ffb400;
326}
327
328.status-badge.training {
329 background: rgba(0, 180, 216, 0.3);
330 color: #00b4d8;
331}
332
333.btn-remove {
334 background: #ff0055;
335 color: #ffffff;
336 border: none;
337 width: 30px;
338 height: 30px;
339 border-radius: 50%;
340 font-size: 1.5rem;
341 cursor: pointer;
342 display: flex;
343 align-items: center;
344 justify-content: center;
345 transition: all 0.3s ease;
346}
347
348.btn-remove:hover {
349 transform: scale(1.1);
350 box-shadow: 0 0 10px rgba(255, 0, 85, 0.5);
351}
352
353.logs-list {
354 display: flex;
355 flex-direction: column;
356 gap: 0.75rem;
357}
358
359.log-entry {
360 display: grid;
361 grid-template-columns: auto auto auto 1fr;
362 gap: 1rem;
363 padding: 1rem;
364 background: rgba(0, 180, 216, 0.1);
365 border-left: 4px solid #00b4d8;
366 border-radius: 5px;
367 align-items: center;
368 transition: all 0.3s ease;
369}
370
371.log-entry:hover {
372 background: rgba(0, 180, 216, 0.2);
373 transform: translateX(5px);
374}
375
376.log-entry.log-discovery {
377 border-left-color: #00ff88;
378}
379
380.log-entry.log-maintenance {
381 border-left-color: #ffb400;
382}
383
384.log-number {
385 color: #00b4d8;
386 font-weight: bold;
387 font-size: 0.9rem;
388}
389
390.log-sol {
391 color: #00ff88;
392 font-weight: bold;
393}
394
395.log-type {
396 background: rgba(0, 255, 136, 0.2);
397 color: #00ff88;
398 padding: 0.25rem 0.75rem;
399 border-radius: 12px;
400 font-size: 0.8rem;
401 text-transform: uppercase;
402}
403
404.log-message {
405 color: #ffffff;
406}
407
408.modules-grid {
409 display: grid;
410 grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
411 gap: 1.5rem;
412}
413
414.module-card {
415 background: rgba(0, 255, 136, 0.1);
416 border: 2px solid #00ff88;
417 border-radius: 8px;
418 padding: 1.5rem;
419}
420
421.module-card h3 {
422 color: #00ff88;
423 margin-top: 0;
424 margin-bottom: 1rem;
425}
426
427.sections-list {
428 list-style: none;
429 padding: 0;
430 margin: 0;
431}
432
433.section-item {
434 padding: 0.75rem;
435 background: rgba(0, 180, 216, 0.1);
436 border-radius: 5px;
437 margin-bottom: 0.5rem;
438 color: #ffffff;
439 transition: all 0.3s ease;
440}
441
442.section-item:hover {
443 background: rgba(0, 180, 216, 0.2);
444 transform: translateX(5px);
445}
446
447.section-number {
448 color: #00b4d8;
449 font-weight: bold;
450 margin-right: 0.5rem;
451}
452
453.equipment-list {
454 display: grid;
455 grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
456 gap: 1.5rem;
457}
458
459.equipment-card {
460 background: rgba(0, 180, 216, 0.1);
461 border: 2px solid #00b4d8;
462 border-radius: 8px;
463 padding: 1.5rem;
464}
465
466.equipment-name {
467 color: #00ff88;
468 font-weight: bold;
469 font-size: 1.1rem;
470 margin-bottom: 0.5rem;
471}
472
473.equipment-status {
474 padding: 0.25rem 0.75rem;
475 border-radius: 12px;
476 font-size: 0.8rem;
477 font-weight: bold;
478 display: inline-block;
479 margin-bottom: 1rem;
480 text-transform: uppercase;
481}
482
483.equipment-status.operational {
484 background: rgba(0, 255, 136, 0.3);
485 color: #00ff88;
486}
487
488.equipment-status.maintenance {
489 background: rgba(255, 180, 0, 0.3);
490 color: #ffb400;
491}
492
493.battery-container {
494 margin-top: 1rem;
495}
496
497.battery-label {
498 color: #ffffff;
499 font-size: 0.9rem;
500 margin-bottom: 0.5rem;
501}
502
503.battery-bar {
504 background: rgba(255, 255, 255, 0.1);
505 border-radius: 10px;
506 height: 30px;
507 overflow: hidden;
508}
509
510.battery-fill {
511 height: 100%;
512 display: flex;
513 align-items: center;
514 justify-content: center;
515 font-weight: bold;
516 font-size: 0.9rem;
517 transition: all 0.3s ease;
518}
519
520.battery-low {
521 background: linear-gradient(90deg, #ff0055, #ff6600);
522}
523
524.battery-medium {
525 background: linear-gradient(90deg, #ffb400, #ffd700);
526 color: #0a0e27;
527}
528
529.battery-high {
530 background: linear-gradient(90deg, #00ff88, #00b4d8);
531 color: #0a0e27;
532}
533
534.info-box {
535 background: rgba(0, 255, 136, 0.05);
536 border: 2px solid #00ff88;
537 border-radius: 10px;
538 padding: 1.5rem;
539 margin-top: 2rem;
540}
541
542.info-box h3 {
543 color: #00ff88;
544 margin-top: 0;
545}
546
547.info-box ul {
548 list-style: none;
549 padding: 0;
550}
551
552.info-box li {
553 padding: 0.5rem 0;
554 color: #ffffff;
555 border-left: 3px solid #00b4d8;
556 padding-left: 1rem;
557 margin: 0.5rem 0;
558}
559
560.info-box strong {
561 color: #00b4d8;
562}
563</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 iterować po tablicy w Vue?
2. Dlaczego :key jest ważny w v-for?
3. Jak uzyskać indeks elementu w v-for?
4. Jak w v-for odczytać jednocześnie wartość i klucz każdej właściwości obiektu?
5. Która praktyka jest zalecana gdy chcesz filtrować elementy listy?
Zadania praktyczne w grze
- Edytor kodu
Użyj v-for do renderowania listy sensorów z :key i indeksem.
- Klikanie w kolejności
Ułóż składnię v-for z :key (najpierw v-for, potem :key):
- Układanie w pionie
Uporządkuj argumenty v-for po obiekcie od wartości do indeksu:
- Układanie w poziomie
Ułóż poprawną składnię v-for z :key:
- Edytor kodu
Użyj v-for do iteracji po właściwościach obiektu (value, key, index).
- Układanie w pionie
Ułóż od najlepszego do najgorszego sposobu ustawiania :key:
- Klikanie w kolejności
Ułóż poprawne użycie template wrapper z v-for i v-if: