[Element Plus 源码解析] Checkbox 多选框

2,499 阅读3分钟

一、组件介绍

官网链接:Checkbox 组件 | Element (gitee.io)

Checkbox组件是日常最为常用的组件之一,用于用户勾选选项。

1.1 属性

1.1.1 值绑定相关

  • model-value / v-model: string/number/boolean类型,绑定的值;
  • label: string / number / boolean / object,选中时的值,在checkbox-group或者绑定对象类型为array时有效;
  • true-lable: string / number, 选中时的值;
  • false-label: string / number, 未选中时的值;
  • checked: boolean类型,当前是否勾选,默认false;

1.1.2 展示相关

  • disabled: boolean类型,是否禁用;
  • border: boolean类型,是否显示边框;
  • name: string类型,原生name属性;
  • size: string类型,仅在border状态下生效,可选值medium/small/mini;
  • indeterminate: boolean类型,设置半选状态,仅设置样式,默认false;

1.2 事件

  • change: 绑定值发生变化时触发;

二、源码分析

2.1 template

<template>
  // 外层采用lable标签
  <label
    :id="id"
    class="el-checkbox"
    :class="[
      border && checkboxSize ? 'el-checkbox--' + checkboxSize : '',
      { 'is-disabled': isDisabled },
      { 'is-bordered': border },
      { 'is-checked': isChecked }
    ]"
    :aria-controls="indeterminate ? controls : null"
  >
    // 选择框部分
    <span
      class="el-checkbox__input"
      :class="{
        'is-disabled': isDisabled,
        'is-checked': isChecked,
        'is-indeterminate': indeterminate,
        'is-focus': focus
      }"
      :tabindex="indeterminate ? 0 : false"
      :role="indeterminate ? 'checkbox' : false"
      :aria-checked="indeterminate ? 'mixed' : false"
    >
      // 用于控制选中/未选中样式
      <span class="el-checkbox__inner"></span>
      // 有trueLable/falseLabel时,给input添加自定义属性true-value/false-value
      <input
        v-if="trueLabel || falseLabel"
        v-model="model"
        :checked="isChecked"
        class="el-checkbox__original"
        type="checkbox"
        :aria-hidden="indeterminate ? 'true' : 'false'"
        :name="name"
        :disabled="isDisabled"
        :true-value="trueLabel"
        :false-value="falseLabel"
        @change="handleChange"
        @focus="focus = true"
        @blur="focus = false"
      >
      <input
        v-else
        v-model="model"
        class="el-checkbox__original"
        type="checkbox"
        :aria-hidden="indeterminate ? 'true' : 'false'"
        :disabled="isDisabled"
        :value="label"
        :name="name"
        @change="handleChange"
        @focus="focus = true"
        @blur="focus = false"
      >
    </span>
    // 文字部分
    <span v-if="$slots.default || label" class="el-checkbox__label">
      <slot></slot>
      <template v-if="!$slots.default">{{ label }}</template>
    </span>
  </label>
</template>

2.2 script

// checkbox.vue
// 部分核心代码
import { useCheckbox } from './useCheckbox'

export default defineComponent({
  setup(props) {
    // 调用useCheckbox方法
    return useCheckbox(props)
  },
})
// useCheckbox.ts

export const useCheckboxGroup = () => {
  const ELEMENT = useGlobalConfig()
  // checkbox用于form/checkbox-group中时,通过inject注入数据
  const elForm = inject(elFormKey, {} as ElFormContext)
  const elFormItem = inject(elFormItemKey, {} as ElFormItemContext)
  const checkboxGroup = inject<ICheckboxGroupInstance>('CheckboxGroup', {})
  // 是否是group模式
  const isGroup = computed(() => checkboxGroup && checkboxGroup?.name === 'ElCheckboxGroup')
  // form-item的size
  const elFormItemSize = computed(() => {
    return elFormItem.size
  })
  return {
    isGroup,
    checkboxGroup,
    elForm,
    ELEMENT,
    elFormItemSize,
    elFormItem,
  }
}

// 
const useModel = (props: ICheckboxProps) => {
  const selfModel = ref(false)
  const { emit } = getCurrentInstance()
  const { isGroup, checkboxGroup } = useCheckboxGroup()
  const isLimitExceeded = ref(false)
  // group模式下,取group绑定值;独立使用情况下,取传入的modelValue值
  const store = computed(() => checkboxGroup ? checkboxGroup.modelValue?.value : props.modelValue)
  //设置了getter和setter的计算属性
  const model = computed({
    get() {
      return isGroup.value
        ? store.value
        : props.modelValue ?? selfModel.value
    },

    set(val: unknown) {
      if (isGroup.value && Array.isArray(val)) {
        // chebox-group模式下,数据是数组格式的
        isLimitExceeded.value = false
        // 判断是否超过checbox-group设定的可选个数范围
        if (checkboxGroup.min !== undefined && val.length < checkboxGroup.min.value) {
          isLimitExceeded.value = true
        }
        if (checkboxGroup.max !== undefined && val.length > checkboxGroup.max.value) {
          isLimitExceeded.value = true
        }

        isLimitExceeded.value === false && checkboxGroup?.changeEvent?.(val)
      } else {
        // 发射v-model事件,即update:modelValue
        emit(UPDATE_MODEL_EVENT, val)
        selfModel.value = val as boolean
      }
    },
  })

  return {
    model,
    isLimitExceeded,
  }
}

// checkbox状态
const useCheckboxStatus = (props: ICheckboxProps, { model }: PartialReturnType<typeof useModel>) => {
  const { isGroup, checkboxGroup, elFormItemSize, ELEMENT } = useCheckboxGroup()
  const focus = ref(false)
  const size = computed<string | undefined>(() => checkboxGroup?.checkboxGroupSize?.value || elFormItemSize.value || ELEMENT.size)
  // 是否选中
  const isChecked = computed(() => {
    const value = model.value
    // 各种数据类型下的判断处理
    if (toTypeString(value) === '[object Boolean]') {
      // boolean类型,直接返回model.value
      return value
    } else if (Array.isArray(value)) {
      // checkbox-group模式下,值是数组,判断数组是否包含当前checkbox的lable
      return value.includes(props.label)
    } else if (value !== null && value !== undefined) {
      // 其他数据类型,判断是否与传入的trueLabel相等
      return value === props.trueLabel
    }
  })
  // size:传入的size > form-item的size > 全局配置的size
  const checkboxSize = computed(() => {
    const temCheckboxSize = props.size || elFormItemSize.value || ELEMENT.size
    return isGroup.value
      ? checkboxGroup?.checkboxGroupSize?.value || temCheckboxSize
      : temCheckboxSize
  })

  return {
    isChecked,
    focus,
    size,
    checkboxSize,
  }
}

// 是否禁用
const useDisabled = (
  props: ICheckboxProps,
  { model, isChecked }: PartialReturnType<typeof useModel> & PartialReturnType<typeof useCheckboxStatus>,
) => {
  const { elForm, isGroup, checkboxGroup } = useCheckboxGroup()
  // 由于可选数量范围导致的禁用判断
  const isLimitDisabled = computed(() => {
    const max = checkboxGroup.max?.value
    const min = checkboxGroup.min?.value
    return !!(max || min) && (model.value.length >= max && !isChecked.value) ||
      (model.value.length <= min && isChecked.value)
  })
  // group模式下:group的disabeld > 自身的disabled > 数量限制的disabled
  // 单独使用:prop的disabled > form的disabled
  const isDisabled = computed(() => {
    const disabled = props.disabled || elForm.disabled
    return isGroup.value
      ? checkboxGroup.disabled?.value || disabled || isLimitDisabled.value
      : props.disabled || elForm.disabled
  })

  return {
    isDisabled,
    isLimitDisabled,
  }
}

// 
const setStoreValue = (props: ICheckboxProps, { model }: PartialReturnType<typeof useModel>) => {
  function addToStore() {
    // group模式,数组中不包含当前checkbox的label,则将当前checkbox的label push到数组中
    if (
      Array.isArray(model.value) &&
      !model.value.includes(props.label)
    ) {
      model.value.push(props.label)
    } else {
      model.value = props.trueLabel || true
    }
  }
  // 选中时执行addToStore
  props.checked && addToStore()
}

// 事件
const useEvent = (props: ICheckboxProps, { isLimitExceeded }: PartialReturnType<typeof useModel>) => {
  const { elFormItem } = useCheckboxGroup()
  const { emit } = getCurrentInstance()
  // change处理函数
  function handleChange(e: InputEvent) {
    if (isLimitExceeded.value) return
    const target = e.target as HTMLInputElement
    const value = target.checked
      ? props.trueLabel ?? true
      : props.falseLabel ?? false
    // 向外抛出change事件
    emit('change', value, e)
  }

  watch(() => props.modelValue, val => {
    elFormItem.formItemMitt?.emit('el.form.change', [val])
  })

  return {
    handleChange,
  }
}

// useCheckbox在checkbox.vue中被调用
export const useCheckbox = (props: ICheckboxProps) => {
  const { model, isLimitExceeded } = useModel(props)
  const { focus, size, isChecked, checkboxSize } = useCheckboxStatus(props, { model })
  const { isDisabled } = useDisabled(props, { model, isChecked })
  const { handleChange } = useEvent(props, { isLimitExceeded })

  setStoreValue(props, { model })

  return {
    isChecked,
    isDisabled,
    checkboxSize,
    model,
    handleChange,
    focus,
    size,
  }
}

2.3 总结

  1. checkbox.vue的script部分主要是调用useCheckbox方法,相对于将所有属性方法都写在setup函数中,这种写法更优雅简洁,按功能划分函数,相应的数据和方法在同一个函数中,也是vue3推出的composition-api式写法的初衷;
  2. checkbox的model值在单独使用时可以是string/boolean/number类型;但是在checkbox-group模式下,被设置为checkbox-group的model值,是数组形式,通过computed的getter和setter方法做到了单个checkbox和全局checkbox-group的联动;