Kurs Vue.js · Moduł 14: Dyrektywy i pluginy
Renderless Components i Functional Patterns
W tej lekcji6
W Centrum Dowodzenia NOVA LAB nie wszystkie moduły interfejsu muszą mieć wizualną reprezentację. Niektóre odpowiadają wyłącznie za logikę - to renderless components. Poznajmy zaawansowane wzorce komponentów Vue.
Czym jest Renderless Component?
Renderless component to komponent, który nie renderuje własnego HTML - zamiast tego udostępnia logikę przez slot. Cały wygląd definiuje komponent-rodzic.
1<script setup>
2import { ref, onMounted, onUnmounted } from 'vue'
3
4// Renderless component - logika zegara
5const now = ref(new Date())
6let timer = null
7
8onMounted(() => {
9 timer = setInterval(() => {
10 now.value = new Date()
11 }, 1000)
12})
13
14onUnmounted(() => clearInterval(timer))
15
16defineExpose({ now })
17</script>
18
19<template>
20 <slot :now="now" :formatted="now.toLocaleTimeString('pl-PL')" />
21</template>Użycie:
1<template>
2 <Clock v-slot="{ formatted }">
3 <div class="station-clock">{{ formatted }}</div>
4 </Clock>
5</template>Komponent Clock nie ma własnego HTML - przekazuje dane przez scoped slot, a rodzic decyduje o wyglądzie.
Wzorzec Renderless z logiką toggle
Popularny wzorzec - komponent zarządzający stanem toggle:
1<!-- Toggle.vue -->
2<script setup>
3import { ref } from 'vue'
4
5const isOn = ref(false)
6
7function toggle() {
8 isOn.value = !isOn.value
9}
10
11function setOn() { isOn.value = true }
12function setOff() { isOn.value = false }
13</script>
14
15<template>
16 <slot :isOn="isOn" :toggle="toggle" :setOn="setOn" :setOff="setOff" />
17</template>Użycie w różnych kontekstach:
1<template>
2 <!-- Jako przełącznik -->
3 <Toggle v-slot="{ isOn, toggle }">
4 <button @click="toggle">
5 System: {{ isOn ? 'AKTYWNY' : 'WYŁĄCZONY' }}
6 </button>
7 </Toggle>
8
9 <!-- Jako panel rozwijany -->
10 <Toggle v-slot="{ isOn, toggle }">
11 <div>
12 <h3 @click="toggle">Diagnostyka ▼</h3>
13 <div v-if="isOn">Szczegóły systemu...</div>
14 </div>
15 </Toggle>
16</template>Functional Components
W Vue 3 każdy komponent z <script setup> jest już zoptymalizowany. Ale możesz też tworzyć proste komponenty funkcyjne jako zwykłe funkcje renderujące:
1import { h } from 'vue'
2
3// Functional component - nie ma stanu, tylko props
4function StationBadge(props) {
5 return h('span', {
6 class: 'badge',
7 style: {
8 color: props.color || '#00ff88',
9 border: '1px solid ' + (props.color || '#00ff88'),
10 padding: '4px 12px',
11 borderRadius: '12px',
12 fontSize: '12px'
13 }
14 }, props.label)
15}
16
17StationBadge.props = ['label', 'color']Użycie w szablonie:
1<template>
2 <StationBadge label="ONLINE" color="#2ecc71" />
3 <StationBadge label="ALERT" color="#e74c3c" />
4</template>Render Functions z h()
Funkcja h() (hyperscript) tworzy VNode - wirtualny element DOM:
1import { h } from 'vue'
2
3// h(tag, props, children)
4h('div', { class: 'panel' }, [
5 h('h2', null, 'Tytuł'),
6 h('p', null, 'Treść panelu')
7])Komponent z render function zamiast szablonu:
1import { h, ref, defineComponent } from 'vue'
2
3const Counter = defineComponent({
4 setup() {
5 const count = ref(0)
6
7 return () => h('div', { class: 'counter' }, [
8 h('p', null, 'Licznik: ' + count.value),
9 h('button', {
10 onClick: () => count.value++
11 }, '+1')
12 ])
13 }
14})Higher-Order Components (HOC)
HOC to funkcja, która przyjmuje komponent i zwraca nowy komponent z dodaną funkcjonalnością:
1import { h, ref, onMounted } from 'vue'
2
3function withLoading(WrappedComponent) {
4 return defineComponent({
5 setup(props, { attrs }) {
6 const loading = ref(true)
7
8 onMounted(() => {
9 setTimeout(() => {
10 loading.value = false
11 }, 1000)
12 })
13
14 return () => {
15 if (loading.value) {
16 return h('div', { class: 'loading' }, 'Ładowanie modułu...')
17 }
18 return h(WrappedComponent, { ...attrs })
19 }
20 }
21 })
22}Użycie:
1const MissionPanelWithLoading = withLoading(MissionPanel)Kiedy używać tych wzorców?
| Wzorzec | Kiedy stosować |
|---|---|
| Renderless component | Logika wielokrotnego użytku z elastycznym UI |
| Functional component | Proste, bezstanowe elementy prezentacyjne |
| Render function (h()) | Dynamiczne generowanie struktury DOM |
| HOC | Dodawanie zachowania do istniejących komponentów |
Renderless components i scoped slots to idiomatyczny sposób Vue na separację logiki od prezentacji - szczególnie przydatny w systemach tak złożonych jak Centrum Dowodzenia NOVA LAB.
Kod do tej lekcji: App.vue
1<script setup>
2import { ref, h, defineComponent } from 'vue'
3
4// Renderless Toggle component
5const Toggle = defineComponent({
6 setup(_, { slots }) {
7 const isOn = ref(false)
8 function toggle() { isOn.value = !isOn.value }
9 function setOn() { isOn.value = true }
10 function setOff() { isOn.value = false }
11
12 return () => slots.default?.({
13 isOn: isOn.value,
14 toggle,
15 setOn,
16 setOff
17 })
18 }
19})
20
21// Functional component
22function StationBadge(props) {
23 return h('span', {
24 style: {
25 color: props.color || '#00ff88',
26 border: '1px solid ' + (props.color || '#00ff88'),
27 padding: '4px 12px',
28 borderRadius: '12px',
29 fontSize: '12px',
30 display: 'inline-block'
31 }
32 }, props.label)
33}
34StationBadge.props = ['label', 'color']
35
36// Counter with render function
37const Counter = defineComponent({
38 setup() {
39 const count = ref(0)
40 return () => h('div', { class: 'counter-box' }, [
41 h('span', null, 'Licznik: ' + count.value),
42 h('button', {
43 onClick: () => count.value++,
44 class: 'btn small',
45 style: 'margin-left: 10px'
46 }, '+1'),
47 h('button', {
48 onClick: () => count.value = 0,
49 class: 'btn small reset',
50 style: 'margin-left: 6px'
51 }, 'Reset')
52 ])
53 }
54})
55</script>
56
57<template>
58 <div class="nova-lab">
59 <h1>Renderless Components i Functional Patterns</h1>
60
61 <section class="section">
62 <h2>Renderless Toggle</h2>
63 <Toggle v-slot="{ isOn, toggle }">
64 <div>
65 <button @click="toggle" :class="['btn', { active: isOn }]">
66 System: {{ isOn ? 'AKTYWNY' : 'WYLACZONY' }}
67 </button>
68 <div v-if="isOn" class="panel">
69 <p>Systemy diagnostyczne uruchomione.</p>
70 <p>Temperatura: 22.5°C | Cisnienie: 1.01 atm</p>
71 </div>
72 </div>
73 </Toggle>
74 </section>
75
76 <section class="section">
77 <h2>Renderless Toggle - inny uzytek</h2>
78 <Toggle v-slot="{ isOn, toggle }">
79 <div>
80 <h3 @click="toggle" class="expandable">
81 {{ isOn ? '▼' : '▶' }} Logi stacji
82 </h3>
83 <ul v-if="isOn" class="log-list">
84 <li>[09:15] System nawigacji OK</li>
85 <li>[09:16] Skan sektora zakonczony</li>
86 <li>[09:17] Synchronizacja danych</li>
87 </ul>
88 </div>
89 </Toggle>
90 </section>
91
92 <section class="section">
93 <h2>Functional Component - StationBadge</h2>
94 <div class="badge-row">
95 <StationBadge label="ONLINE" color="#2ecc71" />
96 <StationBadge label="ALERT" color="#e74c3c" />
97 <StationBadge label="STANDBY" color="#f39c12" />
98 <StationBadge label="NOVA LAB" color="#00b4d8" />
99 </div>
100 </section>
101
102 <section class="section">
103 <h2>Render Function - Counter</h2>
104 <Counter />
105 </section>
106 </div>
107</template>
108
109<style scoped>
110.nova-lab {
111 background: #0a0e27;
112 color: #c0c0c0;
113 min-height: 100vh;
114 padding: 20px;
115 font-family: monospace;
116}
117h1 { color: #00ff88; margin-bottom: 20px; }
118h2 { color: #00b4d8; font-size: 16px; margin-bottom: 10px; }
119h3 { color: #00ff88; margin: 0; }
120.section {
121 background: rgba(0, 180, 216, 0.05);
122 border: 1px solid rgba(0, 180, 216, 0.2);
123 padding: 15px;
124 border-radius: 8px;
125 margin-bottom: 15px;
126}
127.btn {
128 background: rgba(0,255,136,0.15);
129 color: #00ff88;
130 border: 1px solid #00ff88;
131 padding: 8px 16px;
132 border-radius: 6px;
133 cursor: pointer;
134 font-family: monospace;
135}
136.btn.active { background: #00ff88; color: #0a0e27; }
137.btn.small { font-size: 12px; padding: 4px 10px; }
138.btn.reset { color: #e74c3c; border-color: #e74c3c; }
139.panel {
140 margin-top: 10px;
141 padding: 10px;
142 background: rgba(0,255,136,0.05);
143 border: 1px solid rgba(0,255,136,0.2);
144 border-radius: 6px;
145}
146.panel p { margin: 4px 0; font-size: 13px; }
147.expandable { cursor: pointer; user-select: none; }
148.expandable:hover { color: #00b4d8; }
149.log-list {
150 list-style: none;
151 padding: 0;
152 margin: 8px 0 0;
153}
154.log-list li {
155 padding: 4px 0;
156 font-size: 13px;
157 color: #888;
158 border-bottom: 1px solid rgba(0,180,216,0.1);
159}
160.badge-row { display: flex; gap: 10px; flex-wrap: wrap; }
161.counter-box {
162 display: flex;
163 align-items: center;
164 padding: 10px;
165 background: rgba(0,255,136,0.05);
166 border-radius: 6px;
167}
168</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 to jest renderless component w Vue?
2. Jak renderless component przekazuje dane do komponentu-rodzica?
To 2 z 4 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Zaimplementuj komponent Toggle z ref isOn, funkcją toggle i scoped slot.
- Układanie w pionie
Ułóż kroki tworzenia renderless component w poprawnej kolejności:
- Klikanie w kolejności
Ułóż poprawną kolejność argumentów funkcji h():
- Edytor kodu
Zaimplementuj StationBadge(props) jako funkcję zwracającą h('span', ...) z props.label i props.color.
- Układanie w poziomie
Ułóż poprawny import funkcji h z Vue:
- Edytor kodu
Zaimplementuj Counter z ref count i render function zwracającą h() z przyciskami +1 i Reset.
- Układanie w pionie
Ułóż wzorce od najprostszego do najbardziej zaawansowanego: