1

I have a component that displays rows of data which I want to toggle to show or hide details. This is how this should look: working version

This is done by making the mapping the data to a new array and adding a opened property. Full working code:

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

const props = defineProps({
  data: {
    type: Array,
    required: true,
  },
  dataKey: {
    type: String,
    required: true,
  },
});

const rows = reactive(props.data.map(value => {
  return {
    value,
    opened: false,
  };
}));

function toggleDetails(row) {
  row.opened = !row.opened;
}
</script>

<template>
  <div>
    <template v-for="row in rows" :key="row.value[dataKey]">
      <div>
        <!-- Toggle Details -->
        <a @click.prevent="() => toggleDetails(row)">
          {{ row.value.key }}: {{ row.opened ? 'Hide' : 'Show' }} details
        </a>

        <!-- Details -->
        <div v-if="row.opened" style="border: 1px solid #ccc">
          <div>opened: <pre>{{ row.opened }}</pre></div>
          <div>value: </div>
          <pre>{{ row.value }}</pre>
        </div>
      </div>
    </template>
  </div>
</template>

However, I do not want to make the Array deeply reactive, so i tried working with ref to only make opened reactive:

const rows = props.data.map(value => {
  return {
    value,
    opened: ref(false),
  };
});

function toggleDetails(row) {
  row.opened.value = !row.opened.value;
}

The property opened is now fully reactive, but the toggle doesn't work anymore:

not working version

How can I make this toggle work without making the entire value reactive?

1 Answer 1

1

The problem seems to come from Vue replacing the ref with its value.

When row.opened is a ref initialized as ref(false), a template expression like this:

{{ row.opened ? 'Hide' : 'Show' }}

seems to be interpreted as (literally)

{{ false ? 'Hide' : 'Show' }}

and not as expected as (figuratively):

{{ row.opened.value ? 'Hide' : 'Show' }}

But if I write it as above (with the .value), it works.

Same with the if, it works if I do:

<div v-if="row.opened.value">

It is interesting that the behavior occurs in v-if and ternaries, but not on direct access, i.e. {{ rows[0].opened }} is reactive but {{ rows[0].opened ? "true" : "false" }} is not. This seems to be an issue with Vue's expression parser. There is a similar problem here.

2
  • 1
    Thank you for pointing me in the right direction. I found more information through the link you sent and found this answer: stackoverflow.com/a/73191385/5122964. Thank you Commented Feb 5, 2023 at 16:22
  • @AdriaanMeuris Very interesting. "It is because they want to" - hard to argue with that lol. Still confusing why {{ rows[0].opened }} works, my guess is that it uses toString() of the ref, which itself is reactive. Anyway, I learned something today. Thank you! Commented Feb 5, 2023 at 16:57

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.