Vue.js course Β· Module 8: Slots & Dynamic Components
Project: NOVA LAB Storage System
In this lesson4
Time to apply all the storage patterns in a single project! Dr. Nova assigns you the station's cargo bay management panel. The requirements sound simple, but each of them is a different mechanism: the panel has to show three views (the module list, the add form and the log), the form must not lose typed data when you switch views, and an alarm about a damaged module has to pop up above everything else. You already know each of these tools separately. In this project you will see how they work together.
The code in the editor uses:
- Named Slots - a flexible cargo panel layout with
header,defaultandfootersections, - Dynamic Components - switching station views,
- KeepAlive - preserving form state,
- Teleport - a critical alert modal rendered in
<body>, - a custom
alertevent that connects the cargo list to the modal.
You will add Scoped Slots for rendering the module list, and async components, yourself when you extend the project.
Architecture
Before you open the files, look at the component tree. App.vue fills the slots of the StationDashboard panel and swaps views in its main section; the alarm modal hangs next to it, outside the panel:
1App
2 βββ StationDashboard (named slots: header, default, footer)
3 β βββ #header β Navigation tabs
4 β βββ #default β <component :is> + KeepAlive
5 β β βββ CargoList (module list, emits alert)
6 β β βββ AddModule (form with local state)
7 β β βββ StationLog (event log)
8 β βββ #footer β station status bar
9 βββ Teleport β alert modal (rendered in body)StationDashboard knows nothing about cargo: it only receives a header with tabs, the main content and a footer. The header and the footer are wrapped in a v-if that checks slots.header and slots.footer, and the default slot has the fallback "No panel selected".
The Alarm Flow
The most interesting part is the path the alarm travels. The main section of the panel is a <component :is> wrapped in KeepAlive, with a listener for the alert event:
1<template #default>
2 <KeepAlive>
3 <component
4 :is="currentTab"
5 @alert="triggerAlert"
6 />
7 </KeepAlive>
8</template>The listener reaches whichever view is currently rendered. CargoList declares the event with defineEmits(['alert']) and emits it when you click "Inspect" next to a module with the damaged status. The triggerAlert function in App.vue stores the message in alertMsg and sets showAlert to true. The modal reads that flag:
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>The modal renders in <body>, so no panel frame will clip it, while its state still belongs to App.vue. The .self modifier makes a click on the semi-transparent background close the window, but a click inside the alarm frame doesn't.
Task
Analyze the code in the editor and understand how all the patterns work together:
- StationDashboard - a component with named slots (header, default, footer) creating a layout,
- CargoList - a list of cargo modules that emits
alertfor a damaged module, - Dynamic components -
<component :is>switches between CargoList, AddModule, and StationLog, - KeepAlive - preserves AddModule state after switching tabs,
- Teleport - the alert modal rendered in body, triggered from any panel.
Key observations: named slots in StationDashboard provide a flexible layout without a hardcoded structure, KeepAlive prevents form data loss in AddModule, and Teleport ensures proper modal positioning. Check it yourself: type a module name in the form, go to the log and come back - the text will still be there.
Extensions
Once you understand the finished code, extend it with the patterns it doesn't have yet:
- In
CargoList, wrap the row markup in<slot :item="item">, keeping the current look as the fallback. The scoped slot will then let the parent decide how to render each cargo module. - Replace the
StationLogimport withdefineAsyncComponent, so the log loads only when it is opened for the first time. - Add the
:include="['AddModule']"attribute toKeepAliveand check that the cache now covers only the form.
My advice: extend the project step by step, and after every change switch the tabs and trigger the alarm, because each of these mechanisms breaks in a different way. You will send the finished system to your mentor in the next task, together with a short description of the architecture.
Remember: a good storage bay is a well-coordinated team of patterns - slots build the frame, dynamic components swap the cargo, KeepAlive guards the state, and Teleport sends the alarm where the whole crew will see it.
Code for this lesson: 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: 'Cargo', component: CargoList },
10 { name: 'Add Module', component: AddModule },
11 { name: 'Log', 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>NOVA LAB Cargo System</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 | Modules: 4 | Status: Operational</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>STATION ALERT</h3>
59 <p>{{ alertMsg }}</p>
60 <button @click="showAlert = false">Dismiss</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>Spotted a mistake in this lesson?