Kurs Vue.js · Moduł 14: Dyrektywy i pluginy

Renderless Components i Functional Patterns

3 min czytania
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?

WzorzecKiedy stosować
Renderless componentLogika wielokrotnego użytku z elastycznym UI
Functional componentProste, bezstanowe elementy prezentacyjne
Render function (h())Dynamiczne generowanie struktury DOM
HOCDodawanie 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. 1. Co to jest renderless component w Vue?

  2. 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:

Przydatne artykuły