Home > Software design >  Vue 3 custom checkbox components bound to array of selected values
Vue 3 custom checkbox components bound to array of selected values

Time:11-10

I've been trying to create a simple component with a styled checkbox and a corresponding label. The values (strings) of all selected checkboxes should be stored in an array. This works well with plain html checkboxes:


<template>
  <div>
    <div >
      <div>
        <input type="checkbox" value="EVO" v-model="status" /> <label for="EVO">EVO</label>
      </div>
      <div>
        <input type="checkbox" value="Solist" v-model="status" /> <label for="Solist">Solist</label>
      </div>
      <div>
        <input type="checkbox" value="SPL" v-model="status" /> <label for="SPL">SPL</label>
      </div>
    </div>
    <div >{{status}}</div>
  </div>
</template>

<script setup>
  import { ref } from 'vue'
  
  let status = ref([]);
</script>

It results in the following, desired situation:

Expected result with native checkboxes

Now if I replace those checkboxes with my custom checkbox component, I can't get it to work. If I check a box, it's emitted value seems to replace the status array instead of it being added to or removed from it, resulting in the following:

enter image description here

So all checkboxes are checked by default for some reason, when I click on one of them they all get unchecked and the status value goes to false and clicking any of the checkboxes again will check them all and make status true.

Now I get that returning whether the box is checked or not in the emit returns a true or false value, but I don't get how Vue does this with native checkboxes and how to implement this behaviour with my component.

Here's the code of my checkbox component:

<template>
  <div >
    <input
      type="checkbox"
      :id="id ?? null"
      :name="name"
      :value="value"
      :checked="modelValue ?? false"
      
      @input="updateValue"
    />
    {{ label }}
  </div>
</template>

<script setup>
  const props = defineProps({
    id: String,
    label: String,
    name: String,
    value: String,
    errors: Object,
    modelValue: Boolean,
  })
  
  const emit = defineEmits(['update:modelValue'])
  
  const updateValue = function(event) {
    emit('update:modelValue', event.target.checked)
  }
</script>

And the parent component only uses a different template:

<template>
  <div>
    <div >
      <Checkbox v-model="status" value="EVO" label="EVO" name="status"  />
      <Checkbox v-model="status" value="Solist" label="Solist" name="status" />
      <Checkbox v-model="status" value="SPL" label="SPL" name="status" />
    </div>
    <div >{{status}}</div>
  </div>
</template>

I've tried to look at this answer from StevenSiebert, but it uses an object and I want to replicate the original Vue behaviour with native checkboxes.

I've also referred the official Vue docs on v-model, but can't see why this would work different with native checkboxes than with components.

CodePudding user response:

Your v-model is the same for every checkbox, maybe like following snippet:

const { ref } = Vue
const app = Vue.createApp({
  setup() {
    const status = ref([{label: 'EVO', status: false}, {label: 'Solist', status: false}, {label: 'SPL', status: false}])
    return {
      status
    }
  },
})
app.component('Checkbox', {
  template: `
    <div >
      <input
        type="checkbox"
        :id="id ?? null"
        :name="name"
        :value="value"
        :checked="modelValue ?? false"
        
        @input="updateValue"
      />
      {{ label }}
    </div>
  `,
  props:{
    id: String,
    label: String,
    name: String,
    value: String,
    errors: Object,
    modelValue: Boolean,
  },
  setup(props, {emit}) {
    const updateValue = function(event) {
      emit('update:modelValue', event.target.checked)
    }
    return {
      updateValue
    }
  }
})
app.mount('#demo')
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.18/tailwind.min.css" integrity="sha512-JfKMGsgDXi8aKUrNctVLIZO1k1iMC80jsnMBLHIJk8104g/8WTaoYFNXWxFGV859NY6CMshjktRFklrcWJmt3g==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="demo">
  <div>
    <div  v-for="box in status">
      <Checkbox v-model="box.status" :value="box.label" :label="box.label" name="status"></Checkbox>
    </div>
    <div >{{status}}</div>
  </div>
</div>

CodePudding user response:

I can pass an array as the v-model to the Checkbox component and mark is as checked if the value is within that array. When the checkbox gets toggles, I add or remove the value to/from the array depending if it's already in there.

Parent component:

<template>
  <div>
    <div >
      <Checkbox v-for="box in ['EVO', 'Solist', 'SPL']" v-model="status" :value="box" :label="box" name="status" />
    </div>
    <div >{{status}}</div>
  </div>
</template>

<script setup>
  import { ref } from 'vue'
  
  let status = ref([]);
</script>

Checkbox component:

<template>
  <div >
    <input
      type="checkbox"
      :id="id ?? null"
      :name="name"
      :value="value"
      :checked="modelValue.includes(value)"
      
      @input="updateValue"
    />
    {{ label }}
  </div>
</template>

<script setup>
  const props = defineProps({
    id: String,
    label: String,
    name: String,
    value: String,
    errors: Object,
    modelValue: Boolean,
  })

  const emit = defineEmits(['update:modelValue'])

  const updateValue = function(event) {
    const arr = props.modelValue;
    if(arr.includes(props.value)) { // Remove if present
      arr.splice(arr.indexOf(props.value), 1)
    }
    else { // Add if not present
      arr.push(props.value)
    }
    emit('update:modelValue', arr)
  }
</script>

Or to accomodate for booleans as well (like native checkboxes):

<template>
  <!-- ... -->
  <input
    type="checkbox"
    :value="value"
    :checked="isChecked"
    @input="updateValue"
    <!-- ... -->
  />
  <!-- ... -->
</template>

<script setup>
  // ...

  const isChecked = computed(() => {
    if(props.modelValue instanceof Array) {
      return props.modelValue.includes(props.value)
    }
    return props.modelValue;
  })

  const updateValue = function(event) {
    let model = props.modelValue;
    if(model instanceof Array) {
      if(isChecked()) {
        model.splice(model.indexOf(props.value), 1)
      }
      else {
        model.push(props.value)
      }
    }
    else {
      model = !model
    }
    emit('update:modelValue', model)
  }
  • Related