学习Element Plus组件库(三):Checkbox组件

489 阅读3分钟

首先构建Checkbox组件的目录结构:

├── packages
│   ├── components
│   │   ├── checkbox
│   │   │   ├── src               # 组件入口目录
│   │   │   │   ├── checkbox.ts   # Ts类型和组件属性
│   │   │   │   └── checkbox.vue  # 组件代码
│   │   │   └── index.ts          # 组件入口文件

Ts类型声明和组件属性定义

checkbox

import { UPDATE_MODEL_EVENT } from '@storm/constants'
import { isBoolean, isNumber, isString } from '@storm/utils'
import { ExtractPropTypes } from 'vue'

export type CheckboxValueType = number | string | boolean
export const checkboxProps = {
  modelValue: {
    type: [Number, String, Boolean],
    default: undefined,
  },
  label: {
    type: [String, Boolean, Number]
  },
  // 设置半选状态,仅负责样式控制
  indeterminate: Boolean,
  disabled: Boolean,
  checked: Boolean,
  name: String
}

export const checkboxEmits = {
  [UPDATE_MODEL_EVENT]: (val: CheckboxValueType) => isNumber(val) || isString(val) || isBoolean(val),
  change: (val: CheckboxValueType) => isNumber(val) || isString(val) || isBoolean(val)
}
// 提供外部使用
export type CheckboxProps = ExtractPropTypes<typeof checkboxProps>
export type CheckboxEmits = typeof checkboxEmits

checkbox-group

import { ExtractPropTypes, PropType, WritableComputedRef, InjectionKey } from 'vue'
import { CheckboxValueType } from './checkbox'
import { UPDATE_MODEL_EVENT } from '@storm/constants'
import { isArray } from '@storm/utils'

// 排除CheckboxValueType中的boolean类型
export type CheckboxGroupValueType = Exclude<CheckboxValueType, boolean>[]
type CheckboxGroupContext = {
  modelValue?: WritableComputedRef<any>,
  disabled?: boolean,
  changeEvent?: (...args: any) => any
}
export const checkboxGroupProps = {
  modelValue: {
    type: Array as PropType<CheckboxGroupValueType>,
    default: () => []
  },
  diabled: Boolean
}

export const checkGroupEmits = {
  [UPDATE_MODEL_EVENT]: (val: CheckboxGroupValueType) => isArray(val),
  change: (val: CheckboxGroupValueType) => isArray(val)
}
// inject传递的一些属性的类型
export const checkboxGroupContextKey: InjectionKey<CheckboxGroupContext> = Symbol('checkboxGroupContextKey')
// 提供外部使用
export type CheckboxGroupProps = ExtractPropTypes<typeof checkboxGroupProps>
export type CheckboxGroupEmits = typeof checkGroupEmits

组件实现

checkbox

<template>
  <label :class="[
    bem.b(),
    bem.is('disabled', isDisabled),
    bem.is('checked', isChecked)
  ]">
    <span :class="[
      bem.e('input'),
      bem.is('disabled', isDisabled),
      bem.is('checked', isChecked),
      bem.is('indeterminate', indeterminate)
    ]">
      <input
        type="checkbox"
        :class="bem.e('origin')"
        v-model="model"
        :value="label"
        :disabled="isDisabled"
        :name="name"
        @change="handleChange"
      >
      <span :class="bem.e('inner')"></span>
    </span>
    <span :class="bem.e('label')" v-if="hasOwnLabel">
      <slot>
        {{ label }}
      </slot>
    </span>
  </label>
</template>

<script setup lang="ts">
import { createNamespace } from '@storm/utils';
import { checkboxEmits, checkboxProps } from './checkbox';
import { useCheckbox } from './use-checkbox';
import { useSlots } from 'vue';
defineOptions({ name: 'SCheckbox' })
const props = defineProps(checkboxProps)
defineEmits(checkboxEmits)
const slots = useSlots()

const bem = createNamespace('checkbox')
const { model, isChecked, isDisabled, hasOwnLabel, handleChange } = useCheckbox(props, slots)
</script>

use-checkbox.ts

import { SetupContext, computed, getCurrentInstance, inject } from "vue";
import { CheckboxProps } from "./checkbox";
import { checkboxGroupContextKey } from "./checkbox-group";
import { isArray, isBoolean } from "@storm/utils";
import { UPDATE_MODEL_EVENT } from "@storm/constants";

export const useCheckbox = (props: CheckboxProps, slots: SetupContext['slots']) => {
  const { emit } = getCurrentInstance()!
  // 注入checkbox-group的值
  const checkboxGroup = inject(checkboxGroupContextKey, undefined)
  const isGroup = computed(() => !!checkboxGroup)

  const model = computed({
    get() {
      return isGroup.value ? checkboxGroup?.modelValue?.value : props.modelValue
    },
    set(val: unknown) {
      if (isGroup.value && isArray(val)) {
        checkboxGroup?.changeEvent?.(val)
      } else {
        emit(UPDATE_MODEL_EVENT, val)
      }
    }
  })
  // 处理不同绑定值的选中状态
  const isChecked = computed(() => {
    const value = model.value
    if (isBoolean(value)) {
      return value
    } else if (isArray(value)) {
      return value.includes(props.label)
    } else {
      return !!value
    }
  })
  const isDisabled = computed(() => checkboxGroup?.disabled ? checkboxGroup.disabled : props.disabled)
  // 没传label也没有默认插槽
  const hasOwnLabel = computed<boolean>(() => {
    return !!(slots.default || props.label)
  })

  const handleChange = (e: Event) => {
    const target = e.target as HTMLInputElement
    emit('change', target.checked)
  }

  return {
    model,
    isChecked,
    isDisabled,
    hasOwnLabel,
    handleChange
  }
}

checkbox-group

<template>
  <div :class="bem.b('group')">
    <slot />
  </div>
</template>

<script setup lang="ts">
import { createNamespace } from '@storm/utils';
import { checkGroupEmits, checkboxGroupProps, CheckboxGroupValueType, checkboxGroupContextKey } from './checkbox-group';
import { computed, provide } from 'vue';
import { UPDATE_MODEL_EVENT } from '@storm/constants';
defineOptions({ name: 'SCheckboxGroup' })
const props = defineProps(checkboxGroupProps)
const emit = defineEmits(checkGroupEmits)

const bem = createNamespace('checkbox')
const modelValue = computed({
  get() {
    return props.modelValue
  },
  set(val: CheckboxGroupValueType) {
    changeEvent(val)
  }
})

const changeEvent = (value: CheckboxGroupValueType) => {
  emit(UPDATE_MODEL_EVENT, value)
  emit('change', value)
}
// 提供给checkbox的一些值和方法
provide(checkboxGroupContextKey, {
  modelValue,
  disabled: props.diabled,
  changeEvent
})
</script>

入口文件

import { withInstall } from '@storm/utils'
import _Checkbox from './src/checkbox.vue'
import _CheckboxGroup from './src/checkbox-group.vue'
// 添加install方法 注册checkbox的时候同时注册checkbox-group
export const Checkbox = withInstall(_Checkbox, {
  _CheckboxGroup
})
export default Checkbox
export * from './src/checkbox'
export * from './src/checkbox-group'

// 配合volar插件 可以在模版中被解析
declare module 'vue' {
  export interface GlobalComponents {
    SCheckbox: typeof Checkbox
  }
}

样式文件

s-checkbox__inner类加上伪类用来实现勾选框。

@use "./mixins/mixins.scss" as *;
@use "./common/var.scss" as *;

@include b(checkbox) {
  display: inline-flex;
  align-items: center;
  height: 32px;
  margin-right: 30px;
  font-size: $font-size-base;
  font-weight: 500;
  color: $color-text;
  white-space: nowrap;
  user-select: none;
  cursor: pointer;
  @include e(input) {
    display: inline-flex;
    vertical-align: middle;
    @include when(checked) {
      .#{$namespace}-checkbox__inner {
        border-color: $color-primary;
        background-color: $color-primary;
        &:after {
          border-color: $color-white;
          transform: rotate(45deg) scaleY(1);
        }
      }
      & + .#{$namespace}-checkbox__label {
        color: $color-primary;
      }
    }
    @include when(disabled) {
      .#{$namespace}-checkbox__inner {
        border-color: $color-border;
        background-color: $color-bg-disabled;
      }
      & + span.#{$namespace}-checkbox__label {
        color: $color-placeholder-text;
      }
      &.is-checked {
        .#{$namespace}-checkbox__inner {
          &::after {
            border-color: $color-placeholder-text;
          }
        }
      }
      &.is-indeterminate {
        .#{$namespace}-checkbox__inner {
          border-color: $color-border;
          background-color: $color-bg-disabled;
          &::before {
            background-color: $color-placeholder-text;
          }
        }
      }
    }
    @include when(indeterminate) {
      .#{$namespace}-checkbox__inner {
        border-color: $color-primary;
        background-color: $color-primary;
        &:after {
          display: none;
        }
        &:before {
          content: "";
          position: absolute;
          top: 5px;
          left: 0;
          right: 0;
          height: 2px;
          background-color: $color-white;
          transform: scale(0.5);
        }
      }
    }
  }
  @include e(origin) {
    position: absolute;
    width: 0;
    height: 0;
    z-index: -1;
    opacity: 0;
  }
  @include e(inner) {
    position: relative;
    display: inline-block;
    width: 14px;
    height: 14px;
    border-radius: 2px;
    border: 1px solid $color-border;
    background-color: $color-white;
    &:after {
      content: "";
      position: absolute;
      top: 1px;
      left: 4px;
      width: 3px;
      height: 7px;
      border: 1px solid transparent;
      border-left: 0;
      border-top: 0;
      transform-origin: center;
      transform: rotate(45deg) scaleY(0);
      transition: transform 0.15s ease-in 0.05s;
    }
  }
  @include e(label) {
    display: inline-block;
    vertical-align: middle;
    line-height: 1;
    padding-left: 8px;
  }
  @include when(disabled) {
    cursor: not-allowed;
  }
}

最终效果

基础用法:

image.png

多选框组:

image.png

中间状态:

image.png