Kurs Vue.js · Moduł 8: Sloty i komponenty dynamiczne
Teleport
W tej lekcji6
W NOVA LAB system alarmowy wyświetla alert na głównym ekranie stacji, niezależnie od tego, który moduł go wywołał. Vue ma <Teleport> - mechanizm renderowania treści w innym miejscu DOM, poza hierarchią komponentu.
Problem
Modale, toasty i popupy logicznie należą do komponentu, ale wizualnie powinny być renderowane na najwyższym poziomie DOM (np. bezpośrednio w <body>), aby uniknąć problemów z z-index, overflow: hidden i pozycjonowaniem. Wyobraź sobie alarm tlenowy zagnieżdżony w panelu modułu, który ma overflow: hidden: modal zostanie przycięty do rozmiaru panelu. Jeśli któryś przodek ma właściwość transform, nawet position: fixed przestaje liczyć się względem okna przeglądarki. Przenoszenie modala do komponentu App.vue rozwiązałoby problem z CSS, ale rozerwałoby logikę: stan alarmu żyje przecież w module tlenu.
Podstawy Teleport
Teleport pozwala zostawić kod modala tam, gdzie jest jego logika, i jednocześnie wyrenderować jego HTML w innym miejscu. W poniższym panelu przycisk ustawia showModal, a cały modal jest opakowany w <Teleport to="body">:
1<script setup>
2import { ref } from 'vue'
3const showModal = ref(false)
4</script>
5
6<template>
7 <div class="module-panel">
8 <h2>Oxygen Module</h2>
9 <button @click="showModal = true">Show Alert</button>
10
11 <Teleport to="body">
12 <div v-if="showModal" class="modal-overlay">
13 <div class="modal">
14 <h3>Critical Alert!</h3>
15 <p>Oxygen level below 20%</p>
16 <button @click="showModal = false">Dismiss</button>
17 </div>
18 </div>
19 </Teleport>
20 </div>
21</template>Treść wewnątrz <Teleport> jest renderowana fizycznie w <body>, ale logicznie należy do komponentu - ma dostęp do jego danych i props. Zmienił się tylko adres w DOM: przekazywanie props, emitowanie zdarzeń i inject działają dokładnie tak jak wcześniej, a Vue Devtools nadal pokazują modal jako dziecko panelu.
Atrybut to
Atrybut to przyjmuje selektor CSS, a w razie potrzeby także gotowy element DOM. Najczęściej spotkasz trzy warianty:
1<!-- Do body -->
2<Teleport to="body">...</Teleport>
3
4<!-- Do elementu z ID -->
5<Teleport to="#modal-container">...</Teleport>
6
7<!-- Do elementu z klasą -->
8<Teleport to=".notifications">...</Teleport>Cel musi już istnieć w DOM w chwili montowania Teleportu. Najlepiej, gdy jest to element poza całą aplikacją Vue, na przykład <div id="modal-container"> dodany w index.html obok #app. Jeśli wskażesz selektor, którego nie ma, Vue wypisze ostrzeżenie, a treść się nie pojawi.
Deferred Teleport
Od Vue 3.5 Teleport ma atrybut defer. Dzięki niemu cel może być wyrenderowany przez tę samą aplikację nieco dalej w szablonie, bo Vue szuka go dopiero po zamontowaniu reszty drzewa:
1<template>
2 <Teleport defer to="#station-log">
3 <p>Nowy wpis w dzienniku stacji</p>
4 </Teleport>
5
6 <!-- ten element renderuje się dopiero niżej w szablonie -->
7 <div id="station-log"></div>
8</template>Bez defer ten sam kod szukałby #station-log, zanim element powstanie. Z defer cel musi się tylko pojawić w tym samym cyklu renderowania co Teleport.
Disabled Teleport
Możesz dynamicznie wyłączyć teleportację atrybutem disabled. Tutaj zmienna isMobile sprawia, że na telefonie popup renderuje się w miejscu, a na komputerze w <body>:
1<script setup>
2import { ref } from 'vue'
3const isMobile = ref(false)
4</script>
5
6<template>
7 <!-- Na mobile renderuj lokalnie, na desktop w body -->
8 <Teleport to="body" :disabled="isMobile">
9 <div class="popup">
10 <p>Popup content</p>
11 </div>
12 </Teleport>
13</template>Gdy disabled ma wartość true, treść renderuje się w oryginalnym miejscu w drzewie komponentów. Zmiana flagi w trakcie działania przenosi ten sam element tam i z powrotem, bez tworzenia go od nowa.
Wiele Teleportów do jednego celu
Możesz mieć kilka <Teleport> renderujących do tego samego elementu - treść jest dołączana w kolejności montowania, więc późniejsze trafiają za wcześniejsze:
1<template>
2 <Teleport to="#notifications">
3 <div class="toast">Alert 1</div>
4 </Teleport>
5
6 <Teleport to="#notifications">
7 <div class="toast">Alert 2</div>
8 </Teleport>
9</template>W kontenerze #notifications pojawi się najpierw "Alert 1", a pod nim "Alert 2". Żaden Teleport nie nadpisuje treści pozostałych, dlatego ten wzorzec świetnie nadaje się na stos powiadomień.
Moja rada: dodaj w index.html osobny kontener na modale obok #app i teleportuj tam wszystkie okna alarmowe. body też zadziała, ale własny kontener porządkuje warstwy strony. W projekcie na końcu modułu Teleport wyświetli alarm krytyczny wywołany z listy cargo. W edytorze poniżej moduł tlenu wywołuje taki alarm: modal ląduje w <body>, choć jego stan żyje w App.vue.
Zapamiętaj: Teleport przenosi ekran alarmowy tam, gdzie widać go najlepiej, ale sterowanie zostaje w module, który go wywołał.
Kod do tej lekcji: App.vue
1<script setup>
2import { ref } from 'vue'
3
4const showModal = ref(false)
5const alertMessage = ref('Krytyczny poziom tlenu: 15%')
6</script>
7
8<template>
9 <div class="nova-lab">
10 <h1>Demo: Teleport</h1>
11
12 <div class="module-panel">
13 <h2>Moduł tlenu</h2>
14 <p>Poziom: 15% - KRYTYCZNY</p>
15 <button @click="showModal = true" class="btn alert">
16 Pokaż alarm krytyczny
17 </button>
18 </div>
19
20 <div class="module-panel">
21 <h2>Moduł wody</h2>
22 <p>Poziom: 82% - w normie</p>
23 </div>
24
25 <!-- Modal przeniesiony przez Teleport do body -->
26 <Teleport to="body">
27 <div v-if="showModal" class="modal-overlay" @click.self="showModal = false">
28 <div class="modal">
29 <h3>ALARM KRYTYCZNY</h3>
30 <p>{{ alertMessage }}</p>
31 <p>Wymagana natychmiastowa reakcja!</p>
32 <button @click="showModal = false" class="btn">
33 Zamknij alarm
34 </button>
35 </div>
36 </div>
37 </Teleport>
38 </div>
39</template>
40
41<style scoped>
42.nova-lab {
43 background: #0a0e27;
44 color: #00ff88;
45 min-height: 100vh;
46 padding: 20px;
47 font-family: monospace;
48}
49h1 { color: #00ff88; margin-bottom: 20px; }
50.module-panel {
51 border: 2px solid #00b4d8;
52 padding: 15px;
53 margin: 10px 0;
54 background: rgba(0, 180, 216, 0.05);
55}
56.module-panel h2 { color: #00b4d8; margin-bottom: 8px; }
57.btn {
58 background: #00b4d8;
59 color: #0a0e27;
60 border: none;
61 padding: 10px 20px;
62 cursor: pointer;
63 font-family: monospace;
64 font-weight: bold;
65}
66.btn.alert {
67 background: #ff4444;
68 color: white;
69}
70</style>
71
72<style>
73/* Style globalne dla modala przeniesionego przez Teleport */
74.modal-overlay {
75 position: fixed;
76 inset: 0;
77 background: rgba(0, 0, 0, 0.8);
78 display: flex;
79 align-items: center;
80 justify-content: center;
81 z-index: 9999;
82}
83.modal {
84 background: #1a1a2e;
85 border: 3px solid #ff4444;
86 padding: 30px;
87 max-width: 400px;
88 text-align: center;
89 color: #00ff88;
90 font-family: monospace;
91}
92.modal h3 {
93 color: #ff4444;
94 margin-bottom: 15px;
95 font-size: 20px;
96 letter-spacing: 2px;
97}
98.modal p { margin: 8px 0; }
99.modal button {
100 margin-top: 15px;
101 background: #ff4444;
102 color: white;
103 border: none;
104 padding: 10px 25px;
105 cursor: pointer;
106 font-family: monospace;
107 font-weight: bold;
108}
109</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. Co robi komponent <KeepAlive> opakowujący dynamiczny komponent?
2. Do czego służy <Teleport> w Vue?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Opakuj <component :is> w <KeepAlive> i sprawdź, że stan formularza jest zachowany po przełączeniu tabów.
- Układanie w poziomie
Ułóż składnię KeepAlive z dynamicznym komponentem:
- Układanie w pionie
Ułóż zdarzenia przy przełączaniu komponentu w <KeepAlive>:
- Klikanie w kolejności
Ułóż kroki implementacji modala z Teleport:
- Edytor kodu
Stwórz komponent AlertModal używający <Teleport to="body"> do renderowania modala poza hierarchią komponentu.