Kurs Vue.js · Moduł 8: Sloty i komponenty dynamiczne
Projekt: System Magazynu NOVA LAB
W tej lekcji4
Czas zastosować wszystkie wzorce magazynowe w jednym projekcie! Dr. Nova zleca Ci panel zarządzania cargo bay stacji. Wymagania brzmią prosto, ale każde z nich to inny mechanizm: panel ma pokazywać trzy widoki (listę modułów, formularz dodawania i dziennik), formularz nie może tracić wpisanych danych przy przełączaniu, a alarm o uszkodzonym module musi wyskoczyć ponad wszystkim innym. Każde z tych narzędzi znasz już osobno. W projekcie zobaczysz, jak współpracują.
Kod w edytorze wykorzystuje:
- Named Slots - elastyczny layout panelu cargo z sekcjami
header,defaultifooter, - Dynamic Components - przełączanie widoków stacji,
- KeepAlive - zachowanie stanu formularzy,
- Teleport - modal alertu krytycznego renderowany w
<body>, - własne zdarzenie
alert, które łączy listę cargo z modalem.
Scoped Slots do renderowania listy modułów i async components dołożysz sam w rozbudowie projektu.
Architektura
Zanim otworzysz pliki, obejrzyj drzewo komponentów. App.vue wypełnia sloty panelu StationDashboard, a w jego głównej sekcji podmienia widoki; modal alarmu wisi obok, poza panelem:
1App
2 ├── StationDashboard (named slots: header, default, footer)
3 │ ├── #header → Navigation tabs
4 │ ├── #default → <component :is> + KeepAlive
5 │ │ ├── CargoList (lista modułów, emituje alert)
6 │ │ ├── AddModule (formularz z lokalnym stanem)
7 │ │ └── StationLog (log zdarzeń)
8 │ └── #footer → pasek statusu stacji
9 └── Teleport → modal alertu (renderowany w body)StationDashboard nie wie nic o cargo: dostaje tylko nagłówek z zakładkami, główną treść i stopkę. Nagłówek i stopka są opakowane w v-if sprawdzający slots.header i slots.footer, a slot domyślny ma fallback "No panel selected".
Przepływ alarmu
Najciekawsza jest droga, jaką pokonuje alarm. Główna sekcja panelu to <component :is> opakowany w KeepAlive, z nasłuchem zdarzenia alert:
1<template #default>
2 <KeepAlive>
3 <component
4 :is="currentTab"
5 @alert="triggerAlert"
6 />
7 </KeepAlive>
8</template>Nasłuch trafia do widoku, który jest akurat wyrenderowany. CargoList deklaruje zdarzenie przez defineEmits(['alert']) i emituje je, gdy klikniesz "Inspect" przy module ze statusem damaged. Funkcja triggerAlert w App.vue zapisuje komunikat w alertMsg i ustawia showAlert na true. Tę flagę czyta modal:
1<Teleport to="body">
2 <div v-if="showAlert" class="modal-overlay" @click.self="showAlert = false">
3 <div class="modal">
4 <h3>STATION ALERT</h3>
5 <p>{{ alertMsg }}</p>
6 <button @click="showAlert = false">Dismiss</button>
7 </div>
8 </div>
9</Teleport>Modal renderuje się w <body>, więc żadna ramka panelu go nie przytnie, a jego stan nadal należy do App.vue. Modyfikator .self sprawia, że kliknięcie w półprzezroczyste tło zamyka okno, a kliknięcie wewnątrz ramki alarmu - nie.
Zadanie
Przeanalizuj kod w edytorze i zrozum, jak wszystkie wzorce współpracują:
- StationDashboard - komponent z named slots (header, default, footer) tworzący layout,
- CargoList - lista modułów cargo, która przy uszkodzonym module emituje
alert, - Dynamic components -
<component :is>przełącza między CargoList, AddModule i StationLog, - KeepAlive - zachowuje stan AddModule po przełączeniu tabów,
- Teleport - modal alertu renderowany w body, wywoływany z dowolnego panelu.
Kluczowe obserwacje: named slots w StationDashboard dają elastyczny layout bez sztywnej struktury, KeepAlive zapobiega utracie danych formularza w AddModule, a Teleport zapewnia prawidłowe pozycjonowanie modala. Sprawdź to sam: wpisz nazwę modułu w formularzu, przejdź do dziennika i wróć - tekst wciąż tam będzie.
Rozbudowa
Gdy zrozumiesz gotowy kod, rozbuduj go o wzorce, których jeszcze w nim nie ma:
- W
CargoListopakuj znaczniki wiersza w<slot :item="item">, zostawiając obecny wygląd jako fallback. Wtedy scoped slot pozwoli rodzicowi zdecydować, jak renderować każdy moduł cargo. - Zamień import
StationLognadefineAsyncComponent, żeby dziennik ładował się dopiero przy pierwszym otwarciu. - Dodaj do
KeepAliveatrybut:include="['AddModule']"i sprawdź, że cache obejmuje teraz tylko formularz.
Moja rada: rozbudowuj projekt krok po kroku i po każdej zmianie przełączaj zakładki oraz wywołuj alarm, bo każdy z tych mechanizmów psuje się w inny sposób. Gotowy system wyślesz mentorowi w następnym zadaniu, razem z krótkim opisem architektury.
Zapamiętaj: dobry magazyn to zgrany zespół wzorców - sloty budują ramę, dynamic components wymieniają ładunek, KeepAlive pilnuje stanu, a Teleport wysyła alarm tam, gdzie zobaczy go cała załoga.
Kod do tej lekcji: App.vue
1<script setup>
2import { ref, shallowRef } from 'vue'
3import StationDashboard from './StationDashboard.vue'
4import CargoList from './CargoList.vue'
5import AddModule from './AddModule.vue'
6import StationLog from './StationLog.vue'
7
8const tabs = [
9 { name: 'Ładunek', component: CargoList },
10 { name: 'Dodaj moduł', component: AddModule },
11 { name: 'Dziennik', component: StationLog }
12]
13
14const currentTab = shallowRef(CargoList)
15const showAlert = ref(false)
16const alertMsg = ref('')
17
18const triggerAlert = (msg) => {
19 alertMsg.value = msg
20 showAlert.value = true
21}
22</script>
23
24<template>
25 <div class="nova-lab">
26 <StationDashboard>
27 <template #header>
28 <h1>System ładunków NOVA LAB</h1>
29 <nav class="tabs">
30 <button
31 v-for="tab in tabs"
32 :key="tab.name"
33 :class="{ active: currentTab === tab.component }"
34 @click="currentTab = tab.component"
35 >
36 {{ tab.name }}
37 </button>
38 </nav>
39 </template>
40
41 <template #default>
42 <KeepAlive>
43 <component
44 :is="currentTab"
45 @alert="triggerAlert"
46 />
47 </KeepAlive>
48 </template>
49
50 <template #footer>
51 <p>NOVA LAB v2087.3 | Moduły: 4 | Status: sprawny</p>
52 </template>
53 </StationDashboard>
54
55 <Teleport to="body">
56 <div v-if="showAlert" class="modal-overlay" @click.self="showAlert = false">
57 <div class="modal">
58 <h3>ALARM STACJI</h3>
59 <p>{{ alertMsg }}</p>
60 <button @click="showAlert = false">Zamknij</button>
61 </div>
62 </div>
63 </Teleport>
64 </div>
65</template>
66
67<style scoped>
68.nova-lab {
69 background: #0a0e27;
70 color: #00ff88;
71 min-height: 100vh;
72 padding: 20px;
73 font-family: monospace;
74}
75h1 { color: #00ff88; margin: 0; font-size: 18px; }
76.tabs { display: flex; gap: 5px; margin-top: 10px; }
77.tabs button {
78 background: rgba(0, 180, 216, 0.1);
79 border: 1px solid #00b4d8;
80 color: #00b4d8;
81 padding: 6px 14px;
82 cursor: pointer;
83 font-family: monospace;
84}
85.tabs button.active {
86 background: #00b4d8;
87 color: #0a0e27;
88}
89</style>
90
91<style>
92.modal-overlay {
93 position: fixed;
94 inset: 0;
95 background: rgba(0,0,0,0.8);
96 display: flex;
97 align-items: center;
98 justify-content: center;
99 z-index: 9999;
100}
101.modal {
102 background: #1a1a2e;
103 border: 3px solid #ff4444;
104 padding: 30px;
105 max-width: 400px;
106 text-align: center;
107 color: #00ff88;
108 font-family: monospace;
109}
110.modal h3 { color: #ff4444; margin-bottom: 15px; letter-spacing: 2px; }
111.modal button {
112 margin-top: 15px;
113 background: #ff4444;
114 color: white;
115 border: none;
116 padding: 8px 20px;
117 cursor: pointer;
118 font-family: monospace;
119}
120</style>Widzisz błąd w tej lekcji?