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

Renderowanie List z v-for

5 min czytania
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. 1. Jak iterować po tablicy w Vue?

  2. 2. Dlaczego :key jest ważny w v-for?

  3. 3. Jak uzyskać indeks elementu w v-for?

  4. 4. Jak w v-for odczytać jednocześnie wartość i klucz każdej właściwości obiektu?

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

Przydatne artykuły