5

I want to create a custom select component in Vue.js. Since I need specific options styling, I need to create 'select' made of div's etc that looks and acts like a real html select.

Currently I have something like this:

Vue.component('child', {
  template: `<div class="component-container"  @click="showOptions = !showOptions">
        <div class="component__select">
            <span class="component__select--name">Select Fruit</span>
            
            <span class="c-arrow-down" v-if="!showOptions"></span>
            <span class="c-arrow-up" v-if="showOptions"></span>
        </div>
        <ul class="component__select-options" v-if="showOptions" >
            <li class="select--option" v-for="option in options">
                <label> <input type="checkbox" :value="option"/> {{option.name}}</label>
            </li>
        </ul>
    </div>`,

  methods: {
    selectOption(option) {
      this.$emit('option', option)
    }
  },
  data: () => ({
  showOptions: false,
  }),
  props: ['options']
});

var vm = new Vue({
  el: '#app',
  data: () => ({
 options: [
  {id: 0, name: 'Apple'},
  {id: 1, name: 'Banana'},
  {id: 2, name: 'Orange'},
  {id: 2, name: 'Strawberry'},
  ],
  selectedFruit: ''
  }),
})
 .component__select {
   height: 38px;
   background-color: #F5F7FA;
   border: 1px solid #dddddd;
   line-height: 38px;
   display: grid;
   max-width: 500px;
   grid-template-columns: 10fr 1fr;
 }

 .component__select--name {
   font-size: 0.8rem;
   padding: 0 0 0 25px;
   cursor: pointer;
 }

 .c-arrow-down {
   justify-self: end;
 }

 .component__select-options {
   max-height: 180px;
   border: 1px solid #dddddd;
   border-top: none;
   overflow: auto;
   position: absolute;
   z-index: 1500;
   max-width: 500px;
   width: 500px;
   margin: 0;
   padding: 0;
 }

 .select--option {
   height: 35px;
   display: grid;
   align-content: center;
   padding: 0 0 0 25px;
   background-color: #f5f5fa;
   border-bottom: 1px solid #dddddd;
 }

 .select--option:last-child {
   border-bottom: none;
 }

 .select--option:nth-child(2n) {
   background-color: #ffffff;
 }
 
 .select--option input{
   display: none;
 }

 .single-option {
   height: 55px;
   background-color: #2595ec;
   font-size: 0.8rem;
   border: 1px solid red;
 }

 .cust-sel {
   width: 200px;
   height: 38px;
   background-color: #f5f5fa;
   border: 1px solid #dddddd;
 }

 .cust-sel:focus {
   outline-width: 0;
 }
<html>
<head>
  <title>An example</title>
</head>
<body>
  <div id="app">
    <span> This is parent component</span>
    <p>I want to have data from select here:  "{{selectedFruit}}"</p>
    <child :options="options" v-model="selectedFruit"></child>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</body>
</html>

But my problem is now how to return data from child to parent component using v-model on child component.

(I know I could emit data from child component and do:
<custom-select :options="someOptions" @selected="setSelectedOption"/>
but I need it to be reusable and writing more and more methods to retrieve data from every select in parent component is not exactly how it should work I think.)

Also I need to have an entire object returned, not only ID. (that's why i've got :value="option") Any ideas?

Derek Pollard
  • 6,953
  • 6
  • 39
  • 59
ineffable
  • 169
  • 1
  • 5
  • 15

2 Answers2

5

As Vue Guide said:

v-model is essentially syntax sugar for updating data on user input events, plus special care for some edge cases.

The syntax sugar will be like:

the directive=v-model will bind value, then listen input event to make change like v-bind:value="val" v-on:input="val = $event.target.value"

So for your use case, you need to create one prop=value, then emit the selected option with event=input.

Like below demo (bind/emit the whole option object):

Vue.config.productionTip = false
Vue.component('child', {
  template: `<div class="component-container"  @click="showOptions = !showOptions">
        <div class="component__select">
            <span class="component__select--name">{{value ? value.name : 'Select Fruit'}}</span>
            
            <span class="c-arrow-down" v-if="!showOptions"></span>
            <span class="c-arrow-up" v-if="showOptions"></span>
        </div>
        <ul class="component__select-options" v-if="showOptions" >
            <li class="select--option" v-for="option in options" @click="selectOption(option)">
                <label> <input type="checkbox" :value="option"/> {{option.name}}</label>
            </li>
        </ul>
    </div>`,

  methods: {
    selectOption(option) {
      this.$emit('input', option)
    }
  },
  data: () => ({
    showOptions: false
  }),
  props: ['options', 'value']
});

var vm = new Vue({
  el: '#app',
  data: () => ({
    options: [
  {id: 0, name: 'Apple'},
  {id: 1, name: 'Banana'},
  {id: 2, name: 'Orange'},
  {id: 2, name: 'Strawberry'},
  ],
  selectedFruit: ''
  }),
})
 .component__select {
   height: 38px;
   background-color: #F5F7FA;
   border: 1px solid #dddddd;
   line-height: 38px;
   display: grid;
   max-width: 500px;
   grid-template-columns: 10fr 1fr;
 }

 .component__select--name {
   font-size: 0.8rem;
   padding: 0 0 0 25px;
   cursor: pointer;
 }

 .c-arrow-down {
   justify-self: end;
 }

 .component__select-options {
   max-height: 180px;
   border: 1px solid #dddddd;
   border-top: none;
   overflow: auto;
   position: absolute;
   z-index: 1500;
   max-width: 500px;
   width: 500px;
   margin: 0;
   padding: 0;
 }

 .select--option {
   height: 35px;
   display: grid;
   align-content: center;
   padding: 0 0 0 25px;
   background-color: #f5f5fa;
   border-bottom: 1px solid #dddddd;
 }

 .select--option:last-child {
   border-bottom: none;
 }

 .select--option:nth-child(2n) {
   background-color: #ffffff;
 }
 
 .select--option input{
   display: none;
 }

 .single-option {
   height: 55px;
   background-color: #2595ec;
   font-size: 0.8rem;
   border: 1px solid red;
 }

 .cust-sel {
   width: 200px;
   height: 38px;
   background-color: #f5f5fa;
   border: 1px solid #dddddd;
 }

 .cust-sel:focus {
   outline-width: 0;
 }
<script src="https://unpkg.com/vue@2.5.16/dist/vue.js"></script>
<div id="app">
  <span> This is parent component</span>
  <p>I want to have data from select here:  "{{selectedFruit}}"</p>
  <child :options="options" v-model="selectedFruit"></child>
</div>
tony19
  • 125,647
  • 18
  • 229
  • 307
Sphinx
  • 10,519
  • 2
  • 27
  • 45
0

When using v-model on custom component all you need is to declare a prop named 'value' and when you need the component to chance it emit an 'input' event.

Something like this:

<template>
  <form @submit.prevent="$emit('onSearch',val)" class="form-perfil">
    <div class="form-group col-md-12">
      <input v-model="val" @input="$emit('input',val)" 
      placeholder="filtrar resultados" class="form-control">
    </div>
  </form>
</template>
<script>
module.exports = {
  name: "CaixaFiltro",
  props: ["value"],
  data: _ => ({ val: "" }),
  created() {
    this.val = this.value
  }
}
</script>

Then you can use (after register the component) it like this:

<caixa-filtro v-model="textoBusca" @onSearch="listar"></caixa-filtro>

You can find more detailed info there:

Sombriks
  • 3,370
  • 4
  • 34
  • 54