Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple on Listbox or Combobox doesn't work if v-model is separate from dropdown options #1766

Closed
juresrpcic opened this issue Aug 13, 2022 · 2 comments

Comments

@juresrpcic
Copy link

What package within Headless UI are you using?

"@headlessui/vue"

What version of that package are you using?

"^1.6.7"

What browser are you using?

Chrome 104

Reproduction URL

https://stackblitz.com/edit/headless-ui-listbox-multiselect-with-pinia?file=src%2Fcomponents%2FListbox.vue

Describe your issue

I need to use a multiselect with a Pinia store. Initially I thought it was a store issue, but have since discovered that v-model binding breaks (or at least half-breaks - initial values are not shown and tracked wrong after initialization) when using a separate array for the v-model.

Quick example - this works (default Headless UI example):

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
];
const selectedPeople = $ref([
  people[0],people[3]
]);

But if selectedPeople is defined separately, using the same objects of course, it breaks:

// this doesn't work
const selectedPeople = $ref([
  { id: 2, name: 'Kenton Towne' },
  { id: 5, name: 'Katelyn Rohan' },
]);

In my app, I'm using a store getter to provide the dropdown options (ie. people in top example), but I manged to get the error even with the minimum reproduction above. I've built a Stackblitz with a full example, both using a Pinia store and a regular component.

I even tried separating the modelValue and @update:modelValue to use my own model updating function, but the value (val in code example below) returned is already wrong - so the issue is caused somewhere before it gets to v-model.

<Listbox
  :options="myStore.getDropdown"
  :modelValue="myStore.model"
  @update:modelValue="(val) => myOwnModelUpdateHandler(val)"
></Listbox>

Is my use case just unsupported or am I missing something?

When using a regular (non-multiselect) Listbox using the same approach (store getter for options, separate array of objects for model), everything works.

Thanks.

@thecrypticace
Copy link
Contributor

This happens because the objects are different instances and currently comparison relies on object identity. The only reason the non-multiple version appears to work is because the value is overwritten. There is little comparison of the object itself to the current "list of values" because there's only one.

I would recommend that you:

  1. Always use the same object instance. Not doing this can (and will almost always) cause hard-to-debug reactivity issues in Vue. This is because, even though the object "look" the same, they are distinct. As such a change in one object won't affect the other and vice-versa and tracking one object doesn't track the other.
  2. Track selection by IDs instead. In JS primitives like strings and numbers compare equal (generally) when they "look" the same. This means that tracking by a string or numeric ID allows you to not worry about object identity checks. You can do this currently by using IDs for the listbox option values and using computed properties to turn that list of IDs back into a list of objects.

Alternatively, in our insiders build, we have a by prop which lets you customize how comparison is performed between objects by using your own function or a string (indicating property access).

You can look here for details on the by prop, how things work currently, and how they'll be able to work in the future when it's available: #1482 — The example there is in React but it's the same situation with Vue.

@juresrpcic
Copy link
Author

Thanks for that explanation. In my case, I needed to use comparisons due to some other constraints, but I agree that using the same object instance is best when possible.

The by prop is amazing though and I wasn't aware it was already available in insiders. This solves my issue - thanks so much for making me aware of it.

If anyone else comes across this, this is where by solved my use case by comparing values (works fine on primitives, but be aware of downsides that @thecrypticace mentions above)

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
];

// this now works using by
const selectedPeople = $ref([
  { id: 2, name: 'Kenton Towne', foo: 'bar' },
  { id: 5, name: 'Katelyn Rohan' },
]);

The Listbox will work comparing either on id or name.

<Listbox by="id" v-model="selectedPeople" multiple>
<Listbox by="name" v-model="selectedPeople" multiple>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants