primefaces / primevue

Next Generation Vue UI Component Library
https://primevue.org
MIT License
10.53k stars 1.23k forks source link

Select: Selected Label changed with Value on filter #6392

Open vetinary opened 1 month ago

vetinary commented 1 month ago

Describe the bug

I use Select component with both editable and filter properties enabled. When I just choose an existing option from the dropdown area, everything works great until I try to filter options: when I do that, text data in selected area is changed with the value of the selected option.

Reproducer

https://stackblitz.com/edit/vitejs-vite-swaew4?file=src%2FApp.vue

PrimeVue version

4.0.0

Vue version

3.x

Language

ALL

Build / Runtime

Vite

Browser(s)

No response

Steps to reproduce the behavior

  1. Open the test project https://stackblitz.com/edit/vitejs-vite-swaew4?file=src%2FApp.vue
  2. In select choose the option named 'Title 1'
  3. Open the select clicking the arrow and type '2' in filter input field
  4. You see the 'Title 1' in selected value area is changed with 'id1' step1 step2

Expected behavior

Filter actions must not affect the area of the selected value

dzhebrak commented 1 month ago

There have been several other similar issues (both in v3 and v4), but the bug is still not fixed. Temporary solution is customizing the #value slot:

<template #value="slotProps">
  <template v-if="slotProps.value">
    <span style="color: var(--p-select-color)">{{ label }}</span>
  </template>
  <span v-else>
    {{ slotProps.placeholder }}
  </span>
</template>
vetinary commented 1 month ago

@dzhebrak thank you for the suggestion.

It was the first thing I tried to apply to my code before reporting the issue. Unfortunately, template for value is not applicable for editable mode, since the editable mode involves usage of the InputText component, and template for value is ignored in that mode.

CCodam commented 4 weeks ago

[!IMPORTANT] It's because the label and editableInputValue computed properties uses the findSelectedOptionIndex() method, which only searches visibleOptions.

Consider creating a new method findSelectedOptionIndexAll() and have label and editableInputValue use that instead. I haven't done substantial testing, but from the few tests I have done, it doesn't look like it will have any adverse effect.

findSelectedOptionIndex() {
  return this.hasSelectedOption ? this.options.findIndex((option) => this.isValidSelectedOption(option)) : -1;
}

[!TIP] Now, if you want a workaround... https://stackblitz.com/edit/vitejs-vite-bwrqua?file=src%2FApp.vue

First we'll have to ref the component

const comSelect = ref();

<ComponentSelect ref="comSelect" editable filter v-model="selectedId"...>

Then a couple of support functions.

function whoCalledMe() { const stack = new Error().stack.split('\n'); return stack.map(line => { const match = line.match(/at (\S+)/); return match ? match[1] : null; }).filter(name => name); };


Because **editableInputValue** is readonly, being a computed property, we will have to get creative.
- First wait for **comSelect** to be ready, and then override the functions.
- When **editableInputValue** calls **findSelectedOptionIndex()** we will return -2 to trick **editableInputValue** into calling **getOptionLabel**.
- Then when **editableInputValue** calls **getOptionLabel** we will use our own **findSelectedOptionIndexAll** and return the correct option.
```js
waitForComSelect()
  .then(() => {
    comSelect.value.findSelectedOptionIndexAll = function() {
      return this.hasSelectedOption ? this.options.findIndex((option) => this.isValidSelectedOption(option)) : -1;
    };

    comSelect.value.findSelectedOptionIndex = function() {
      const stackFunc = whoCalledMe();

      // If calling function was editableInputValue()
      if (stackFunc.includes('Proxy.editableInputValue')) {
        // If hasSelectedOption return -2 to trick editableInputValue() to call getOptionLabel
        // We will find the actual option when getOptionLabel is called
        return this.hasSelectedOption ? -2 : -1;
      } else {
        return this.hasSelectedOption ? this.visibleOptions.findIndex((option) => this.isValidSelectedOption(option)) : -1;
      }
    };

    comSelect.value.getOptionLabel = function(option) {
      const stackFunc = whoCalledMe();

      // If calling function was editableInputValue()
      if (stackFunc.includes('Proxy.editableInputValue')) {
        // Find the actual option and return it
        const selectedOptionIndex = this.findSelectedOptionIndexAll();
        return selectedOptionIndex !== -1
          ? (this.optionLabel ? resolveFieldData(this.options[selectedOptionIndex], this.optionLabel) : this.options[selectedOptionIndex])
          : this.modelValue || '';
      } else {
        return this.optionLabel ? resolveFieldData(option, this.optionLabel) : option;
      }
    };
  })
  .catch((error) => {
    console.log(error.message);
  });