Open callumacrae opened 7 years ago
Hey Callum! This is a common problem that would require a rewrite of the mechanism behind preselecting the values. I want to focus on multiselect but it’s kinda hard given the work on VueConf. Hopefully, I can fix it in the coming weeks along with other bugs and introduce some enhancements. Sadly I can’t promise anything for now. :(
Another possibly easier way to do it could be to have options
be an array of keys, and have a method lookup for the translated version. It would certainly work for my case, not sure if it would help with other similar cases.
Yup this is how I imagine this. However, this would make it impossible to select a value that is not yet present in the options. That would probably break the async support as it is right now.
I have the same issue. I'm only saving the country code "GB" but want to display the country name "United Kingdom" in the select box.
I came up with a solution via a few questions on SO.
Get the value based on the key: https://stackoverflow.com/questions/44421643/trying-to-get-an-object-value-in-from-a-key-in-javascript
Use that value in the dropdown: https://stackoverflow.com/questions/44422783/get-the-value-of-a-computed-property-in-vue-js-and-use-it-in-a-data-property
Hope this helps someone else.
I have the same issue as well, binding on the id only of the object within my categories. I need to update the product.category when selecting an option. See below:
My Vue instance data:
categories: [
{ id: 45, value: "Drinks"},
{ id: 43, value: "Food"},
{ id: 48, value: "Other"}
],
product: {
title: "Coca Cola",
category: 45
}
Multiselect component
<multiselect
v-model="product.category"
:options="categories"
placeholder="Select a category">
</multiselect>
Hi!
Any news about it? I think this is a very common case, it would be great to fix it.
Thanks!
Needing the same thing.
You can build a computed property where the getter would take the key values and return full obejcts. The setter would map the objects to keys only.
If you need to abstract the behaviour you can create a wrapper component that does just that. And again – this is not a bug, it’s a feature that makes several other behaviours possible (like async options).
See example.
computed: {
completeValue: {
get () {
return this.value.map(value => this.options.find(option => option.key === value)
},
set (v) {
this.value = v.map(value => value.key)
}
}
}
@shentao Yes, I've had to do something similar to that, but in one of my use cases, I have a dynamic list of these, so I can't have a computed property.
shentao commented on Jun 5, 2017 Yup this is how I imagine this. However, this would make it impossible to select a value that is not yet present in the options. That would probably break the async support as it is right now.
Could the component hold onto a value not present, and later when there is a matching option, then it would turn into present? It might be useful to have customization hooks to control:
Here is an SFC wrapper below called KeyMultiselect I wrote to support this scenario. I updated this post on March 1, 2019, in case anyone wants the updated version since I posted my original solution on Sept 6, 2018.
I also wrote an async version called RemoteMultiselect that you can check out on this gist: https://gist.github.com/jacobg/8503eb18ca3754f1749eb6ce2879c0d2
<template>
<multiselect
v-bind="$attrs"
v-on="listeners"
:value="completeValue"
:options="options"
:track-by="trackBy"
:taggable="taggable"
@tag="addTag"
class="key-multiselect"
>
<!-- Pass on all named slots -->
<slot v-for="slot in Object.keys($slots)" :name="slot" :slot="slot"/>
<!-- Pass on all scoped slots -->
<template v-for="slot in Object.keys($scopedSlots)" :slot="slot" slot-scope="scope">
<slot :name="slot" v-bind="scope"/>
</template>
</multiselect>
</template>
<script>
import Multiselect from 'vue-multiselect'
// See discussion on this issue:
// https://github.com/shentao/vue-multiselect/issues/385
export default {
name: 'KeyMultiselect',
inheritAttrs: false,
components: {
Multiselect
},
props: {
value: [Number, String, Array],
options: Array,
trackBy: String,
taggable: {
type: Boolean,
default: false
}
},
computed: {
completeValue: {
get () {
if (!this.value) return null
if (this.$attrs['multiple']) {
// TODO: handle value not found if taggable
return this.value.map(value => this.findOption(value)).filter(value => value)
} else {
const completeValue = this.findOption(this.value)
if (completeValue === undefined && this.taggable) {
this.addTag(this.value)
}
return completeValue
}
},
set (v) {
this.$emit('input', this.$attrs['multiple']
? v.map(value => value[this.trackBy])
: (v && v[this.trackBy])
)
}
},
listeners () {
return {
...this.$listeners,
input: this.onChange
}
}
},
watch: {
completeValue (value) {
this.$emit('fullValueChange', value)
}
},
methods: {
onChange (value) {
this.completeValue = value
},
findOption (value) {
return this.options.find(option => option[this.trackBy] === value)
},
addTag (value) {
const newOption = {
[this.trackBy]: value,
[this.$attrs.label]: value
}
this.options.push(newOption)
// TODO: if multiple then push
this.completeValue = newOption
}
}
}
</script>
<style lang="scss">
.key-multiselect {
.multiselect__option, .multiselect__single {
max-width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
</style>
@jacobg
Could the component hold onto a value not present, and later when there is a matching option, then it would turn into present? It might be useful to have customization hooks to control:
whether to auto-create a placeholder option until the real options get loaded loading message when there's a pending value whether to clear the pending value when there is no matching option in the subsequent update
This is exactly why I don’t want to implement that feature in the current form – adding the code that would do what you described would make the component even harder to maintain as it is now. I already added too many features that crippled my ability to maintain it efficiently. That’s why I look forward to the v3 rewrite since it should leave those features to the user-land by giving everyone the tools to do it.
@shentao What is this v3 rewrite? ... BTW, this component is fantastic. You may be bothered by "what's in the sausage", but from the app developer and user point of view, it looks great!
It is a new version that I’m working on https://github.com/shentao/vue-multiselect/tree/v3 that aims to make the API much more flexible.
The current main component is split into several smaller ones (like MultiselectInput, MultiselectOptions, MultiselectValue).
The logic is kept in a "root" renderless component, meaning it does not have any visual elements.
There also exists a "preconfigured" default component that uses all those small "partials" and the "root" component. The result is a composition of components that mimic the current behavior and look you know from v2. This might be enough for most cases.
However, if you need to introduce some special customizations you can choose to replace some or all of those small "partials" with your own components. Each child of the "root" component is provided with the needed data and methods to drive the "root" component.
This is supposed to make it easier for people to customize the behavior without having to introduce features into the library itself (since as you can see I’m the bottleneck). This should also help with making it much more stable thanks to an additional layer of abstraction. It should also make it possible for other people to create "plugins/partials" for this component or entire compositions by replacing the "default" one.
At least this is the idea. So far I managed to rewrite most of the component except for the options list. Sadly I stumbled upon some issues including a bug in Vue itself related to tunneling slots (https://github.com/vuejs/vue/issues/8546, https://github.com/vuejs/vue/issues/8587) inside scoped-slots. Another issue is making sure that data provided through provide/inject stays reactive which is not the default behavior.
Also thanks for the kind words. I really appreciate it and wish to make it much better. Just need to get through all the current issues.
I'm really looking forward to being able to just use keys
in v-model
as well. At the moment I have a bunch of boilerplate / conversion code that is needed just to use this as a replacement for a normal
I use this for multi-select, but for consistency also want to use it for standard
Any way to get this into v2 as a new option? Would be SUPER appreciated.
@jacobg I used your suggested wrapper and that is working perfectly for me. Thank you.
(Again, thank you @shentao for the amazing work on!)
Any update on this v3 thing? Really would LOVE this option ... how do we encourage this?
@jacobg's solution isn't working for me at all. Returning a lot undefined and single array elements on clicking.
Why not create some prop named 'useFullModel', which will be by default 'true'. If 'false' > use trackBy field (or 'id') for preselect and emit.
Is it really so difficult to implement this functionality? I'll take a look at sources, but for me this sounds not so impossible, like it was described above.
The only problem I see here is to handle situations, when there is only key in v-model and options are async. In this case we cannot render selected property immediately, because it uses some label/name field from the option object.
Since my original post above with a key based multiselect, I've updated it. Check out my edited post above for the latest version I am using. There's also a link to a gist with an async multiselect component.
You can make item-value
prop as in this library, I also saw this in vuetify.
👍 We have this problem since day 1.
You can build a computed property where the getter would take the key values and return full obejcts. The setter would map the objects to keys only.
If you need to abstract the behaviour you can create a wrapper component that does just that. And again – this is not a bug, it’s a feature that makes several other behaviours possible (like async options).
See example.
computed: { completeValue: { get () { return this.value.map(value => this.options.find(option => option.key === value) }, set (v) { this.value = v.map(value => value.key) } } }
How would this work in a for loop where this.value can change
@shentao any solution in version 2.1.6??
ping
computed: { completeValue: { get () { return this.value.map(value => this.options.find(option => option.key === value) }, set (v) { this.value = v.map(value => value.key) } } }
@shentao yes, I do this in all my projects with vue-multiselect. By the way, with composition api it's much easier now, because we can now create computedValues dynamically.
I would like to add, that this snippet requires additional checking like if (this.value === null) return null
@jacobg solution (KeyMultiselect) solves the problem for me! Tested with both single and multiple value, only thing i changed was the addTag method for my own.
@shentao yes, I do this in all my projects with vue-multiselect. By the way, with composition api it's much easier now, because we can now create computedValues dynamically.
I would like to add, that this snippet requires additional checking like
if (this.value === null) return null
computed: {
completeValue: {
get () {
return this.value
.map(value => this.options.find(option => option.key === value)
.filter(option => option != null)
},
set (v) {
this.value = v.map(value => value.key)
}
}
}
I have the same issue and almost done, now my problem was the setter in computed not working at all.
<multiselect
:value="completeValue"
:options="$store.state.optLists"
track-by="id"
:multiple="true"
label="name">
</multiselect>
export default {
name: 'Campaigns',
components: { Multiselect },
data() {
return {
item: {
id: null,
name:'',
subject:'',
type:'',
status:'',
stats:[],
scheduledAt:'',
testSent:'',
replyTo:'',
toField:'',
recipients:[],
exclusionLists:'',
created_at:'',
updated_at:''
}
}
},
computed: {
completeValue: {
get: function () {
console.log('get')
let self = this
return this.item.recipients
.map(value => this.$store.state.optLists.find(option => option.id === value))
.filter(option => option != null)
},
set: function (v) {
console.log('set') //console never show this message
this.item.recipients = v.map(value => value.id)
}
}
}
}
The trick is that v-model is just a sintax sugar for :value and @input so instead of v-model="completeValue" you may use:
:value="options.find(o => o.id == completeValue.id)" @input="(value) => completeValue.id = value.id"
Another working example for someone using script setup:
const model = defineModel({ default: [] })
const tagsStore = useTagsStore()
const { tags, isLoading } = storeToRefs(tagsStore)
const computedModel = computed({
get: () => {
return model.value.map((id) => {
const tag = tagsStore.getById(id)
return {
id: id,
name: tag?.name || id
}
})
},
set: (value) => {
model.value = value.map((tag) => tag.id)
}
})
<VueMultiselect
v-model="computedModel"
:taggable="true"
:multiple="true"
:options="tags"
:loading="isLoading"
label="name"
track-by="id"
>
</VueMultiselect>
This will display the selected ID in the select list if the tags (from my store) hasn't loaded yet.
Taking the multiple select example from the docs: http://monterail.github.io/vue-multiselect/#sub-multiple-select
Is it possible to have the resulting array contain only the keys, and not the values? E.g.
['Vue.js', 'Sinatra']
instead of the whole objects. Myoptions
object containskey
s and human-readablevalue
s, but it isn't very useful to submit thevalue
s back to the server and I'd prefer to do as little processing in the submit handler as possible.Thanks!