0

I'm trying to toggle references on and off in Vue. This is how I'd like to do it (or something similar) by having a conditional :ref="option.selected? 'foobar': null" on the items:

<script setup lang="ts">
import {ref, useTemplateRef} from "vue";

const options = ref([
  {label: "One"},
  {label: "Two", selected: true},
  {label: "Three"},
]);

const foobar = useTemplateRef('foobar');
</script>

<template>
  <span
    v-for="option in options"
    :key="option.label"
    :ref="option.selected? 'foobar': null"
    @click="option.selected = !option.selected"
  >{{ option.label }}</span>

  Selected elements: {{ foobar }}
</template>

But I can only make it work if I wrap the elements in a <template> and use v-if="option.selected" and v-else on the elements:

...

<template>
  <template
    v-for="option in options"
    :key="option.label"
  >
    <span
      v-if="option.selected"
      :ref="'foobar'"
      @click="option.selected = !option.selected"
    >{{ option.label }}</span>
    <span
      v-else
      @click="option.selected = !option.selected"
    >{{ option.label }}</span>
  </template>

  Selected elements: {{ foobar }}
</template>

Clicking the item toggles it being selected.

In the first code block, once an element gets added to refs.foobar, it never gets removed even if it gets deselected. So if I select all the elements, then deselect them all, they all still show in {{ foobar }}. :ref="option.selected? 'foobar': null" does work to conditionally add an element to refs, but it doesn't remove them.

In the second code block, only those elements that are currently selected end up in refs.foobar, however, it's clumsy because I have to repeat exactly the same element with an if-else structure. It needs to refresh a second time to see the correct number of currently selected elements in {{ foobar }}, otherwise it shows the previous number.

My real world use case has a lot more code than just a simple <span> resulting in a lot of duplication. Other than v-if="option.selected" :ref="'foobar'" and v-else the code for the two blocks is identical.

Can values in refs be set conditionally in a simple way that always gives the correct/expected results?

2 Answers 2

0

useTemplateRef helper being relatively new, I don't know if it can be used this way, or if there is, I don't know the proper syntax. Maybe someone else can add an answer that uses this helper. I can answer this though using the previous style of template refs, which uses plain old ref along with binding the template ref to a function.

<script setup lang="ts">
import { ref } from "vue";

const options = ref([
  {label: "One", },
  {label: "Two", selected: true},
  {label: "Three"}
]);

const foobar = ref([]);
</script>

<template>
  <span
    v-for="(option, index) in options"
    :key="option.label"
    :ref="(el) => { option.selected ? foobar[index] = el : foobar[index] = null }"
    @click="option.selected = !option.selected"
  >{{ option.label }}</span>

  Selected elements: {{ foobar }}
</template>

Playground example

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

2 Comments

The behaviour both with and without useTemplateRef seems identical. I did try it without first, but then thought maybe useTemplateRef adds something special, which it didn't.
With your solution I'd still need to filter out all of the unselected items since they are inserted into the array as nulls. I can already do that with the standard solution. The if-else workaround, as cumbersome as it is, doesn't require filtering the list of items.
0

You can use a directive if the order of selected elements isn't important:

Playground

<script setup lang="ts">
import { ref, reactive } from "vue";

type Option = {
  label: string;
  selected?: boolean;
}

const options = ref<Option[]>([
  {label: "One", },
  {label: "Two", selected: true},
  {label: "Three"}
]);

const foobar = reactive<HTMLSpanElement[]>([]);

const vSelect = (el: HTMLSpanElement, {value} : {value: Option}) => {
  const idx = foobar.indexOf(el);
  value.selected ? idx < 0 && foobar.push(el) : idx >= 0 && foobar.splice(idx, 1);
}

</script>

<template>
  <span
    v-for="(option, index) in options"
    v-select="option"
    :key="option.label"
    @click="option.selected = !option.selected"
  >{{ option.label }} - selected: {{ option.selected }}</span>

  Selected elements: {{ foobar }}
</template>

Comments

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.