[提问] 如何自定义 checkbox 和 checkbox-group

我自己先定义了一个 checkboxcheckbox-group ,他有些问题,比如在使用 checkbox-group

<template>
  <div class="checkbox-view">
    <CheckboxGroup v-model="selected2">
      <Checkbox value="hello">Hello</Checkbox>
      <Checkbox value="world">World</Checkbox>
      <Checkbox value="fuck">Fuck</Checkbox>
      <Checkbox value="you">You</Checkbox>
    </CheckboxGroup>

    <CheckboxGroup v-model="selected2">
      <Checkbox value="hello">Hello</Checkbox>
      <Checkbox value="world">World</Checkbox>
      <Checkbox value="fuck">Fuck</Checkbox>
      <Checkbox value="you">You</Checkbox>
    </CheckboxGroup>
  </div>
</template>

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

const selected = ref(["1"] as string[])
const selected2 = ref([] as string[])
</script>

selected2 更新时,两个 checkbox-group 下的 checkbox 也应该更新 checked 属性,但是实际不是这样的
image

然而 checkbox-group 中的代码是这样的

<template>
  <div class="checkbox-group">
    <slot/>
  </div>
</template>

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

const props = defineProps<{
  modelValue: any[]
}>()

const emits = defineEmits(["update:modelValue"])

const selected = ref(props.modelValue)
provide("selected", selected)

watch(selected, (newvalue, _) => {
  emits("update:modelValue", newvalue)
})

</script>

也就是说每个 checkbox-group 中都会拷贝一份 modelValue ,所以 selected2 更新时两组 checkbox-group 下的 checkbox 的更新不是同步的
反观 element-plus

其中的 checkbox 根据 checkList 同步更新


代码问题也找到了,我的问题是,如何才能使两个在 checkbox-group 中的 checkbox 同步更新

这是相关的代码
Checkbox.vue

<template>
  <span class="checkbox" :class="{'disabled': disabled}">
    <span class="input">
      <input type="checkbox" :checked="ischecked" :value="value"
             @change="updateInput($event)"
             :disabled="disabled"/>
    </span>

    <span class="label" @click="handleClick">
      <slot/>
      <template v-if="!$slots.default">{{label}}</template>
    </span>
  </span>
</template>

<script lang="ts" setup>
import {computed, inject, PropType, Ref} from "vue";

const props = defineProps({
  modelValue: {
    type: Array as PropType<any[]>
  },

  disabled: {
    type: Boolean,
    default: false
  },

  value: {
    type: [String, Number, Boolean, Object],
    required: true
  },

  label: String
})

const emits = defineEmits(["update:modelValue"])
const selected: any = props.modelValue !== undefined ? null : inject<Ref<any[]>>("selected")


const ischecked = computed(() => {
  if(props.modelValue instanceof Array) {
    return props.modelValue.includes(props.value)
  } else {
    return selected.value.includes(props.value)
  }
})

const updateInput = (event: any) => {
  let checked = event.target.checked
  if(props.modelValue instanceof Array) {
    let newvalue = [...props.modelValue]

    if(checked) {
      newvalue.push(props.value)
    } else {
      newvalue.splice(newvalue.indexOf(props.value), 1)
    }

    emits("update:modelValue", newvalue)
  } else {
    let newvalue = [...selected.value]
    if(checked) {
      newvalue.push(props.value)
    } else {
      newvalue.splice(newvalue.indexOf(props.value), 1)
    }

    selected.value = newvalue
  }
}

const handleClick = () => {
  if(props.modelValue instanceof Array) {
    let newvalue = [...props.modelValue]
    if(ischecked.value) {
      newvalue.splice(newvalue.indexOf(props.value), 1)
    } else {
      newvalue.push(props.value)
    }

    emits("update:modelValue", newvalue)
  } else {
    let newvalue = [...selected.value]
    if(ischecked.value) {
      newvalue.splice(newvalue.indexOf(props.value), 1)
    } else {
      newvalue.push(props.value)
    }

    selected.value = newvalue
  }
}
</script>

<style lang="scss" scoped>
span.checkbox {
  cursor: pointer;
  display: inline-flex;
  align-items: center;

  span.input {
    display: inline-flex;
    position: relative;
    cursor: pointer;
    
    input[type="checkbox"] {
      cursor: pointer;
    }
  }
}

span.checkbox.disabled {
  cursor: not-allowed;
  span.input {
    cursor: not-allowed;
    input[type="checkbox"] {
      cursor: not-allowed;
    }
  }

  span.label {
    cursor: not-allowed;
    color: rgba(0, 0, 0, 0.15);
  }
}
</style>

CheckboxGroup.vue

<template>
  <div class="checkbox-group">
    <slot/>
  </div>
</template>

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

const props = defineProps<{
  modelValue: any[]
}>()

const emits = defineEmits(["update:modelValue"])

const selected = ref(props.modelValue)
provide("selected", selected)

watch(selected, (newvalue, _) => {
  emits("update:modelValue", newvalue)
})

</script>

<style scoped>

</style>

思考一下

  1. 你所group所使用的值是原本的那个selected2么?
  2. 如果不是,那你对原始的selected2进行更改group还会去响应嘛?

.
.
.
.
.
.
.
.


看看你CheckboxGroup.vue里的代码

const selected = ref(props.modelValue) // 所使用的已经不是原本的selected了
watch(...,() => {
  emit('...', selected) // 你确实把原始值给更新了
})

你更新了原来的值,问题是你group里的selected是新的列表,怎么会影响到内部
知道了原因,开始更改,比较简单的改法 CheckboxGroup.vue

const selected = computed({
  get: () => props.modelValue,
  set: (newVal) => emit('update:modelValue',newVal)  
})
//删除 watch就可以了

以上代码计算属性实现了 selected 返回props.modelValue, 且给selected赋值则更新modelValue

顺便一提

  • 以下结构可以替换掉你的那一坨,不需要监听click
<label><input type="checkbox" ...><span><slot /></span></label>
  • Checkbox的modelValue可以不弄成list

给几个建议:

  1. TS 是可以自己推断类型的,例如 ref(['1']),不需要用 as,多余的代码除了影响阅读和 refactor 之外没有益处。
  2. 对于无法自动推断类型的地方,ref 的类型可以考虑官方推荐的方式 ref<string[]>([])

仅供参考。