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.

Sign up to request clarification or add additional context in comments.

2 Comments

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
@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!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.