0

I am attempting to make use of the dataset attributes of elements, to enable data to be transferred from one item to another during a drag/drop process. But I am having some odd results depending on how Vues' v-if condition is used. The code below works as expected: if you click the 'move' button, then look at the underlying elements' datasets using the 'inspect' button, the dataset for the second item changes.....

<script setup>
  import {  onMounted,ref,reactive } from 'vue'
  const thisData = reactive(['AAA','BBB','CCC','DDD','EEE'])

  var items = [];
  var moves = 0;

  onMounted(() => {
    items= document.querySelectorAll('.swappable') ;
    inspect();

  });
  function inspect() {
    for (let n=0; n < 5; n++)  {
      let data = items[n].dataset.transfer;
      const elem = document.getElementById(idString(n+100));
      elem.innerText = 'id: ' + items[n].id + ' :' + data ;
    }
  }
  function move() {
      thisData[1] = thisData[0];
      ++moves;
  }

  function localTest(x) {
        return x > 2;
  }
  function globalTest(chip) {
    return moves > 0;
    
  } 
  function idString(n) {
    return 'btn'+n;
  }
  function dataTransfer(n) {
    return 'data stored: '+n;
  }

</script>

<template>
   <template v-for="(item,n) in thisData" :key="n" >
     <button  v-if="localTest(n)"  style="background-color: lightblue;" class="swappable"
        :data-transfer = dataTransfer(item)
        :id=idString(n)
        > {{ item }} 
     </button>
     <button    v-else   style="background-color: lightgreen;" class="swappable"
        :data-transfer = dataTransfer(item)
        :id=idString(n)
        > {{ item}} 
    </button>
     <p :id=idString(n+100) >   </p>
   </template>
     <button  @click="inspect()">Inspect Element Data</button >
    <p></p>
   <button  @click="move()">Copy btn 1 to btn 2</button >
</template>

... but if the v-if test is changed to globalTest(n) instead of localTest(n), the dataset does not change. My app needs to use a test using a 'global' (a reactive one in fact) but I also need to access the changed dataset.

The effect can be seen here, by editing the v-if test.

quilkin
  • 874
  • 11
  • 31
  • I only added `moves` to show the problem, it doesn't actually do anything useful. All the components themselves update correctly; it's the data behind that isn't updating with the 'global' test. – quilkin May 21 '23 at 11:03
  • I don't think there's anything here that is unneccesary! It's already taken me several hours to distil my original code to the minumum needed to show the effect. – quilkin May 21 '23 at 17:36

1 Answers1

1

Inspecting the resulting page using the browser toolbox shows that the elements are updating correctly even when using globalTest.

The problem is that you are using references to detached elements, which obviously don't update. You shouldn't store elements in a simple array when using Vue, as Vue sometimes changes and replaces elements when updating the component, causing the references you stored to become obsolete.

Instead, you should use template refs:

ref is a special attribute, similar to the key attribute discussed in the v-for chapter. It allows us to obtain a direct reference to a specific DOM element or child component instance after it's mounted.

In your case, we can replace var items = [] with var items = ref([]) and all other references to items with items.value, then remove items= document.querySelectorAll('.swappable') ; from onMounted. We should also add ref="items" to the button.

This resolves issue, as now the references are automatically updated when Vue replaces an element.

Here is the fixed code:

<script setup>
  import {  onMounted,ref,reactive } from 'vue'
  const thisData = reactive(['AAA','BBB','CCC','DDD','EEE'])

  var items = ref([]);
  var moves = 0;

  onMounted(() => {
    inspect();
  });
  function inspect() {
    for (let n=0; n < 5; n++)  {
      let data = items.value[n].dataset.transfer;
      const elem = document.getElementById(idString(n+100));
      elem.innerText = 'id: ' + items.value[n].id + ' :' + data ;
    }
  }
  function move() {
      thisData[1] = thisData[0];
      ++moves;
  }

  function localTest(x) {
        return x > 2;
  }
  function globalTest(chip) {
    return moves > 0;
    
  } 
  function idString(n) {
    return 'btn'+n;
  }
  function dataTransfer(n) {
    return 'data stored: '+n;
  }

</script>

<template>
   <template v-for="(item,n) in thisData" :key="n" >
     <button  v-if="globalTest(n)"  style="background-color: lightblue;" class="swappable"
        :data-transfer = dataTransfer(item)
        :id=idString(n)
        ref="items"
        > {{ item }} 
     </button>
     <button    v-else   style="background-color: lightgreen;" class="swappable"
        :data-transfer = dataTransfer(item)
        :id=idString(n)
        ref="items"
        > {{ item}} 
    </button>
     <p :id=idString(n+100) >   </p>
   </template>
     <button  @click="inspect()">Inspect Element Data</button >
    <p></p>
   <button  @click="move()">Copy btn 1 to btn 2</button >
</template>
Michael T
  • 370
  • 4
  • 16
  • As per the code in my question, your answer is a solution so I have marked it as correct. Sadly, though, in the real world my project is far more complex and after much restructuring I still have the same problem. I guess this is one potential pitfall of a 'minimum reproducible example' - I have created a simpler illness with the same symptoms but a different cause :( - I will come back here later if/when I solve it. – quilkin May 23 '23 at 07:50
  • @quilkin I did notice that in your example, when using `localTest` the `v-if` was only triggered on creation, whereas with `globalTest`, the `v-if` was also triggered after clicking "copy". Maybe it's related somehow. Good luck finding the root cause. – Michael T May 23 '23 at 08:20