3

I have this Vue3 component using composition API:

<script setup lang="ts">
import { reactive } from "vue";
import draggable from "vuedraggable"
const myArray = reactive([
    { name : "AAA", id : 1},
    { name : "BBB", id : 2},
    { name : "CCC", id : 3},
    { name : "DDD", id : 4}
])
</script>

<template>
  <draggable
    :list="myArray"
    @end="endDrag"
    item-key="id">
    <template #item="{element}">
        <div>{{element.name}}</div>
    </template>
  </draggable>
</template>

This works and in endDrag routine I see myArray rearranged. But if I change :list to v-model the list on screen does not change and the dragged element returns to its original place. Not to say that myArray is not changed.

Bug or feature? I'm doing anything wrong? Thanks for enlightening me!

SiliconValley
  • 1,465
  • 17
  • 30
  • Hey, did you resolve the problem? I have pretty the same issue. When I have `myArray = ref([])` then v-model works well and everyting sort corectly. But when I set `myArray` as `reactive` it loses reactivity - I mean when I `console.log` the `myArray` then I have initial value of array. – Dafik May 08 '23 at 13:34
  • It doesn't make sense. ref should be for array/objects and ref for simple values – Dafik May 08 '23 at 13:35
  • Well, now it works with `v-model` with a computed variable with get/set accessors. I'm using `"vuedraggable": "^4.1.0"` – SiliconValley May 09 '23 at 14:24
  • I don't know why was not working before. Seems that using `ref()` and not `reactive()` solves the problem. Sincerely, now it works and do now want to investigate further. – SiliconValley May 09 '23 at 14:29

1 Answers1

1

I just came across the same problem. Let me share with you my understanding.

First let's establish a few facts.

As it's widely known v-model is basically this:

<Component :modelValue="myObject" @update:modelValue="(newObject) => myObject = newObject/>

If you use the script setup, the const declared and other declarations become a property of an object.

So this:

const myObject = reactive({
   id: 'App',
});

Becomes this:

export default {
  setup() {
    const myObject = reactive({
      id: 'App',
    });

    return {
      myObject,
    };
  },
}

Now let's put this knowledge together. When child component emits the event update:modelValue with new object, what Vue does is an assignment to the object returned from setup. In pseudo code: setupObject.myObject = newObjectFromEvent

You can prove it by this simple code:

<script>
import { isProxy, reactive } from 'vue';
import Test from './Test';

export default {
  components: {
    Test
  },
  setup() {
    const myObject = reactive({
      id: 'App',
    });

    const setupObject = {
      myObject,
    };

    setTimeout(() => {
      console.log(setupObject.myObject);
      console.log(isProxy(setupObject.myObject));
    }, 5000);

    return setupObject;
  },
}
</script>

<template>
  <Test v-model="myObject"/>
</template>


<script setup>
import { defineProps, isRef, watch } from 'vue';

const props = defineProps(['modelValue']);

watch(props.modelValue, () => {
  console.log('Value changed!');
});

</script>
<template>
  <button @click="$emit('update:modelValue', { id: 'Test' })">Click me</button>
</template>

What you will observe after clicking the button is that our console.log will output: {id: 'Test'} and false.

Also the watcher from the Test component wouldn't be triggered, cause our property is no longer reactive and our Test component still holds the old value of myObject.

The exact same thing happens with the draggable v-model. When you drag and drop the item, the property (in your case myArray) that becomes part of a larger object created upon completing the setup code is overridden with the non-reactive object emitted from draggable component, thus you don't see item position changed.

Why it works with ref you might ask? Well, going back to our @update:modelValue="(newObject) => myObject = newObject if you use ref, the Proxy doesn't vanish, because the assignment is done to the myObject.value property. Thus when you used ref it has started working.

Other way to make it work with reactive is to handle the update manually:

const handleUpdate = (newObject) => {
    Object.assign(myObject, newObject);
};

<Test :modelValue="myObject" @update:modelValue="handleUpdate"/>

This way you wouldn't loose the reactivity, cause you keep the same Proxy and just reassign properties.

The bottom line is you have to be very cautious when using v-model with reactive objects or arrays.

Kamil Latosinski
  • 756
  • 5
  • 28