Home > front end >  Custom multi-select and single select issue
Custom multi-select and single select issue

Time:01-08

i have created a component which works perfect apart from binding the a string rather than an array. The event's fire correctly.

So on the multi-select version if I select and deselect items the multiSelected variable is updated correctly. However when selecting another value within the single select the singleSelected variable isn't being changed, however the same event is firing.

Dummed down the code here so you can just see the logic and issue:

Vue.component('dropdown', {
  template: `<div >
    <label v-for="item in items" :key="item.value" :>
      {{ item.label }}
      <input type="checkbox" :value="item.value" :checked="selected.indexOf(item.value) > -1" @change="selected = $event" />
    </label>
  </div>`,
  props: [ 'value', 'items', 'multiSelect' ],
  computed: {
    selected: {
      get: function() {
        if (this.value === undefined) {
          return [];
        }
        if (!Array.isArray(this.value)) {
          return [ this.value ];
        }
        return this.value;
      },
      set: function($event) {
        let current = this.selected;

        if (!this.multiSelect) {
          current = $event.target.value;
        }

        if (this.multiSelect && !$event.target.checked) {
          const index = current.indexOf($event.target.value);
          if (index > -1) {
            current.splice(index, 1)
          }
        }
        if (this.multiSelect && $event.target.checked) {
          current.push($event.target.value);
        }

        console.log(current);
        this.$emit('value', current);
      }
    }
  }
});

Vue.component('wrapper', {
  template: `
    <div>
      Single
      <dropdown :items="items" v-model="singleSelected" :multi-select="false" name="single" />
      <br />
      Multi
      <dropdown :items="items" v-model="multiSelected" :multi-select="true" name="multi" />
      <p>Models</p>
      <p>singleSelected: {{ singleSelected }}</p>
      <p>multiSelected: {{ multiSelected }}</p>
    </div>
  `,
  data() {
    return {
      items: [{value:'bmw',label:'BMW',count:1},{value:'audi',label:'Audi',count:1},{value:'kia',label:'KIA',count:1}],
      multiSelected: ['kia'],
      singleSelected: 'kia',
    }
  }
});

new Vue().$mount('#app');
.dropdown {
  border: 1px solid black;
  padding: 10px;
  display: block;
}
label {
  margin: 5px;
}


.selected {
  color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <wrapper>
    
  </wrapper>
</div>

CodePudding user response:

v-model on custom component expects that the component will emit input event, not value

Only reason why your component is (sort of) working in multi-select mode is because you are directly mutating the input array passed to you by a value prop (in other words the emitted event is useless). Don't do that. Create a new array instead before emitting

Also, there is one more issue with single-select mode - after deselecting only checked box, you still emitting the value so the model is set to a value even none of the checkboxes is checked

Personally I find your design strange for several reasons:

  1. Using different v-model "type" (array or string) feels unnatural
  2. In single-select mode it behaves as radio so why not use radio
  3. What will happen if you switch multiSelect prop at runtime?

Vue.component('dropdown', {
  template: `<div >
    <label v-for="item in items" :key="item.value" :>
      {{ item.label }}
      <input type="checkbox" :value="item.value" :checked="selected.indexOf(item.value) > -1" @change="selected = $event" />
    </label>
  </div>`,
  props: [ 'value', 'items', 'multiSelect' ],
  computed: {
    selected: {
      get: function() {
        if (this.value === undefined) {
          return [];
        }
        if (!Array.isArray(this.value)) {
          return [ this.value ];
        }
        return this.value;
      },
      set: function($event) {
        if(this.multiSelect) {
          if (!$event.target.checked) {
            this.$emit('input', this.selected.filter(v => v !== $event.target.value))
          } else {
            this.$emit('input', [...this.selected, $event.target.value])
          }
        } else {
          this.$emit('input', $event.target.checked ? $event.target.value : "")
        }
      }
    }
  }
});

Vue.component('wrapper', {
  template: `
    <div>
      Single
      <dropdown :items="items" v-model="singleSelected" :multi-select="false" name="single" />
      <br />
      Multi
      <dropdown :items="items" v-model="multiSelected" :multi-select="true" name="multi" />
      <p>Models</p>
      <p>singleSelected: {{ singleSelected }}</p>
      <p>multiSelected: {{ multiSelected }}</p>
    </div>
  `,
  data() {
    return {
      items: [{value:'bmw',label:'BMW',count:1},{value:'audi',label:'Audi',count:1},{value:'kia',label:'KIA',count:1}],
      multiSelected: ['kia'],
      singleSelected: 'kia',
    }
  }
});

new Vue().$mount('#app');
.dropdown {
  border: 1px solid black;
  padding: 10px;
  display: block;
}
label {
  margin: 5px;
}


.selected {
  color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <wrapper>
    
  </wrapper>
</div>

CodePudding user response:

You can wait for event and triger method:

Vue.component('dropdown', {
  template: `<div >
    <label v-for="item in items" :key="item.value" :>
      {{ item.label }}
      <input type="checkbox" :value="item.value" :checked="selected.indexOf(item.value) > -1" @change="selected = $event" />
    </label>
  </div>`,
  props: [ 'value', 'items', 'multiSelect' ],
  computed: {
    selected: {
      get: function() {
        if (this.value === undefined) {
          return [];
        }
        if (!Array.isArray(this.value)) {
          return [ this.value ];
        }
        return this.value;
      },
      set: function($event) {
        let current = this.selected;

        if (!this.multiSelect) {
          current = $event.target.value;
        }

        if (this.multiSelect && !$event.target.checked) {
          const index = current.indexOf($event.target.value);
          if (index > -1) {
            current.splice(index, 1)
          }
        }
        if (this.multiSelect && $event.target.checked) {
          current.push($event.target.value);
        }

        console.log(current);
        this.$emit('value', current);
      }
    }
  }
});

Vue.component('wrapper', {
  template: `
    <div>
      Single
      <dropdown :items="items" v-model="singleSelected" :multi-select="false" name="single" @value="updateSingle" />
      <br />
      Multi
      <dropdown :items="items" v-model="multiSelected" :multi-select="true" name="multi"  />
      <p>Models</p>
      <p>singleSelected: {{ singleSelected }}</p>
      <p>multiSelected: {{ multiSelected }}</p>
    </div>
  `,
  data() {
    return {
      items: [{value:'bmw',label:'BMW',count:1},{value:'audi',label:'Audi',count:1},{value:'kia',label:'KIA',count:1}],
      multiSelected: ['kia'],
      singleSelected: 'kia',
    }
  },
  methods: {
    updateSingle(val) {
      this.singleSelected = val
    }
  }
});

new Vue().$mount('#app');
.dropdown {
  border: 1px solid black;
  padding: 10px;
  display: block;
}
label {
  margin: 5px;
}


.selected {
  color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <wrapper>
    
  </wrapper>
</div>

  •  Tags:  
  • Related