Search code examples
vue.jsvue-component

Vue: Array Input Bindings with Custom Components


Using Vue's v-bind syntax, we can bind a number of checkboxes with different value attributes to the same array, which will then contain the corresponding values of all checked checkboxes. See the example: https://vuejs.org/guide/essentials/forms.html#checkbox

Is it possible to achieve the same with custom components? Let's say I define a component CheckBox.vue as follows:

<script lang="ts" setup>
import { ref } from "vue";

const modelValue = ref(false);

const emit = defineEmits<{ "update:modelValue": [modelValue: boolean] }>();

function handle() {
  modelValue.value = !modelValue.value;

  emit("update:modelValue", modelValue.value);
}
</script>

<template>
  <label>
    <button type="button" @click="handle">
      <span v-if="modelValue">✔️</span>
    </button>
  </label>
</template>

Now I can use v-model to synchronize a variable with the value of my custom checkbox. How can I make it so that I can also use a number of these checkboxes and bind them to an array?

Simply using the component as it is defined in the question in combination with v-bind and an array does the following: The first checkbox will add a single item to the array, which will change between true and false when clicking the checkbox. The other boxes have no effect on the array. Setting the value attribute on the button does not change this behavior.


Solution

  • Vue SFC Playground

    You have to use value property as for <input type="checkbox">:

    App.vue

    <script setup>
    import { ref } from 'vue'
    import Checkbox from './Checkbox.vue';
    
    const arr = ref([]);
    
    </script>
    
    <template>
    
      <checkbox v-model="arr" name="first" />
      <checkbox v-model="arr" name="second" />
      <checkbox v-model="arr" name="third" />
      <div> {{ arr }} </div>
    </template>
    
    

    Checkbox.vue

    <script setup>
    import { computed } from "vue";
    
    const props = defineProps({name: String, modelValue: [Array, Boolean]});
    const emit = defineEmits(['update:modelValue']);
    
    const value = computed({
      get(){
        return props.modelValue?.includes?.(props.name) ?? props.modelValue;
      }, set(val){
        emit('update:modelValue', Array.isArray(props.modelValue) ? 
        props.modelValue.includes(props.name) ?  props.modelValue.filter(item => item !== props.name) : [...props.modelValue, props.name]
        : val);
    }});
    
    
    </script>
    
    <template>
      <label>
        <button type="button" @click.prevent="value = !value">
          <span v-if="value">✔️</span>
          <span v-else>&nbsp;</span>
        </button>
      </label>
    </template>
    

    You can though keep Checkbox.vue as simple as handling a boolean value only. For that you use computed to get the same array from an object properties bound to the checkboxes:

    Vue SFC Playground

    App.vue

    <script setup>
    import { ref } from 'vue'
    import Checkbox from './Checkbox.vue';
    
    const arr = ref([]);
    
    </script>
    
    <template>
    
      <checkbox v-model="arr" value="first" />
      <checkbox v-model="arr" value="second" />
      <checkbox v-model="arr" value="third" />
      <div> {{ arr }} </div>
    </template>
    
    

    Checkbox.vue could be left the same (handling both arrays and booleans) but could be simplified in this scenario:

    <script setup>
    import { computed } from "vue";
    
    const props = defineProps({value: String, modelValue: [Array, Boolean]});
    const emit = defineEmits(['update:modelValue']);
    
    const value = computed({
      get(){
        return props.modelValue?.includes?.(props.value) ?? props.modelValue;
      }, set(val){
        emit('update:modelValue', Array.isArray(props.modelValue) ? 
        props.modelValue.includes(props.value) ?  props.modelValue.filter(item => item !== props.value) : [...props.modelValue, props.value]
        : val);
    }});
    
    
    </script>
    
    <template>
      <label>
        <button type="button" @click.prevent="value = !value">
          <span v-if="value">✔️</span>
          <span v-else>&nbsp;</span>
        </button>
      </label>
    </template>