vuejs3vueuse

vueuse useDraggable with svg and v-for


I would like to use vueuse's useDraggable with svg components, setting the svg position instead of using the style to move the element.

Thanks to yoduh's comment, the red circle now works, but what is the best way to make multiple elements generated with v-for (the blue circles in the sample code) draggable?

Another way to put the question is, what's the best way to organize the output of useDraggable for multiple elements from a v-for? Is there a way to put the {x,y} output like in the red circle into some kind of larger data structure that I can pass to the template? Or a how would I make the approach I tried in the sample work?

(I'm also not yet sure how to handle the template refs in this case.)

Simple example:

import { useDraggable } from '@vueuse/core'
import { ref, reactive } from 'vue';


const circs = reactive([]);
for (let i = 0; i < 3; i++) {
  const circ = reactive({ x: 10, y: 10, r: 10 });
  circs.push(circ);
}
const circRefs = circs.map(() => ref(null));

const redCirc = reactive({ x: 40, y: 40, r: 10 });
const redCircRef = ref(null);

circs.forEach((circ, index) => {
  const el = circRefs[index];
  useDraggable(el, {
    initialValue: { x: circ.x, y: circ.y },
    onMove: (x, y) => {
      circ.x = x;
      circ.y = y;
    },
  });
});


const {x, y} = useDraggable(redCircRef, {
  initialValue: { x: redCirc.x, y: redCirc.y },
  onMove: (x, y) => {
    redCirc.x = x;
    redCirc.y = y;
  },
});


<svg>
  <circle
      v-for="circ, index in circs"
      :cy="circ.y"
      :cx="circ.x"
      :r="circ.r"
      fill="blue"
      ref="circRefs[index]"
    />
   <circle
      :cy="y"
      :cx="x"
      :r="redCirc.r"
      fill="red"
      ref="redCircRef"
    />
  </svg>

Solution

    1. refs inside v-for are only populated on mount. So you'll need to bind useDraggable to each element in the v-for inside onMounted().

    2. The initial value of a template refs array should be an empty array []. Vue will push the refs on mounting. If you pre-fill the array (e.g: circs.map(() => ref(null));, after the refs are pushed by Vue into the array you'll end up with nulls followed by the template refs and, obviously, the index-es of the template refs won't match.

    Here's how I'd write it:

    <script lang="ts" setup>
    import { useDraggable } from '@vueuse/core'
    import { ref, onMounted } from 'vue'
    
    const circles = ref(
      Array.from({ length: 3 }).map(() => ({
        cx: 20,
        cy: 20,
        r: 10
      }))
    )
    const refs = ref([])
    
    onMounted(() => {
      refs.value.forEach((el, index) => {
        useDraggable(el, {
          onMove: ({ x: cx, y: cy }) => {
            Object.assign(circles.value[index], { cx, cy })
          }
        })
      })
    })
    </script>
    <template>
      <svg
        version="1.1"
        width="400"
        height="320"
        xmlns="http://www.w3.org/2000/svg"
      >
        <circle
          v-for="(circle, key) in circles"
          :key="key"
          v-bind="circle"
          fill="blue"
          ref="refs"
        />
      </svg>
    </template>
    

    Working demo.


    Notes:

    1. In current form, your question is likely to be closed as off-topic, as it asks for opinionated answers, rather than facts. (e.g: "What's the best way to... X"). Avoid formulating your questions like this.
    2. The codesandbox example is taken further than the one included in the answer: