Select in component with customized v-model changes its value after options adding

Hello!
I have a select with options being filled from an API and I want to move in to a separate component. I did it according to Vue’s guides for using v-model on components and for custom events.
But in my new component select changes its value to the first option’s value when I add options.

What I have (works fine):

  <select v-model="itemId">
    <option v-for="item in items"
            :value="item.id">
      {{ item.name }}
    </option>
  </select>

jsfiddle example

What I’m trying to do:

<item-select v-model.number="itemId"></item-select>
Vue.component('item-select', {
  model: {
    prop: 'value',
    event: 'change',
  },
  props: {
    value: Number,
  },
  data: function () {
    return {
      items: [],
    }
  },
  mounted: function () {
    // getting items
  },
  template: `
      <select
          :value="value"
          @change="$emit('change', $event.target.value)">
        <option v-for="item in items"
            :value="item.id">
          {{ item.name }}
        </option>
      </select>
  `
})

jsfiddle example

My browser: Firefox 77.0b8

Try to add a dummy empty option like this

<select :value="value" @change="$emit('change', $event.target.value)">
  <option value="" hidden selected>Select an item</option>
  <option v-for="item in items" :value="item.id">{{item.name}}</option>
</select>

Two other things
1)

<item-select v-model.number="itemId"></item-select>

Unless you are using vue 3 with the new v-model api what you meant to use was

<item-select :value="itemId"></item-select>

(v-model is just syntax sugar for binding and updating value)

<option v-for="item in items" :value="item.id">
{{ item.name }}
</option>

When you loop on lists of items it is 99% better to add a key to avoid weird issues when the dom is updated, so like this

<option v-for="(item, key) in items" :key="key" :value="item.id">
{{ item.name }}
</option>
1 Like

Thanks, it seems like that might help to avoid the problem. But I’d like to know, what’s the reason of such behavior.

No, I do want a two-way binding.

My suspect is that because you are changing the available options when you receive them from your API you are triggering a change event that updates your value with a new selected value (the first item of your list).

You are doing a two way binding also with :value

<tag v-model="something">

Basically is syntax sugar for

<tag
   v-bind:value="something"
   v-on:input="something = $event.target.value"
>

In your case it is correct to use both, but v-model will also attach an event listener that you don’t explicitely use, so maybe to avoid an ipothetic side effect it is better to use a plain :value.

No. I checked.

I probably found an answer:

If nodes are inserted or nodes are removed causing the list of options to gain or lose one or more option elements, or if an option element in the list of options asks for a reset, then, if the select element’s multiple attribute is absent, the user agent must run the first applicable set of steps from the following list:

If the select element’s display size is 1, and no option elements in the select element’s list of options have their selectedness set to true

Set the selectedness of the first option element in the list of options in tree order that is not disabled, if any, to true.

But then again, why it worked not like that in the first variant?

This helped me. I added :key="items.length" to the select:

<select
    :key="items.length"
    :value="value"
    @change="$emit('change', $event.target.value)">
  <option v-for="item in items" :value="item.id">{{item.name}}</option>
</select>