Kurs Vue.js · Moduł 8: Sloty i komponenty dynamiczne
Scoped Slots
W tej lekcji5
W NOVA LAB system inwentaryzacji przekazuje dane o każdym module cargo, ale to operator decyduje, jak je wyświetlić - lista, tabela, karty. Scoped slots to mechanizm przekazywania danych z dziecka do treści slotu, którą pisze rodzic.
Problem
Zwykłe sloty dają rodzicowi kontrolę nad wyglądem, ale co jeśli dziecko ma dane, których rodzic potrzebuje? Na przykład dziecko iteruje po liście modułów i chce oddać rodzicowi kontrolę nad renderowaniem każdego elementu. Z poprzednich lekcji wiesz, że treść slotu jest kompilowana w scope rodzica, więc zmienna item z pętli v-for wewnątrz dziecka jest dla niej niewidoczna. Potrzebny jest kabel danych biegnący przez port dokujący w przeciwnym kierunku.
Przekazywanie danych przez Slot
Dziecko przekazuje dane jako atrybuty na tagu <slot>, dokładnie tak, jak przekazuje się props komponentom. Te wartości nazywamy propsami slotu (slot props). Poniższy ModuleList w każdym przebiegu pętli renderuje slot z bieżącym elementem i jego numerem:
1<!-- ModuleList.vue (dziecko) -->
2<script setup>
3import { ref } from 'vue'
4const items = ref([
5 { id: 1, name: 'Oxygen Tank', status: 'loaded' },
6 { id: 2, name: 'Water Filter', status: 'pending' },
7 { id: 3, name: 'Solar Cell', status: 'damaged' }
8])
9</script>
10
11<template>
12 <div class="module-list">
13 <div v-for="(item, index) in items" :key="item.id">
14 <slot :item="item" :index="index">
15 <!-- Fallback: proste renderowanie -->
16 <p>{{ item.name }}</p>
17 </slot>
18 </div>
19 </div>
20</template>Slot jest renderowany trzy razy, za każdym razem z danymi innego modułu. Fallback wciąż działa jak w pierwszej lekcji: rodzic, który nie poda treści, dostanie prostą listę nazw.
Odbieranie danych w rodzicu
Rodzic odbiera propsy slotu dyrektywą v-slot z nazwą zmiennej. Vue przekazuje wszystkie propsy slotu w jednym obiekcie, tutaj nazwanym slotProps, a ponieważ wartość v-slot zachowuje się jak parametr funkcji, możesz ją od razu zdestrukturyzować:
1<template>
2 <!-- Pełna składnia -->
3 <ModuleList v-slot="slotProps">
4 <div class="card">
5 <h3>{{ slotProps.item.name }}</h3>
6 <span>{{ slotProps.item.status }}</span>
7 </div>
8 </ModuleList>
9
10 <!-- Z destrukturyzacją -->
11 <ModuleList v-slot="{ item, index }">
12 <div class="card">
13 <h3>#{{ index + 1 }}: {{ item.name }}</h3>
14 <span :class="item.status">{{ item.status }}</span>
15 </div>
16 </ModuleList>
17</template>Oba zapisy są poprawne i dają te same dane; destrukturyzacja po prostu oszczędza powtarzania slotProps.. Do index dodajemy 1, bo numeracja w pętli zaczyna się od zera. Dane nie zmieniły właściciela: lista nadal należy do ModuleList, a rodzic dostaje tylko wgląd w każdy element na czas renderowania.
Jawny slot default
Zapis v-slot na tagu komponentu to skrót dla sytuacji, gdy używasz wyłącznie slotu domyślnego. Ten sam slot możesz opisać jawnie znacznikiem <template> z #default, a destrukturyzację wpisać w jego wartość:
1<template>
2 <ModuleList>
3 <template #default="{ item }">
4 <p>{{ item.name }}</p>
5 </template>
6 </ModuleList>
7</template>Efekt jest identyczny jak wcześniej. Wersja z <template> staje się jednak obowiązkowa, gdy obok slotu domyślnego wypełniasz także sloty nazwane - Vue nie pozwala wtedy umieścić v-slot na samym komponencie i zgłasza błąd kompilacji, żeby nie było wątpliwości, którego slotu dotyczą dane.
Named Scoped Slots
Scoped slots działają też z named slots. Poniższy DataTable ma dwa porty: header dla nagłówka tabeli i row, który w każdym wierszu dostaje item. Zakładamy, że listę items komponent trzyma w swoim skrypcie, tak jak ModuleList wyżej. Rodzic odbiera dane zapisem #row="{ item }":
1<!-- DataTable.vue -->
2<template>
3 <table>
4 <thead>
5 <tr>
6 <slot name="header">
7 <th>Name</th>
8 <th>Status</th>
9 </slot>
10 </tr>
11 </thead>
12 <tbody>
13 <tr v-for="item in items" :key="item.id">
14 <slot name="row" :item="item">
15 <td>{{ item.name }}</td>
16 <td>{{ item.status }}</td>
17 </slot>
18 </tr>
19 </tbody>
20 </table>
21</template>
22
23<!-- Rodzic -->
24<DataTable>
25 <template #header>
26 <th>Module Name</th>
27 <th>Current Status</th>
28 <th>Actions</th>
29 </template>
30
31 <template #row="{ item }">
32 <td>{{ item.name }}</td>
33 <td>{{ item.status }}</td>
34 <td><button>Inspect</button></td>
35 </template>
36</DataTable>Rodzic dołożył trzecią kolumnę z przyciskiem "Inspect", a DataTable nie zmienił ani jednej linii. Zwróć uwagę, że atrybut name nie trafia do propsów slotu - Vue go rezerwuje, więc item jest jedyną wartością, jaką dostaje rodzic.
Moja rada: nazywaj propsy slotu tak, jak nazywałbyś propsy komponentu - item, index, isSelected - i destrukturyzuj je od razu w v-slot. Kod rodzica czyta się wtedy jak zwykły szablon. Scoped slots wrócą w lekcji o zaawansowanych wzorcach, gdzie zbudujemy komponent, który w ogóle nie renderuje własnego HTML. W edytorze poniżej ModuleList pojawia się dwa razy: z domyślnym fallbackiem i z kartami, które rodzic buduje z item oraz index.
Zapamiętaj: scoped slot to port z kablem danych - dziecko podaje przez niego informacje, a rodzic decyduje, jak je pokazać.
Kod do tej lekcji: App.vue
1<script setup>
2import ModuleList from './ModuleList.vue'
3</script>
4
5<template>
6 <div class="nova-lab">
7 <h1>Demo: scoped slots</h1>
8
9 <h2>Domyślne renderowanie (fallback):</h2>
10 <ModuleList />
11
12 <h2>Własne karty od rodzica:</h2>
13 <ModuleList v-slot="{ item, index }">
14 <div class="custom-card">
15 <span class="index">#{{ index + 1 }}</span>
16 <strong>{{ item.name }}</strong>
17 <span
18 class="status"
19 :class="item.status"
20 >
21 {{ item.status }}
22 </span>
23 <span class="weight">{{ item.weight }} kg</span>
24 </div>
25 </ModuleList>
26 </div>
27</template>
28
29<style scoped>
30.nova-lab {
31 background: #0a0e27;
32 color: #00ff88;
33 min-height: 100vh;
34 padding: 20px;
35 font-family: monospace;
36}
37h1 { color: #00ff88; margin-bottom: 20px; }
38h2 { color: #00b4d8; margin: 20px 0 10px; font-size: 14px; }
39.custom-card {
40 display: flex;
41 align-items: center;
42 gap: 12px;
43 padding: 10px 15px;
44 border: 1px solid #00b4d8;
45 margin: 5px 0;
46 background: rgba(0, 180, 216, 0.05);
47}
48.index {
49 color: #666;
50 font-size: 12px;
51}
52.status {
53 padding: 2px 8px;
54 font-size: 11px;
55 border-radius: 3px;
56}
57.status.loaded { background: #00ff88; color: #0a0e27; }
58.status.pending { background: #ff9900; color: #0a0e27; }
59.status.damaged { background: #ff4444; color: #fff; }
60.weight { color: #666; margin-left: auto; }
61</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. Do czego służą scoped slots w Vue?
2. Jak komponent dziecka przekazuje dane do scoped slotu?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Stwórz komponent ModuleList który iteruje po liście i przekazuje każdy item przez scoped slot. Rodzic renderuje item po swojemu.
- Klikanie w kolejności
Ułóż kod odbioru danych ze scoped slotu w rodzicu:
- Edytor kodu
Stwórz komponent DataTable z named scoped slotami: #header dla nagłówka i #row="{ item }" dla wiersza tabeli.