N
Nuxt3mo ago
Atsu

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 :
<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>
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!
1 Reply
Atsu
AtsuOP3mo ago
I've got it to work with this:
<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>
It is somewhat clunky but it works.
Want results from more Discord servers?
Add your server