I have been trying to create a completely custom checkbox/radio group component that does not depend on the native/vue input elements. One reason to have complete freedom of styling it, second for learning purposes. (Vue 3)
It should work similary to what the vue checkbox provides, so that:
a) I can bind the v-model
to an array
of selected
values: ["1", "2", "3"]
<checkbox value="1" v-model="selected"></checkbox>
<checkbox value="2" v-model="selected"></checkbox>
<checkbox value="3" v-model="selected"></checkbox>
b) I can bind it to a v-model
boolean
:
<checkbox v-model="checked"></checkbox>
My checkbox
implementation while not the prettiest seems to work ok (barring any edge cases).
//<Checkbox> component
import {defineComponent} from 'vue';
export default defineComponent({
props: {
value: {type: null, default: null},
modelValue: {type: [Array,Boolean], default: () => []},
},
emits: ['update:modelValue'],
data() {
let _checked;
if (typeof this.modelValue == 'boolean') {
_checked = this.modelValue == true;
} else {
_checked = this.modelValue.indexOf(this.value) != -1;
}
return {
checked: _checked
}
},
computed: {
class() {
return {'checked': this.checked}
},
},
watch: {
modelValue: {
deep: true,
handler(newv, oldv) {
if (typeof newv == 'boolean') {
this.checked = newv == true;
} else {
if (newv) {
this.checked = newv.indexOf(this.value) != -1;
}
}
}
},
value(newv, oldv) {
if (newv == oldv) return;
let arr = [...this.modelValue];
let idx = arr.indexOf(oldv);
if (idx != -1) {
arr[idx] = newv;
}
this.$emit("update:modelValue", arr);
},
checked(newv, oldv) {
if (newv == oldv) return;
if (typeof this.modelValue == 'boolean') {
this.$emit("update:modelValue", newv);
} else {
let arr = [...this.modelValue];
if (newv) {
arr.push(this.value);
} else {
arr.splice(arr.indexOf(this.value), 1);
}
this.$emit("update:modelValue", arr);
}
}
}
})
</script>
<template>
<div @click.stop="checked = !checked" :class="this.class">
<slot></slot>
</div>
</template>
Now, with the checkbox working I wanted to have a way to provide a radio
like functionality so only one component can be checked at a time. My idea was to wrap it in a parent component that will orchestrate it, like so:
<group v-model="selected">
<checkbox value="1"></checkbox>
<checkbox value="2"></checkbox>
<checkbox value="3"></checkbox>
</group>
First thing, I need to have a way for the individual checkbox
es to register with the group
component. I achieved it with using provide
and inject
.
//<Group> component
export default defineComponent({
props: {
modelValue: { type: [Boolean, Array], default: () => []}
},
provide() {
// provide this instance to the child checkbox components
return {
'group': this
}
},
data() {
return {
checkboxes: []
}
},
mounted() {
// got this.checkboxes filled with <checkbox> components at this point
},
});
In the child checkbox
es components, I have added code to receive the injected group
component and register the component with the parent.
//rest of <checkbox> component code...
inject: ['group'],
mounted() {
this.group.checkboxes.push(this);
}
//...rest of <checkbox> component code...
Now at this stage I am pretty much stuck. My <Group>
component has all the available child <Checkbox>
components in it’s mounted
lifecycle. But I have no idea how can I register for the <checkbox>
events or how to pass a modelValue
. There is no API for listening to events, and I can’t pass the Group
s component modelValue to a checkbox.
//<Group component
props: {
modelValue: {type: [Boolean, Array], default: () => []}
},
mounted() {
// can't set the modelValue like this on each checkbox, props are readonly!
//for(let checkbox of this.checkboxes) {
// checkbox.modelValue = this.modelValue;
//}
// no `$on` event listener available in Vue 3!
//for(let checkbox of this.checkboxes) {
// checkbox.$on("update:modelValue", () => {
// //do something with it...
// });
//}
}