vueform / multiselect

Vue 3 multiselect component with single select, multiselect and tagging options (+Tailwind CSS support).
https://vueform.com
MIT License
808 stars 151 forks source link

Label of the selected item is updated incorrectly when using `object` and options change #315

Closed FichteFoll closed 1 year ago

FichteFoll commented 1 year ago

Version

Description

This one took me quite a while to figure out.

Basically, when using a single select with object mode, picking the selected item from the list of options (because they are compared by equality natively) and the options are updated, there is a watcher in useOptions that triggers refreshLabels, which ends up setting the label of the selected item to the first option unconditionally. The result is that the label does not represent the selected value anymore and that a property of the parent is mutated directly (i.e. without a corresponding emit('input', …)).

Demo

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

import Multiselect from '@vueform/multiselect/dist/multiselect.vue2.js';

interface Item { text: string; i: number}

class Option {
    constructor(
        public label: string,
        public value: Item,
        public disabled: boolean,
    ) {}

    static of(item: Item) {
        return new Option(
            `${item.text} - ${item.i}`,
            item,
            false,
        );
    }
}

const items = ref<Array<Item>>([]);

const options = computed(() => items.value.map(Option.of) ?? []);

const nativeValue = { text: 'test2', i: 2 };

const value = computed(() => options.value.find(opt => opt.value.i === nativeValue.i));

const multiselect = ref<InstanceType<typeof Multiselect> | null>(null)

setTimeout(() => {
  items.value = [
    { text: 'test', i: 0 },
    { text: 'test1', i: 1 },
    { text: 'test2', i: 2 },
  ];
}, 400);

</script>

<template>
  <div>
    <p>
      Multiselect update test
    </p>
    <Multiselect
      ref="multiselect"
      :value="value"
      :options="options"
      object
      searchable
    />
    <br>
    <input type="text" />
  </div>
</template>

<style src="@vueform/multiselect/themes/default.css"></style>

2023-02-03_17-33-17

2023-02-03_17-34-31

Workarounds

My current workaround is to define a getter for label only.

I'd also be happy with another solution to select the current item based on a field of the object value, e.g. Item.i.

FichteFoll commented 1 year ago

Can also be reproduced slightly differently if the selected value is a normal ref:

const value = ref({ label: 'tmp', value: nativeValue});

except now the value is not selected in the dropdown (since it's a different object).

2023-02-03_17-43-05

adamberecz commented 1 year ago

You should have:

static of(item: Item) {
  return new Option(
    `${item.text} - ${item.i}`,
    item.i, // <-- note the `.i`
    false,
  );
}

You can also access an option based on value with:

multiselect.value.getOption(2)

So the multiselect value can be set like this:

const value = ref(value)

value.value = multiselect.value.getOption(2)

Hope this helps - feel free to reopen if you still have an issue or questions.

ps: I always appreciate a reproducible example using the link from our issue template (or anything else): https://github.com/vueform/multiselect/issues/new?assignees=&labels=&template=1_bugs.md

FichteFoll commented 1 year ago

Thanks for taking a look. I won't have time to look at this over the next few days but will report back once I have.

FichteFoll commented 1 year ago

First of all, here is a fiddle with the initial report:

https://jsfiddle.net/kp3vyhLn/

I added an additional field extra to the Item interface to clarify that I definitely want the entire object (my real Item has 5 properties) and not just i, the unique identifier.

I also tried getting things to work somewhat without object but I'm even more confused now. https://jsfiddle.net/kp3vyhLn/1/

To be clear, my requirements are as follows:

  1. A list of options that is loaded in the background.
  2. The values of these options are objects with 5 properties (3 in the example).
  3. Each option's label is created dynamically from the properties (but deterministic).
  4. There is a unique property among the 5 properties to uniquely identify a value.

So initially I thought I don't "need" the object mode to achieve this, but I found out after testing (and if my memory serves me right) that I was only able to get it to work with object set.

I suppose, I could flatten the structure to only have a single object with all fields of my option value in addition to label and disabled, which interface with Multiselect. I also tried that, but couldn't get the multiselect to properly select my initial item and don't see the problem right now. https://jsfiddle.net/kp3vyhLn/2/

Any advice is highly appreciated.