1

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 class="mt-6">
      <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 class="mt-3">{{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 class="mt-1 relative">
    <input
      type="checkbox"
      :id="id ?? null"
      :name="name"
      :value="value"
      :checked="modelValue ?? false"
      class="bg-gray-200 text-gold-500 rounded border-0 w-5 h-5 mr-2 focus:ring-2 focus:ring-gold-500"
      @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 class="mt-6">
      <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 class="mt-3">{{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.

2 Answers 2

2

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 class="mt-1 relative">
      <input
        type="checkbox"
        :id="id ?? null"
        :name="name"
        :value="value"
        :checked="modelValue ?? false"
        class="bg-gray-200 text-gold-500 rounded border-0 w-5 h-5 mr-2 focus:ring-2 focus:ring-gold-500"
        @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 class="mt-6" v-for="box in status">
      <Checkbox v-model="box.status" :value="box.label" :label="box.label" name="status"></Checkbox>
    </div>
    <div class="mt-3">{{status}}</div>
  </div>
</div>

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for the suggestion, Nikola. It works for my purposes, but it is of course more boilerplate and repetition than how Vue handles native checkboxes with an array instead of an object. Any ideas how that could be achieved?
2

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 class="mt-6">
      <Checkbox v-for="box in ['EVO', 'Solist', 'SPL']" v-model="status" :value="box" :label="box" name="status" />
    </div>
    <div class="mt-3">{{status}}</div>
  </div>
</template>

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

Checkbox component:

<template>
  <div class="mt-1 relative">
    <input
      type="checkbox"
      :id="id ?? null"
      :name="name"
      :value="value"
      :checked="modelValue.includes(value)"
      class="bg-gray-200 text-gold-500 rounded border-0 w-5 h-5 mr-2 focus:ring-2 focus:ring-gold-500"
      @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)
  }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.