shentao / vue-multiselect

Universal select/multiselect/tagging component for Vue.js
https://vue-multiselect.js.org/
MIT License
6.67k stars 989 forks source link

v-model containing keys only #385

Open callumacrae opened 7 years ago

callumacrae commented 7 years ago

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. My options object contains keys and human-readable values, but it isn't very useful to submit the values back to the server and I'd prefer to do as little processing in the submit handler as possible.

Thanks!

shentao commented 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. :(

callumacrae commented 7 years ago

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.

shentao commented 7 years ago

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.

jackbarham commented 7 years ago

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.

breaktag commented 7 years ago

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>
TisteDup commented 6 years ago

Hi!

Any news about it? I think this is a very common case, it would be great to fix it.

Thanks!

m0ngr31 commented 6 years ago

Needing the same thing.

shentao commented 6 years ago

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)
    }
  }
}
m0ngr31 commented 6 years ago

@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.

jacobg commented 6 years ago

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:

jacobg commented 6 years ago

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>
shentao commented 6 years ago

@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.

jacobg commented 6 years ago

@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!

shentao commented 6 years ago

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.

tarwin commented 5 years ago

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 replacement. I guess it would remove the conversion code for my multi-selects as well.

Any way to get this into v2 as a new option? Would be SUPER appreciated.

tarwin commented 5 years ago

@jacobg I used your suggested wrapper and that is working perfectly for me. Thank you.

(Again, thank you @shentao for the amazing work on!)

tarwin commented 5 years ago

Any update on this v3 thing? Really would LOVE this option ... how do we encourage this?

MatthewBooth commented 5 years ago

@jacobg's solution isn't working for me at all. Returning a lot undefined and single array elements on clicking.

dukhevych commented 5 years ago

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.

jacobg commented 5 years ago

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.

iliyaZelenko commented 5 years ago

You can make item-value prop as in this library, I also saw this in vuetify.

tvavrys commented 5 years ago

👍 We have this problem since day 1.

belardip commented 4 years ago

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

bilalmaqsood commented 4 years ago

@shentao any solution in version 2.1.6??

patie commented 4 years ago

ping

shentao commented 4 years ago
computed: {
  completeValue: {
    get () { 
      return this.value.map(value => this.options.find(option => option.key === value)
    },
    set (v) {
      this.value = v.map(value => value.key)
    }
  }
}
dukhevych commented 4 years ago

@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

arcticlula commented 4 years ago

@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.

pestopancake commented 4 years ago

@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)
    }
  }
}
shinnlu commented 2 years ago

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)
            }
        }
    }
}
shinnlu commented 2 years ago

i just make a jsfiddle sample

fbrisa commented 2 years ago

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"

commercial-hippie commented 3 months ago

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.