Kurs Vue.js · Moduł 8: Sloty i komponenty dynamiczne

Projekt: System Magazynu NOVA LAB

4 min czytania
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, default i footer,
  • 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ą:

  1. StationDashboard - komponent z named slots (header, default, footer) tworzący layout,
  2. CargoList - lista modułów cargo, która przy uszkodzonym module emituje alert,
  3. Dynamic components - <component :is> przełącza między CargoList, AddModule i StationLog,
  4. KeepAlive - zachowuje stan AddModule po przełączeniu tabów,
  5. 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:

  1. W CargoList opakuj 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.
  2. Zamień import StationLog na defineAsyncComponent, żeby dziennik ładował się dopiero przy pierwszym otwarciu.
  3. Dodaj do KeepAlive atrybut :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?

Przydatne artykuły