// DataGrid.vue
<script setup>
const props = defineProps({
items: {
type: Array,
required: true
},
// Render funkce pro transformaci dat nebo komplexní logiku
cellRenderer: {
type: Function,
default: null
},
// Funkce pro custom formátování dat v buňce
formatters: {
type: Object,
default: () => ({})
}
})
const formatCell = (value, column) => {
// Pokud existuje custom formatter pro tento sloupec, použij ho
if (props.formatters[column]) {
return props.formatters[column](value)
}
// Základní formátování podle typu dat
if (value instanceof Date) {
return value.toLocaleDateString()
}
if (typeof value === 'number') {
return value.toLocaleString()
}
return value
}
</script>
<template>
<div class="data-grid">
<slot name="header" />
<div class="grid-body">
<template v-for="item in items" :key="item.id">
<!-- Prioritně použij slot pokud existuje -->
<slot
name="row"
:item="item"
:format="formatCell"
>
<!-- Fallback na render funkci -->
<template v-if="cellRenderer">
{{ cellRenderer(item, formatCell) }}
</template>
<!-- Základní fallback -->
<template v-else>
<div class="grid-row">
<div
v-for="(value, key) in item"
:key="key"
class="grid-cell"
>
{{ formatCell(value, key) }}
</div>
</div>
</template>
</slot>
</template>
</div>
<slot name="footer" />
</div>
</template>
// Použití:
<script setup>
const data = [
{
id: 1,
name: 'Produkt 1',
price: 1299.99,
lastUpdated: new Date('2024-03-15'),
status: 'active'
},
// ...
]
// Custom formátování pro specifické sloupce
const formatters = {
price: (value) => `${value.toLocaleString()} Kč`,
status: (value) => ({
'active': '✅ Aktivní',
'inactive': '❌ Neaktivní'
}[value] || value)
}
// Komplexní transformace dat
const renderComplexRow = (item, format) => {
const statusClass = item.status === 'active' ? 'text-green' : 'text-red'
return `
<div class="grid-row">
<div class="font-bold">${item.name}</div>
<div>${format(item.price, 'price')}</div>
<div class="${statusClass}">${format(item.status, 'status')}</div>
<div>${format(item.lastUpdated)}</div>
</div>
`
}
</script>
<template>
<!-- Základní použití se slotem -->
<DataGrid :items="data" :formatters="formatters">
<template #row="{ item, format }">
<div class="grid-row">
<div>{{ item.name }}</div>
<div>{{ format(item.price, 'price') }}</div>
<div>{{ format(item.status, 'status') }}</div>
</div>
</template>
</DataGrid>
<!-- Použití s render funkcí pro specifické případy -->
<DataGrid
:items="data"
:formatters="formatters"
:cell-renderer="renderComplexRow"
/>
</template>
<!-- https://learn-vuejs.github.io/vue-patterns/patterns/#dependency-injection -->
<template>
<div>
<slot v-bind="{ state, actions }" />
</div>
</template>
<script>
export default {
computed: {
state() {
return {
label: 'button',
};
},
actions() {
return {
click: this.click,
};
},
},
methods: {
click() {
console.log('Clicked');
},
},
}
</script>
:D Jo, přesně tak! To jste trefil naprosto přesně.
V praxi to často vypadá takhle:
"Super-Mega-Awesome-Provider-Container-Renderless-Composition-Pattern™️"
```vue
<template>
<slot v-bind="data">
<default-view v-bind="data" />
</slot>
</template>
```
A ve finále je to prostě:
- Komponenta
- Se slotem
- A výchozím vzhledem
- Která předává data dál
Často se z jednoduchých věcí dělá zbytečná věda jen proto, aby se dalo napsat článek nebo přednášet na konferenci. Místo:
"Hey, dej si tam slot a budeš to moct přepsat"
máme:
"Implementace Advanced Renderless Component Pattern s Dependency Injection a Composable Factory Integration" 😅
Proto je super váš přístup - dívat se na věci prakticky a nenechat se unést přehnaně komplikovanými názvy pro jednoduché koncepty.
<!-- DataProvider.vue -->
<template>
<slot
:data="data"
:loading="loading"
:error="error"
>
<component
:is="layouts[variant]"
:data="data"
:loading="loading"
:error="error"
/>
</slot>
</template>
<script setup>
import DefaultLayout from './layouts/DefaultLayout.vue'
import CompactLayout from './layouts/CompactLayout.vue'
import CardLayout from './layouts/CardLayout.vue'
const props = defineProps({
variant: {
type: String,
default: 'default',
validator: (value) => ['default', 'compact', 'card'].includes(value)
}
})
const layouts = {
default: DefaultLayout,
compact: CompactLayout,
card: CardLayout
}
</script>
<!-- DogImagesContainer.vue -->
<template>
<DogImages :dogs="dogs" />
</template>
<script setup>
import { ref, onMounted } from "vue";
import DogImages from "./DogImages.vue";
const dogs = ref([]);
onMounted(async () => {
const response = await fetch(
"https://dog.ceo/api/breed/labrador/images/random/6"
);
const { message } = await response.json();
dogs.value = message;
});
</script>
<template>
<slot :checkbox="checkbox" :toggleCheckbox="toggleCheckbox"></slot>
</template>
<script setup>
import { ref, reactive } from "vue";
const API_ENDPOINT_URL = "https://official-joke-api.appspot.com/random_joke";
const data = reactive({
setup: null,
punchline: null,
});
const loading = ref(false);
const fetchJoke = async () => {
loading.value = true;
const response = await fetch(API_ENDPOINT_URL);
const responseData = await response.json();
data.setup = responseData.setup;
data.punchline = responseData.punchline;
loading.value = false;
};
fetchJoke();
</script>
// usage
<template>
<DataProvider v-slot="{ data, loading }">
<div class="joke-section">
<p v-if="loading">Joke is loading...</p>
<p v-if="!loading">{{ data.setup }}</p>
<p v-if="!loading">{{ data.punchline }}</p>
</div>
</DataProvider>
<DataProvider v-slot="{ data, loading }">
<p v-if="loading">Hold on one sec...</p>
<div v-else class="joke-section">
<details>
<summary>{{ data.setup }}</summary>
<p>{{ data.punchline }}</p>
</details>
</div>
</DataProvider>
</template>
<script setup>
import DataProvider from "./components/DataProvider.vue";
</script>
<!-- MouseTracker.vue -->
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const x = ref(0)
const y = ref(0)
const update = e => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>
<template>
<slot :x="x" :y="y"/>
</template>
// 1 - composable
import { ref, computed, watchEffect } from 'vue';
import useSomeOtherComposable from './useSomeOtherComposable';
export default function useMyComposable() {
const { asyncValue1, asyncValue2 } = useSomeOtherComposable();
const resolved = ref(false);
// Hodnoty, které vrátíme, až budou splněny podmínky
const computedValue1 = ref(null);
const computedValue2 = ref(null);
// Proměnná promise, která se splní, když budou data připravena
const dataReady = new Promise((resolve) => {
watchEffect(() => {
if (asyncValue1.value === 'desiredValue1' && asyncValue2.value === 'desiredValue2') {
computedValue1.value = asyncValue1.value;
computedValue2.value = asyncValue2.value;
resolved.value = true;
resolve();
}
});
});
/*
// i watchEffect lze zastavit
const dataReady = new Promise((resolve) => {
const stopEffect = watchEffect(() => {
if (asyncValue1.value === 'desiredValue1' && asyncValue2.value === 'desiredValue2') {
computedValue1.value = asyncValue1.value;
computedValue2.value = asyncValue2.value;
resolve(); // Vyřeší Promise, když jsou hodnoty požadované
stopEffect(); // Zastaví watchEffect, protože už není potřeba
}
});
});
*/
// Vrátíme computed hodnoty i promise
return {
computedValue1,
computedValue2,
dataReady,
resolved
};
}
// usage - component
import { defineComponent, onMounted } from 'vue';
import useMyComposable from './useMyComposable'; // another composable which returns computed
export default defineComponent({
setup() {
const { computedValue1, computedValue2, dataReady } = useMyComposable();
onMounted(async () => {
await dataReady; // Počkáme, až budou hodnoty připravené
console.log('Values are ready:', computedValue1.value, computedValue2.value);
});
return {
computedValue1,
computedValue2
};
}
});
// 2 - watch insteadof watchEffect
import { ref, watch } from 'vue';
import useSomeOtherComposable from './useSomeOtherComposable';
export default function useMyComposable() {
const { asyncValue1, asyncValue2 } = useSomeOtherComposable();
const computedValue1 = ref(null);
const computedValue2 = ref(null);
const dataReady = new Promise((resolve) => {
const stopWatching = watch(
[asyncValue1, asyncValue2],
([newVal1, newVal2]) => {
if (newVal1 === 'desiredValue1' && newVal2 === 'desiredValue2') {
computedValue1.value = newVal1;
computedValue2.value = newVal2;
resolve(); // Vyřeší Promise, jakmile hodnoty odpovídají požadavkům
stopWatching(); // Zastaví sledování, protože už není potřeba
}
},
{ immediate: true } // Sleduj hned od začátku
);
});
return {
computedValue1,
computedValue2,
dataReady
};
}
// component usage
import { defineComponent, onMounted } from 'vue';
import useMyComposable from './useMyComposable';
export default defineComponent({
setup() {
const { computedValue1, computedValue2, dataReady } = useMyComposable();
onMounted(async () => {
await dataReady; // Počkáme, až budou hodnoty připravené
console.log('Values are ready:', computedValue1.value, computedValue2.value);
});
return {
computedValue1,
computedValue2
};
}
});
// ---------------------------------------------------
// geolocation checker aka MSP
// ---------------------------------------------------
import { ref, computed } from 'vue';
import { useMyComposable } from './path/to/composable';
export default {
setup() {
// Definuj proměnné na úrovni komponenty
const computedValue1 = ref(null);
const computedValue2 = ref(null);
const dataReady = ref(false);
const check = () => {
const { computedValue1: cv1, computedValue2: cv2, dataReady: dr } = useMyComposable();
// Přiřaď hodnoty z composable do definovaných proměnných
computedValue1.value = cv1.value;
computedValue2.value = cv2.value;
dataReady.value = dr.value;
};
// Spusť funkci nebo ji použij, když potřebuješ načíst data
check();
// Můžeš nyní přistupovat k computedValue1 a computedValue2 přímo mimo `check`
console.log(computedValue1.value, computedValue2.value);
return {
computedValue1,
computedValue2,
dataReady,
check,
};
},
};
<!--
Url: https://adamwathan.me/renderless-components-in-vuejs/
Jeho pristup je predavani atributu a eventu jako cely objekt, moje je :bookmarkNow a prijde mi to lepsi teda ... ale ukladam.
-->
<script setup>
const props = defineProps({
links: {
type: Array,
required: true
}
})
const bookmark = (link) => {
link.bookmarked = !link.bookmarked;
}
</script>
<template>
<slot
v-for="(link, index) in links"
:key="link.id"
:link="link"
:bookmarkNow="() => bookmark(link)"
:bookmarkButtonAttrs="{
style: link.bookmarked ? 'font-weight: bold;' : ''
}"
:bookmarkButtonEvents="{
click: () => bookmark(link)
}"
/>
</template>
<script setup>
import { shallowRef, watchEffect, triggerRef } from 'vue'
const user = shallowRef({
name: 'John',
job: 'BFU',
age: 90
})
// Log the user whenever it changes
watchEffect(() => {
console.log('LOG: watchEffect', user)
});
// Update nested state (no log happens)
setTimeout(() => {
user.value.name = 'Martin';
console.log('LOG: user name changed')
}, 2000)
// Force a reactive update to trigger
setTimeout(() => {
triggerRef(user);
console.log('LOG: whole object changed')
}, 4000)
// [user object]
</script>
<