Attribute :value binding in select tag doesn't update inside Vue 3 component template (Composition API)

Viewed 1377

I have a drop down menu where options are enumerated and shuffled, so that the selected option becomes the first. This script is working as intended:

<div id="main">
  <sub-select :subs="data" @comp-update="onShufflePaths($event)"></sub-select>
</div>

. .

 const ui = {
   setup() {
    
    let data = ref('first_second_thrid');
    
    const onShufflePaths = (ind) => {
        let subs = data.value.match(/[^_]+/g);
        const main = subs.splice(ind, 1)[0];
        data.value = [main, ...subs].join('_');
    }
        
     return {
       data, onShufflePaths, 
     };
   },
 };

 const vueApp = createApp(ui);
 
vueApp.component('sub-select', {
  props: ['subs'],
  emits: ['comp-update'],
  setup(props, { emit }) {
  
        let subs = computed(() => props.subs.match(/[^_]+/g));
    
    let subpath = computed(() => '0: ' + subs.value[0]);
    
    function onChange(evt) {
        emit('comp-update', evt.slice(0,1));                      
    } 
    
    return { subs, subpath, onChange };
  },
  template: `
  <select :value="subpath" @change="onChange($event.target.value)">
    <option v-for="(v,k) in subs">{{k}}: {{v}}</option>
  </select> {{subpath}}`
});
 
 vueApp.mount('#main');

The problem is, if I delete {{subpath}} from the template, the drop down menu comes up with no options selected by default. It looks like :value="subpath" by itself is not enough to update subpath variable when props update, if it's not explicitly mentioned in the template.

How can I make it work?

Basically, I need the first option always to be selected by default.

Thank you!

https://jsfiddle.net/tfoller/uy7k1hvr/26/

3 Answers

So, it looks like it might be a bug in the library.

Solution 1:

wrap select tag in the template in another tag, like this (so it's not the lonely root element in the template):

template: `
  <div><select :value="subpath" @change="onChange($event.target.value)">
    <option v-for="(v,k) in subs">{{k}}: {{v}}</option>
  </select></div>`

Solution 2:

Write a getter/setter to subpath variable, so component definition is as follows:

vueApp.component('sub-select', {
  props: ['subs'],
  emits: ['comp-update'],
  setup(props, { emit }) {
  
        let subs = computed(() => props.subs.match(/[^_]+/g));
    
    let subpath = computed({
      get: () => '0: ' + subs.value[0],
      
      set (value) {
        emit('comp-update', value.slice(0,1))
      }
    });
    
    return { subs, subpath };
  },
  template: `
  <select v-model="subpath">
    <option v-for="(v,k) in subs">{{k}}: {{v}}</option>
  </select>`
});

For having the first option selected by default you need to point to the index 0.

Here your index is the k from v-for

<option v-for="(v,k) in subs" :selected="k === 0">{{k}}: {{v}}</option>

There is no v-model in select, I think that is the main issue. Other is its not clear what you what to do.

please refer the following code and check if it satisfy your need.


    // app.vue
    <template>
      <sub-select v-model="value" :options="options" />
      {{ value }}
    </template>
    
    <script>
    import { ref } from "vue";
    import subSelect from "./components/subSelect.vue";
    
    export default {
      name: "App",
      components: {
        subSelect,
      },
      setup() {
        const value = ref(null);
        const options = ref(["one", "two", "three"]);
    
        return { value, options };
      },
    };
    </script>

see that I have used v-model to bind the value to sub-select component.

the sub-select component as follows

// subSelect.vue
<template>
  <select v-model="compValue">
    <template v-for="(option, index) in compOptions" :key="index">
      <option :value="option">{{ index }}: {{ option }}</option>
    </template>
  </select>
</template>

<script>
import { computed } from "vue";

export default {
  name: "subSelect",
  props: ["modelValue", "options"],
  setup(props, { emit }) {
    // if value is null then update it to be first option.
    if (props.modelValue === null) {
      emit("update:modelValue", props.options[0]);
    }

    const compValue = computed({
      get: () => props.modelValue,
      set: (v) => emit("update:modelValue", v),
    });
   
    // return selected option first in list/Array.
    const compOptions = computed(() => {
      const selected = props.options.filter((o) => o === compValue.value);
      const notSelected = props.options.filter((o) => o !== compValue.value);
      return [...selected, ...notSelected];
    });

    return { compValue, compOptions };
  },
};
</script>

in sub-select component i am checking first if modelValue is null and if so set value to be first option.

and also providing compOptions in such sequence that selected options will always be first in list of selection options.

so it satisfies

  1. The first option always to be selected by default.
  2. Selected option will always be first in list of options.

check the code working at codesandbox

edit

jsfiddle as per request


also i suspect that you need options as underscore separated string for that please refer String.prototype.split() for converting it to array and Array.prototype.join() for joining array back to string. if this is the case please comment so I can update my answer. It should be possible by setting watcher on compOptions and emitting separate event to parent, but I don't think its a good idea!

Related