VueUse - useDraggable with dynamic list
I'm trying to achieve a dynamic list of draggable elements inside a container.
I know can easily do this with one element.
Though I have issues getting it to work with a dynamic list.
I have something like this :
First it seems that draggles access at style creates a circular dependency.
Also this seems awfully inconvenient to do it something like this.
The idea is that I would like to have something like in a design tool where users can add new UI elements themselves.
Has anyone worked in such a way with useDraggable?
I'm not even sure if i have the right idea.
Any help would be much appreciated!
<script setup lang="ts">
const container = useTemplateRef < HTMLDivElement > ('containerRef')
type UIFragment = {
name: string,
classes?: string,
}
const fragments = ref <UIFragment[]> ([])
const fragmentRefs = useTemplateRefsList<HTMLDivElement>()
const draggables = computed( () => {
// wait for the next tick to ensure the DOM is updated
return fragmentRefs.value.map((ref) => useDraggable(ref))
})
const addFragment = () => {
fragments.value.push({
name: `Fragment ${fragments.value.length + 1}`
})
}
watch(draggables, async () => {
await nextTick()
console.log([...draggables.value])
}, {
deep: true,
flush: 'post',
})
</script>
<template>
<div class="flex flex-col">
<h1>Home</h1>
<button @click="addFragment">
Add Fragment
</button>
{{ fragments }}
<div ref="containerRef" class="size-80 bg-green-300">
<div v-for="(fragment, index) in fragments" :key="index" class="flex flex-col" :ref="fragmentRefs.set" :style="draggables[index]?.style">
{{ index }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
const container = useTemplateRef < HTMLDivElement > ('containerRef')
type UIFragment = {
name: string,
classes?: string,
}
const fragments = ref <UIFragment[]> ([])
const fragmentRefs = useTemplateRefsList<HTMLDivElement>()
const draggables = computed( () => {
// wait for the next tick to ensure the DOM is updated
return fragmentRefs.value.map((ref) => useDraggable(ref))
})
const addFragment = () => {
fragments.value.push({
name: `Fragment ${fragments.value.length + 1}`
})
}
watch(draggables, async () => {
await nextTick()
console.log([...draggables.value])
}, {
deep: true,
flush: 'post',
})
</script>
<template>
<div class="flex flex-col">
<h1>Home</h1>
<button @click="addFragment">
Add Fragment
</button>
{{ fragments }}
<div ref="containerRef" class="size-80 bg-green-300">
<div v-for="(fragment, index) in fragments" :key="index" class="flex flex-col" :ref="fragmentRefs.set" :style="draggables[index]?.style">
{{ index }}
</div>
</div>
</div>
</template>
1 Reply
I've got it to work with this:
It is somewhat clunky but it works.
<script setup lang="ts" generic="T extends {
id: string
}">
const container = useTemplateRef<HTMLDivElement>('containerRef')
const props = defineProps<{
fragments: T[]
}>()
const refs = computed(() => {
return props.fragments.map((fragment) => ({
fragment: fragment,
ref: ref<HTMLElement>(),
}));
});
type Draggable = {
position: globalThis.Ref<{
x: number;
y: number;
}>;
isDragging: globalThis.ComputedRef<boolean>;
style: globalThis.ComputedRef<string>;
x: globalThis.Ref<number>;
y: globalThis.Ref<number>;
}
const draggables = ref<Record<string, Draggable>>({})
watch(refs, async (newRefs, oldRefs) => {
await nextTick()
const old = oldRefs ?? []
// check all old refs and see if any of them are not in the new refs
const removedRefs = old.filter((oldRef) => !newRefs.includes(oldRef))
const addedRefs = newRefs.filter((newRef) => !old.includes(newRef))
removedRefs.forEach((ref) => {
delete draggables.value[ref.fragment.id]
})
addedRefs.forEach((ref) => {
draggables.value[ref.fragment.id] = useDraggable(ref.ref, {
containerElement: container,
})
})
}, {
immediate: true
})
</script>
<template>
<div ref="containerRef" class="w-full h-full bg-green-300 relative">
<div v-for="(fragment, index) in fragments" :key="fragment.id" class="absolute" :ref="(el) => refs[index]!.ref.value = el as HTMLElement" :style="(draggables[fragment.id]?.style as any as string)">
<slot :fragment="fragment" :drag="draggables[index]!" />
</div>
</div>
</template>
<script setup lang="ts" generic="T extends {
id: string
}">
const container = useTemplateRef<HTMLDivElement>('containerRef')
const props = defineProps<{
fragments: T[]
}>()
const refs = computed(() => {
return props.fragments.map((fragment) => ({
fragment: fragment,
ref: ref<HTMLElement>(),
}));
});
type Draggable = {
position: globalThis.Ref<{
x: number;
y: number;
}>;
isDragging: globalThis.ComputedRef<boolean>;
style: globalThis.ComputedRef<string>;
x: globalThis.Ref<number>;
y: globalThis.Ref<number>;
}
const draggables = ref<Record<string, Draggable>>({})
watch(refs, async (newRefs, oldRefs) => {
await nextTick()
const old = oldRefs ?? []
// check all old refs and see if any of them are not in the new refs
const removedRefs = old.filter((oldRef) => !newRefs.includes(oldRef))
const addedRefs = newRefs.filter((newRef) => !old.includes(newRef))
removedRefs.forEach((ref) => {
delete draggables.value[ref.fragment.id]
})
addedRefs.forEach((ref) => {
draggables.value[ref.fragment.id] = useDraggable(ref.ref, {
containerElement: container,
})
})
}, {
immediate: true
})
</script>
<template>
<div ref="containerRef" class="w-full h-full bg-green-300 relative">
<div v-for="(fragment, index) in fragments" :key="fragment.id" class="absolute" :ref="(el) => refs[index]!.ref.value = el as HTMLElement" :style="(draggables[fragment.id]?.style as any as string)">
<slot :fragment="fragment" :drag="draggables[index]!" />
</div>
</div>
</template>