Vue-multiselect: Return Value

Created on 15 Feb 2017  ·  44Comments  ·  Source: shentao/vue-multiselect

How can I have a return 'id' instead of a whole object. When I bind it on a data shipper_id I get the whole '{ id: 1, node_name: 'shipper 1' }' instead of just the id

shippers: [
    { id: 1, node_name: 'shipper 1' },
    { id: 2, node_name: 'shipper 2' }
]
enhancement question

Most helpful comment

I’m aware of this as this is the same way I handle pre-populating in my apps. However, I also have cases where I receive a list of selected objects, without having a list of options. Options are fetched only after the user starts searching inside the dropdown. In such cases pre-populating the selected values based only on id wouldn’t work.

I might be able to improve this with a conditional preselection handler that would look for the selected id inside the options and preselect the whole objects based on it. If the key was not found, then it would just use whatever is provided.

As for the return value it could look like this

<multiselect :options="options" return="id" v-model="selectedId"></multiselect>

Wdyt?

All 44 comments

Exactly. I'm trying to figure this out now.

Is this really a problem? The whole object is often required to be able to preselect options (because of the label) etc. And it must work even if the options list doesn’t yet exist to support asynchronous options.

It's not really an issue but in some cases if you are going to bind this data and send ajax request let's say a shipper_id I need to extract the object first just to get the id which is required for the request.

Yes, but that’s actually is quite easy to do. Introducing something like this to multiselect would break some other things. I will think about it at a later time.

Thanks @shentao I was thinking if you can do it on label maybe for the value too. But thanks anyway I will keep on checking for the updates of this package

It is absolutely an issue. I am using data binding to send a request via ajax. If I use a standard input, I can bind to the node "client_id" in my json object that gets posted in the ajax request. It won't work with data.client_id = { label: 'Client Name', value: client_id}. Before posting I have to modify my object by assigning data.client_id = data.client_id.value.

Also with pre-populating data. Normally binding client_id with v-model will preselect the option. Now I have to use the client_id to rebuild the object to match exactly. I have to filter my array of options for the select by client_id and bind that object so it matches the data exactly.

  var userSelectDataSelected = self.userSelectData.filter(user => user.value === buttonData.userid)
  if (userSelectDataSelected.length) {
    self.new_assessment.user_id = userSelectDataSelected[0]
  }

If client_id is the value of the option, I should be able to bind client_id and have the option preselected.

It took quite a bit of extra code to get this working.

I’m aware of this as this is the same way I handle pre-populating in my apps. However, I also have cases where I receive a list of selected objects, without having a list of options. Options are fetched only after the user starts searching inside the dropdown. In such cases pre-populating the selected values based only on id wouldn’t work.

I might be able to improve this with a conditional preselection handler that would look for the selected id inside the options and preselect the whole objects based on it. If the key was not found, then it would just use whatever is provided.

As for the return value it could look like this

<multiselect :options="options" return="id" v-model="selectedId"></multiselect>

Wdyt?

@shentao that looks really good. It could look like a regular select input on html where we can just get the exact wanted value with the benefit of a combo select

Is this suggested enhancement going to make it into vue-multiselect? I would like to use it in a project to replace existing functionality and this would simplify things a lot to make it compatible with selects are currently handled

Probably within a month or two, unless you’re able to provide a PR with this functionality + required tests.

Great thanks for the info. I'll take a look at my workload and when we need to get this feature released by :)

Sure! :)

<multiselect :options="options" return="id" v-model="selectedId"></multiselect>

this code returning object ... in model "selectedId"

aw man, this would be the best feature! I've been using this component for a few projects now. We use this now for Employee Search, which is a name/value pair. We only need to save the employee Id back to our database, so having the ability bind just the value to the model would be ideal. I've been doing this outside of the component but it feels like a hack!

Looks like this post is a month old, any chance this has been implemented?

Maybe you can extend the source like me. I have added a "return" property, incidentally you can import this .vue file directly, without importing css file, and apply i18n:

Select.vue

<script>
import Multiselect from 'vue-multiselect'
function deepClone (obj) {
  if (Array.isArray(obj)) {
    return obj.map(deepClone)
  } else if (obj && typeof obj === 'object') {
    var cloned = {}
    var keys = Object.keys(obj)
    for (var i = 0, l = keys.length; i < l; i++) {
      var key = keys[i]
      cloned[key] = deepClone(obj[key])
    }
    return cloned
  } else {
    return obj
  }
}
export default {
  mixins: [Multiselect],
  data () {
    return {
      internalValue: this.getInternalValue(this.value)
    }
  },
  props: {
    placeholder: {
      type: String,
      default: '请选择'
    },
    tagPlaceholder: {
      type: String,
      default: '按回车创建标签'
    },
    selectLabel: {
      type: String,
      default: '按回车选取'
    },
    selectedLabel: {
      type: String,
      default: '已选取'
    },
    deselectLabel: {
      type: String,
      default: '按回车移除'
    },
    return: {
      type: String,
      default: ''
    }
  },

  methods: {
    /**
     * Converts the external value to the internal value
     * @returns {Array} returns the internal value
     */
    getInternalValue (value) {
      if (value === null || value === undefined) {
        return []
      } else {
        if (this.return) {
          value = this.options.find((option) => String(option[this.return]) === String(value)) || ''
        }
        return this.multiple
          ? deepClone(value)
          : deepClone([value])
      }
    },
    /**
     * Add the given option to the list of selected options
     * or sets the option as the selected option.
     * If option is already selected -> remove it from the results.
     *
     * @param  {Object||String||Integer} option to select/deselect
     * @param  {Boolean} block removing
     */
    select (option, key) {
      /* istanbul ignore else */
      if (this.blockKeys.indexOf(key) !== -1 || this.disabled || option.$isLabel) return
      /* istanbul ignore else */
      if (this.max && this.multiple && this.internalValue.length === this.max) return
      if (option.isTag) {
        this.$emit('tag', option.label, this.id)
        this.search = ''
        if (this.closeOnSelect && !this.multiple) this.deactivate()
      } else {
        const isSelected = this.isSelected(option)
        if (isSelected) {
          if (key !== 'Tab') this.removeElement(option)
          return
        } else if (this.multiple) {
          this.internalValue.push(option)
        } else {
          this.internalValue = [option]
        }
        this.$emit('select', deepClone(option), this.id)
        let value = this.getValue()
        value = this.return ? value[this.return] : value
        this.$emit('input', value, this.id)

        /* istanbul ignore else */
        if (this.clearOnSelect) this.search = ''
      }
      /* istanbul ignore else */
      if (this.closeOnSelect) this.deactivate()
    }
  }
}
</script>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
<style lang="scss" rel="stylesheet/scss">
.multiselect {
  z-index: 5;
}
.tabs .multiselect__content{
  display: block;
}
</style>

Example:

import vSelect from 'components/select'
export default {
  components: {
    vSelect
  },
...
<v-select label="label" trackBy="value" return="value"
                        :options="arrServers" :disabled="!edit.isNew" :maxHeight="140"
                        v-model="item.serverId"
              >
                <span slot="noResult">没有匹配的结果。</span>
              </v-select>

arrServers:

[
  {
    label: 'foo',
    value: 'bar'
  },
...
]

@mirari I like your approach , I have implemented in my project, but sometimes, I don't know why, it returns and error when I try to select but only when I set :multiple="true"

I get

TypeError: this.internalValue.map is not a function

and

Error in render function: "TypeError: this.internalValue.map is not a function"

Without any customization, you can make use of watch to observe when selected options change and extract the values you need. Example:

<multiselect v-model="selectedObjects"
    :options="options"
    :multiple="true" 
    label="name" 
    track-by="id">
</multiselect>
{
  data() {
    return {
      options: [
        {id: 1, name: 'John'},
        {id: 2, name: 'Ana'},
      ],
      selectedObjects: [],
      selectedIds: [],
    }
  },
  watch: {
    selectedObjects(newValues) {
      this.selectedIds = newValues.map(obj => obj.id);
    }
  }
}

Cheers!

The suggested solutions should be sufficient. Closing :)

this is not an acceptable solution.
this way we have to separate the selected options from the original model object and repopulate it using watch. like this:

data: {
   model: <?= json_encode($model) ?>,
   interests: <?= json_encode($model->selectInterestFromDatabase()->mapAll(function($interest){return ['id' => $interes->id, 'value' => $interest->value];})) ?>
},
watch: {
   interests(newValues) {
      this.model.interests = newValues.map(obj => obj.id);
   }
}

this is just a creepy way to do it please just add a future to return just id from selected options

I'm sorry, but there is absolutely no doubt that this is needed as the current behaviour is completely counter-intuitive and impractical in 95% of use-cases. It arbitrarily forces atrocious workarounds for no /good/ reason.

It’s just a matter of one computed property with a getter and setter.

See example:
https://jsfiddle.net/shentao/7jrqcksm/

I understand that it might feel like "95% of use-cases" for you, but I choose not to agree.

This would also require lots of workarounds for making preselection work when options are fetched asynchronously. And from what I’ve seen in action (projects I worked on personally or during my consulting work) usually the cases are either simple enough where "return just ID" is simply emulated by the solution I presented above (computed property with a getter and setter) or they are actually much more complicated, usually involving objects, validations and asynchronous data, including preselecting options that don’t exist in the options list.

I also think it’s pretty understandable that if you pass a list of objects, the selected list will be a list of objects. In other words – you get back the same structure you put in. Want just IDs? Pass a list of IDs only. You can then use scoped slots for building in fancy labels (though that’s probably not the nicest thing to do).

But hey, feel free to submit a PR with an implementation that does just that. Just make sure its backward compatible. Of course, all the features like custom options templates and async support should be kept and be easy to use as they are now.

To sum things up — I’m not saying no to this request. What I’m saying is that it will break a lot of things to implement it in 2.0. I think this should be possible in 3.0 since you should be able to replace parts of the component with your own implementation.

I was wrong. After working a little bit more with this, I realized that the current behaviour is the better default behaviour -- especially when used with slots.

My apologies.

An option to return just the field that is tracked would still be neat. I might implement something like that at some point.

I’m glad we understand each other! 😃

When I bind a selectlist to any variable using v-model="filters.value", i would like to have this value be a string and not an object. Exactly as how a default selectlist would work.

Therefore I think this should be implemented. The watch methods suggested above introduce another variable which is more of a workaround than it is readable code.

Yes, even though I said before that 'i was wrong' .. I ended up needing the actual value (as in ID) and not the object way more often and have written a wrapper for it.

Can we please re-open this? I've gotten around it with extra logic but it makes things a bit ugly. Would be much cleaner to have the "return" prop that was mentioned above.

Hey, I also have an issue with this the return property is really needed, other ways you should do lot's of finds in a component logic, which makes code really ugly.
Only for that it makes sense to wrap multiselect in another component to isolate that logic. But as it have lot's of other properties and events that may be used it is also becoming ugly.
And imaging when you have several multiselects in one component. What to do then?

Have a look at my wrapper as workaround for the time being
https://gist.github.com/doncatnip/0abce58f4735d6d2f0acb2f7d138cf64

@doncatnip it is not a solution. You should update it with new versions.

@mikpetr It is what I am using with the latest vue-multiselect release 2.1.6 and it works fine (needs lodash as _). Take it or leave it.

@shentao any solution with version 2.1.6 ??

thats bad ..
vue-select have a prop for this: https://vue-select.org/guide/values.html#transforming-selections

Yeah I guess we could somehow make it, but certainly not as a default – this would break too much.

with the latest version this work for me

    <multiselect
      v-model="store_front_id"
      label="name"
      :options="storefronts.map(i => i.id)"
      :searchable="false"
    >
      <template slot="singleLabel" slot-scope="props">
        <span class="option__title">{{ getStoreFrontName(props) }}</span>
      </template>
      <template slot="option" slot-scope="props">
        <span class="option__small">{{ getStoreFrontName(props) }}</span>
      </template>
    </multiselect>
    computed: {
        ...mapState('storefront', ['storefronts']),
    },
    methods: {
        getStoreFrontName(props) {
            return this.storefronts.find(i => i.id === props.option).name
        },
    }

There should be props like bootstrap vue.


                  <b-form-select
                    v-model="value"
                    :options="options"
                    :text-field="'name'"
                    :value-field="'id'"
                  ></b-form-select>

This return only id as value.

Hope this module have same feature.

Or like vue-select's "reduce" prop: https://vue-select.org/api/props.html#reduce

had to switch to https://github.com/sagalbot/vue-select because of the lack of this feature.

Any update about this without any 'extra efforts' ?

Any update?

Not yet. The project is currently on hold until I either find some extra time to work on it after I complete the work on other projects or someone else steps up and helps with the maintenance.

Not yet. The project is currently on hold until I either find some extra time to work on it after I complete the work on other projects or someone else steps up and helps with the maintenance.

I can give you $20, if that helps to incentivate you, even if just a little bit.

Vue-Multiselect is currently the best plugin available. I had headaches with a lot of them and this one was the only that did and does its thing - and it does it well!

I appreciate your efforts by a ton!

@Fusseldieb! I appreciate it, but it’s not a matter of money but time. What would help in the short term is someone spending a moment to sort out the current state of the master branch so we could release some minor fixes and not have to worry about it for a while. After that, the work on the future version could be restarted.
If you want you can take a look at the v3 branch. It’s mostly complete except for the accessibility improvements that I started working on (there’s a PR).
Surprisingly, for a small component like this, there are a lot of use cases to be considered.

Would be good to have this issue sorted 👍

But at least one possible solution would be...

(I use collectjs which it is a really nice library)

<template>
    <multiselect
        v-if="user"
        id="timezones"
        :value="timeZone"
        :options="timeZonesOptions"
        :placeholder="$t('select_timezone')"
        @input="setTimezone"
    />
</template>

Then define the object options in

        data () {
            return {
                timeZones: [
                    {
                        "label":"(GMT-10:00) America/Adak (Hawaii-Aleutian Standard Time)",
                        "value":"America/Adak"
                    },
                    {
                        "label":"(GMT-10:00) America/Atka (Hawaii-Aleutian Standard Time)",
                        "value":"America/Atka"
                    }
                ]
            }
        },

```javascript
computed: {
...mapGetters({
user: 'auth/user'
}),
timeZonesOptions () {
return collect(this.timeZones).pluck('label').toArray()
},
timeZone () {
return this.user.timezone ? this.getLabel(this.user.timezone) : null
}
},


Then in methods you could have

```javascript

        methods: {
            getValue (label) {
                let timezone = collect(this.timeZones).where('label', label).first()
                return timezone ? timezone.value : null
            },
            getLabel (value) {
                let timezone = collect(this.timeZones).where('value', value).first()
                return timezone ? timezone.label : null
            },
            setTimezone (label) {
                this.user.timezone = this.getValue(label)
                axios.post('/api/user/timezone', { timezone: this.user.timezone })
            }
        },

Was this page helpful?
0 / 5 - 0 ratings

Related issues

MaxHalford picture MaxHalford  ·  4Comments

katranci picture katranci  ·  3Comments

bushcode picture bushcode  ·  3Comments

dmitov picture dmitov  ·  4Comments

PrimozRome picture PrimozRome  ·  3Comments