// useFormHandler.js
import { ref } from 'vue'
export function useFormHandler(initialData = {}) {
const formData = ref({ ...initialData })
const errors = ref({})
const validate = () => {
errors.value = {}
Object.keys(formData.value).forEach(key => {
if (!formData.value[key]) errors.value[key] = 'Required'
})
return Object.keys(errors.value).length === 0
}
return { formData, errors, validate }
}
// ======================================= //
// useTheme.js
import { ref, provide, inject } from 'vue'
const ThemeSymbol = Symbol('Theme')
export function provideTheme() {
const theme = ref('light')
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide(ThemeSymbol, { theme, toggleTheme })
}
export function useTheme() {
const themeContext = inject(ThemeSymbol)
if (!themeContext) throw new Error('useTheme() must be used after provideTheme()')
return themeContext
}
<!-- App.vue -->
<script setup>
import { provideTheme } from './composables/useTheme'
provideTheme()
</script>
<!-- Navbar.vue -->
<script setup>
import { useTheme } from '../composables/useTheme'
const { theme, toggleTheme } = useTheme()
</script>
<template>
<button @click="toggleTheme">Toggle to {{ theme === 'light' ? 'dark' : 'light' }}</button>
</template>
// ======================================= //
// useEventBus.js
import mitt from 'mitt'
const emitter = mitt()
export function useEventBus() {
return emitter
}
// useNotification.js
import { useEventBus } from './useEventBus'
const bus = useEventBus()
export function useNotification() {
const notify = (msg) => bus.emit('notify', msg)
const onNotify = (handler) => bus.on('notify', handler)
return { notify, onNotify }
}
// ======================================= //
// useAppState.js
import { ref } from 'vue'
const globalState = ref({ user: null, theme: 'light' })
export function useAppState() {
return globalState
}
// app.js
async function loadMathModule() {
const mathModule = await import("./math.js");
console.log(mathModule.add(5, 3));
}
// Or with .then()
import("./math.js")
.then((mathModule) => {
console.log(mathModule.PI);
})
.catch((error) => {
console.error("Failed to load module:", error);
});
<!-- 1 -->
<component
v-if="transport?.image"
:is="typeof transport.image === 'object' ? 'aw-img' : 'img'"
v-bind="
typeof transport.image === 'object'
? { image: transport.image, size: 'eop-icon', style: 'max-width: 40px' }
: { src: transport.image, size: 'eop-icon', style: 'max-width: 40px' }
"
/>
<!-- 2 h render -->
const transportImage = computed(() => {
if (!props.transport?.image) return null
if (typeof props.transport.image === 'object') {
return h(resolveComponent('aw-img'), {
image: props.transport.image,
size: 'eop-icon',
style: 'max-width: 40px'
})
} else {
return h('img', {
src: props.transport.image,
size: 'eop-icon',
style: 'max-width: 40px'
})
}
})
// https://learnvue.co/articles/v-once-v-memo
<script setup>
import { ref } from 'vue'
const subscribers = ref(4000)
const views = ref(10000)
const likes = ref(3000)
</script>
<template>
<div>
<div v-memo="[subscribers]">
<p>Subscribers: {{ subscribers }}</p>
<p>Views: {{ views }}</p>
<p>Likes: {{ likes }}</p>
</div>
<button @click="subscribers++">Subscribers++</button>
<button @click="views++">Views++</button>
<button @click="likes++">Likes++</button>
<div>
<p>Current state:</p>
<p>Subscribers: {{ subscribers }}</p>
<p>Views: {{ views }}</p>
<p>Likes: {{ likes }}</p>
</div>
</div>
</template>
<!-- Parent -->
<script setup>
provide('transportFree', useSlots().transportfree) // slot existuje i kdyz ho vubec nepouziju v komponente, ve smyslu <slot name="transportfree" />, staci zvenci <template #transportfree>content</template>
</script>
<!-- Any Grand-Grand-Child -->
<script setup>
const transportFree = inject('transportFree') // or with params <component :is="transportFree?.({ someProp: 123 })" />
</script>
<template>
<component :is="transportFree" />
</template>
// composables/useMediaObjects.js
export function useMediaObjects() {
const transformMediaObjects = (productGallery, productVideos) => {
const finalObjects = []
// Photos
if (productGallery) {
finalObjects.push(
...productGallery.map((photo, index) => ({
...photo,
type: 'photo',
rank: index + 1
}))
)
}
// Videos
if (productVideos) {
finalObjects.push(
...productVideos.map((videoUrl, index) => {
const youtubeId = extractYouTubeId(videoUrl)
return {
url: videoUrl,
type: 'video',
rank: finalObjects.length + index + 1,
previewThumb: youtubeId
? `https://img.youtube.com/vi/${youtubeId}/sddefault.jpg`
: null
}
})
)
}
return finalObjects
}
const extractYouTubeId = (url) => {
return url.match(
/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/
)?.[1]
}
return {
transformMediaObjects,
extractYouTubeId
}
}
// Product.vue
<script setup>
import { computed } from 'vue'
import { useMediaObjects } from '@/composables/useMediaObjects'
const props = defineProps(['productGallery', 'product'])
const { transformMediaObjects } = useMediaObjects()
const mediaObjects = computed(() => {
return transformMediaObjects(props.productGallery, props.product?.product_video)
})
</script>
// services/MediaTransformer.js
export class MediaTransformer {
static transform(productGallery, productVideos) {
const finalObjects = []
if (productGallery) {
finalObjects.push(
...productGallery.map((photo, index) => ({
...photo,
type: 'photo',
rank: index + 1
}))
)
}
if (productVideos) {
finalObjects.push(
...productVideos.map((videoUrl, index) => {
const youtubeId = this.extractYouTubeId(videoUrl)
return {
url: videoUrl,
type: 'video',
rank: finalObjects.length + index + 1,
previewThumb: youtubeId
? `https://img.youtube.com/vi/${youtubeId}/sddefault.jpg`
: null
}
})
)
}
return finalObjects
}
static extractYouTubeId(url) {
return url.match(
/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/
)?.[1]
}
static generateThumbnail(youtubeId, quality = 'sddefault') {
return `https://img.youtube.com/vi/${youtubeId}/${quality}.jpg`
}
}
// Product.vue
<script setup>
import { computed } from 'vue'
import { MediaTransformer } from '@/services/MediaTransformer'
const props = defineProps(['productGallery', 'product'])
const mediaObjects = computed(() => {
return MediaTransformer.transform(props.productGallery, props.product?.product_video)
})
</script>
// services/MediaTransformer.js
export class MediaTransformer {
constructor(config = {}) {
this.thumbnailQuality = config.thumbnailQuality || 'sddefault'
this.defaultVideoType = config.defaultVideoType || 'video'
}
transform(productGallery, productVideos) {
const finalObjects = []
if (productGallery) {
finalObjects.push(
...productGallery.map((photo, index) => ({
...photo,
type: 'photo',
rank: index + 1
}))
)
}
if (productVideos) {
finalObjects.push(
...productVideos.map((videoUrl, index) => {
const youtubeId = this.extractYouTubeId(videoUrl)
return {
url: videoUrl,
type: this.defaultVideoType,
rank: finalObjects.length + index + 1,
previewThumb: youtubeId
? `https://img.youtube.com/vi/${youtubeId}/${this.thumbnailQuality}.jpg`
: null
}
})
)
}
return finalObjects
}
extractYouTubeId(url) {
return url.match(
/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/
)?.[1]
}
setConfig(newConfig) {
Object.assign(this, newConfig)
return this
}
}
// Product.vue
<script setup>
import { computed, ref } from 'vue'
import { MediaTransformer } from '@/services/MediaTransformer'
const props = defineProps(['productGallery', 'product'])
const mediaTransformer = ref(new MediaTransformer({ thumbnailQuality: 'hqdefault' }))
const mediaObjects = computed(() => {
return mediaTransformer.value.transform(props.productGallery, props.product?.product_video)
})
</script>
// services/MediaTransformer.js
class MediaTransformer {
constructor(config = {}) {
this.thumbnailQuality = config.thumbnailQuality || 'sddefault'
}
transform(productGallery, productVideos) {
// stejná implementace jako výše...
}
extractYouTubeId(url) {
// stejná implementace...
}
setConfig(newConfig) {
Object.assign(this, newConfig)
return this
}
}
// Export instance
export const mediaTransformer = new MediaTransformer()
// Případně export obou
export { MediaTransformer }
// Product.vue
<script setup>
import { computed } from 'vue'
import { mediaTransformer } from '@/services/MediaTransformer'
const props = defineProps(['productGallery', 'product'])
const mediaObjects = computed(() => {
return mediaTransformer.transform(props.productGallery, props.product?.product_video)
})
// Změna konfigurace kdykoliv:
// mediaTransformer.setConfig({ thumbnailQuality: 'maxresdefault' })
</script>
// services/MediaTransformer.js
class MediaTransformer {
static _instance = null
constructor(config = {}) {
if (MediaTransformer._instance) {
return MediaTransformer._instance
}
this.thumbnailQuality = config.thumbnailQuality || 'sddefault'
MediaTransformer._instance = this
}
static getInstance(config = {}) {
if (!this._instance) {
this._instance = new MediaTransformer(config)
}
return this._instance
}
transform(productGallery, productVideos) {
// implementace...
}
extractYouTubeId(url) {
// implementace...
}
}
export default MediaTransformer
// Product.vue
<script setup>
import { computed } from 'vue'
import MediaTransformer from '@/services/MediaTransformer'
const props = defineProps(['productGallery', 'product'])
const mediaObjects = computed(() => {
const transformer = MediaTransformer.getInstance({ thumbnailQuality: 'hqdefault' })
return transformer.transform(props.productGallery, props.product?.product_video)
})
</script>
class MediaTransformer {
static _instances = new Map()
constructor(config = {}) {
this.thumbnailQuality = config.thumbnailQuality || 'sddefault'
this.config = config
}
static getInstance(config = {}) {
// Vytvoř unikátní klíč z configu
const key = JSON.stringify(config)
if (!this._instances.has(key)) {
this._instances.set(key, new MediaTransformer(config))
}
return this._instances.get(key)
}
transform(productGallery, productVideos) {
// implementace...
}
}
const hq = MediaTransformer.getInstance({ thumbnailQuality: 'hqdefault' })
const max = MediaTransformer.getInstance({ thumbnailQuality: 'maxresdefault' })
// Různé instance ✅
class MediaTransformer {
constructor(config = {}) {
this.thumbnailQuality = config.thumbnailQuality || 'sddefault'
}
transform(productGallery, productVideos) {
// implementace...
}
static transform(productGallery, productVideos, config = {}) {
const instance = new MediaTransformer(config)
return instance.transform(productGallery, productVideos)
}
}
export default MediaTransformer
// Varianta 1: Instance
const transformer = new MediaTransformer({ thumbnailQuality: 'hqdefault' })
const media = transformer.transform(gallery, videos)
// Varianta 2: Statická metoda
const media = MediaTransformer.transform(gallery, videos, { thumbnailQuality: 'hqdefault' })
// services/MediaTransformer.js
class MediaTransformer {
constructor(config = {}) {
this.thumbnailQuality = config.thumbnailQuality || 'sddefault'
}
transform(productGallery, productVideos) {
// implementace...
}
extractYouTubeId(url) {
// implementace...
}
}
export const createMediaTransformer = (config = {}) => {
return new MediaTransformer(config)
}
// Export i třídy pro pokročilé použití
export { MediaTransformer }
// Product.vue
<script setup>
import { computed, ref } from 'vue'
import { createMediaTransformer } from '@/services/MediaTransformer'
const props = defineProps(['productGallery', 'product'])
const mediaTransformer = ref(createMediaTransformer({ thumbnailQuality: 'maxresdefault' }))
const mediaObjects = computed(() => {
return mediaTransformer.value.transform(props.productGallery, props.product?.product_video)
})
</script>
// services/mediaService.js
export const mediaService = {
defaultConfig: {
thumbnailQuality: 'sddefault'
},
transform(productGallery, productVideos, config = {}) {
const finalConfig = { ...this.defaultConfig, ...config }
const finalObjects = []
if (productGallery) {
finalObjects.push(
...productGallery.map((photo, index) => ({
...photo,
type: 'photo',
rank: index + 1
}))
)
}
if (productVideos) {
finalObjects.push(
...productVideos.map((videoUrl, index) => {
const youtubeId = this.extractYouTubeId(videoUrl)
return {
url: videoUrl,
type: 'video',
rank: finalObjects.length + index + 1,
previewThumb: youtubeId
? `https://img.youtube.com/vi/${youtubeId}/${finalConfig.thumbnailQuality}.jpg`
: null
}
})
)
}
return finalObjects
},
extractYouTubeId(url) {
return url.match(
/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/
)?.[1]
}
}
// Product.vue
<script setup>
import { computed } from 'vue'
import { mediaService } from '@/services/mediaService'
const props = defineProps(['productGallery', 'product'])
const mediaObjects = computed(() => {
return mediaService.transform(
props.productGallery,
props.product?.product_video,
{ thumbnailQuality: 'hqdefault' }
)
})
</script>
// utils/mediaTransform.js
export const transformMediaObjects = (productGallery, productVideos, config = {}) => {
const { thumbnailQuality = 'sddefault' } = config
const finalObjects = []
if (productGallery) {
finalObjects.push(
...productGallery.map((photo, index) => ({
...photo,
type: 'photo',
rank: index + 1
}))
)
}
if (productVideos) {
finalObjects.push(
...productVideos.map((videoUrl, index) => {
const youtubeId = extractYouTubeId(videoUrl)
return {
url: videoUrl,
type: 'video',
rank: finalObjects.length + index + 1,
previewThumb: youtubeId
? `https://img.youtube.com/vi/${youtubeId}/${thumbnailQuality}.jpg`
: null
}
})
)
}
return finalObjects
}
export const extractYouTubeId = (url) => {
return url.match(
/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/
)?.[1]
}
// Product.vue
<script setup>
import { computed } from 'vue'
import { transformMediaObjects } from '@/utils/mediaTransform'
const props = defineProps(['productGallery', 'product'])
const mediaObjects = computed(() => {
return transformMediaObjects(
props.productGallery,
props.product?.product_video,
{ thumbnailQuality: 'hqdefault' }
)
})
</script>
| Varianta | Výhody | Nevýhody | Použití |
|---|---|---|---|
| Composable | Vue 3 native, reaktivita, auto-import | Vue specific | Vue 3 projekty |
| Statické metody | Jednoduché, žádná konfigurace | Není flexibilní | Jednoduché transformace |
| Export třídy | Flexibilní konfigurace | Musíš vytvářet instance | Různé konfigurace |
| Export instance | Žádné new, sdílený stav |
Pevná konfigurace | Jednotná konfigurace |
| Singleton | Jedna instance, lazy loading | Složitější pattern | Globální konfigurace |
| Factory | Flexibilní vytváření | Extra abstrakce | Komplexní objekty |
| Plain Object | Jednoduché, framework agnostic | Není OOP | Malé utility |
| Pure Functions | Nejjednodušší, testovatelné | Žádná organizace | Funkcionální přístup |
setConfig// https://play.vuejs.org/#eNqdVE2P2jAQ/SsjXwCJBlXtic2itohDK7W72u7Rl5BMwItjW7bDUiH+e8d2krJbRNXlgOyZN1/Pb3Jkn43J9i2yOctdaYXx4NC3ZsGVaIy2Ho5gsZ5C6/ARGyMLjw9YwwlqqxsYUehogH7XVSE7RzaLt5CbAKVWzkMTLCH69lW68Si6RpMbrvJZ6oM6oIvvQHS7LywqD/nawoyut3//Bl++br3XCj6VUpS7W876ypk2qMYTzhZ3dEgN57OEvhpXSu0wBi7D6UJkqNz9b7suEh9EX5+Js+SYDWOxKfOO2KnFJntyWtE7HLkC4KzUjRES7Z3xgtjjbA7RE3yFlPr5W7R52+K0t5dbLHcX7E/uEGyc3Vt0aPfI2eDzhd2gT+7Vzx94oPPgpK5bSegrzgd0WrahxwT70qqK2j7DxW6/RokItXl0q4NH5fqhQqMBeYp4zkgwyyuj/2n3Q/YxxnF1IhYHtQUln6kGYLkVsoJxBEzeJp//VM0/xVKJPezfua1+JrRwIStJo5SFc2SI8KVWxBINukgq6u75jGJfSijuyZXdfbWr/TKmsrSKBBnXhXQ44apuVRk4hzRxoj0hs30hWyR8eDAg0s/Q3cAX4TF1wnNVYS0Urg4m4I+xyjRFw4nKv9x953/J9ITZOSW9FkxRVaSnOby32NzAuih3G6tJfXMwsm3oUxJQDelFqA7U6YXKxNTs9BvEAbCg
// Parent
<script setup>
import { ref, useTemplateRef } from 'vue'
import Modal from './Modal.vue'
const modalRef = useTemplateRef('modal');
</script>
<template>
Parent <br />
=================== <br />
<button @click="modalRef.open()">Open Modal</button>
<button @click="modalRef.close()">Close Modal</button>
<br /><br /><hr />
<Modal ref="modal" />
</template>
// Child - Modal
<template>
Child (Modal) <br />
=================== <br />
<button @click="open()">Open Modal</button>
<button @click="close()">Close Modal</button>
<div v-show="isOpen" class="ModalContent">Modal Content</div>
</template>
<script setup>
import { ref } from 'vue'
const isOpen = ref(false)
function open() {
isOpen.value = true
}
function close() {
isOpen.value = false
}
defineExpose({ open, close })
</script>
<style>
.ModalContent {
padding: 1rem; background: plum;
margin: 1rem;
}
</style>
const showThumbs = (() => {
// really private ;-)
const state = ref(props.enableThumbs)
return computed({
get: () => state.value,
set: value => {
state.value = value
}
})
})()
/*
-----------------------------------
Nejčastější použití je právě pro:
- Vytvoření privátního scope
- Enkapsulaci dat a logiky
- Vyhnutí se globálnímu scope
- Zachování stavu přes closure
-----------------------------------
* 1. Vytvoření privátního scope
* - proměnné uvnitř neuniknou ven
* - čisté API pro vnější svět
*/
const counter = (() => {
let count = 0 // privátní proměnná
return {
increment() { count++ },
getCount() { return count }
}
})()
/**
* 2. Vyhnutí se globálnímu scope
* - dočasné proměnné zůstanou lokální
* - neznečišťuje globální namespace
*/
(() => {
const temp = 'nějaká dočasná proměnná'
// temp existuje jen uvnitř, neznečišťuje globální scope
})()
/**
* 3. Modulární pattern
* - privátní metody a proměnné
* - veřejné API
*/
const module = (() => {
const privateVar = 'private'
const privateMethod = () => {
console.log(privateVar)
}
return {
publicMethod() {
privateMethod() // má přístup k privátním věcem
}
}
})()
/**
* 4. Closure pro zachování stavu
* - privátní stav
* - reaktivní hodnota
*/
const showThumbs = (() => {
const state = ref(props.enableThumbs) // privátní stav
return computed({
get: () => state.value,
set: (value) => {
state.value = value
}
})
})()
/**
* 5. Vyhnutí se konfliktům
* - lokální aliasy
* - izolace kódu
*/
(() => {
const $ = jQuery // lokální alias pro jQuery
// používání $ uvnitř neovlivní jiné knihovny používající $
})()
/**
* 6. Inicializace
* - nastavení při načtení
* - konfigurace
* - event listenery
*/
(() => {
// nějaká inicializace
const config = {
apiKey: 'xxx',
endpoint: 'https://api.example.com'
}
// nastavení event listenerů
document.addEventListener('DOMContentLoaded', () => {
// inicializace UI
})
// další setup
const init = () => {
// ...
}
init()
})()
// https://www.reddit.com/r/tailwindcss/comments/1icfwbo/here_are_10_tailwind_tricks_shared_by_shadcn_they/
// https://x.com/shadcn/status/1842329158879420864
// 1. Dynamic CSS Variables in Tailwind
<div style={{ "--width": isCollapsed ? "8rem" : "14rem" }} className="w-[--width] transition-all" />
// 2. Data Attribute State Management
<div
data-state={isOpen ? "open" : "closed"}
className="data-[state=open]:bg-blue-500"
/>
// 3. Nested SVG Controll
<div
data-collapsed={isCollapsed}
className="[&[data-collapsed=true]_svg]:rotate-180"
>
<svg>...</svg>
</div>
// 4. Parent-Child Style Inheritance
<section :data-collapsed="isCollapsed"
<div className="[[data-collapsed=true]_&]:rotate-180">
{/* Child inherits rotation when parent has data-collapsed=true */}
</div>
</section>
// 5. Group Data States
<div className="group" data-collapsed={isCollapsed}>
<div className="group-data-[collapsed=true]:rotate-180"/>
</div>
// 6. Data Slots
<div className="data-[slot=action]:*:hover:mr-0">
<div data-slot="action" class="-mr-10">...</div>
</div>
// 7. Peer Element Control
<button className="peer" :data-active="isActive">Menu</button>
<div className="peer-data-[active=true]:bg-blue-500"/>
// 8. Named Group Focus
<div className="group/menu">
<button className="group-focus-within/menu:bg-blue-500"/>
</div>
// 9. Group Has Selectors
<div className="group/menu">
<div className="group-has-[[data-active=true]]/menu:bg-blue-500"/>
</div>
// 10. Variant Props
<button
data-variant={variant}
className="data-[variant=ghost]:border-blue-500"
/>