element-plus 第一期 button

293 阅读1分钟

2022/08/16

button 组件实现

布局

  • 加载状态 loading || $slots.loading
  • 图标 icon || $slots.icon
  • 文案 $slots.default
<template>
  <button
    ref="_ref"
    :class="[
      ns.b(),
      ns.m(_type),
      ns.m(_size),
      ns.is('disabled', _disabled),
      ns.is('loading', loading),
      ns.is('plain', plain),
      ns.is('round', round),
      ns.is('circle', circle),
    ]"
    :disabled="_disabled || loading"
    :autofocus="autofocus"
    :type="nativeType"
    :style="buttonStyle"
    @click="handleClick"
  >
    <!-- 判断是否有加载状态 -->
    <template v-if="loading">
      <slot v-if="$slots.loading" name="loading" />
      <el-icon v-else :class="ns.is('loading')">
        <component :is="loadingIcon" />
      </el-icon>
    </template>
    <!-- 判断是否有图标 -->
    <el-icon v-else-if="icon || $slots.icon">
      <component :is="icon" v-if="icon" />
      <slot v-else name="icon" />
    </el-icon>
    <!-- 默认内容 <el-button>搜索</el-button> -->
    <span
      v-if="$slots.default"
      :class="{ [ns.em('text', 'expand')]: shouldAddSpace }"
    >
      <slot />
    </span>
  </button>
</template>

这里注意,class 的定义方式,BEM命名风格,b代表blockm代表modifier, is表示是否状态修饰

:class="[    ns.b(),    ns.m(_type),    ns.m(_size),    ns.is('disabled', _disabled),    ns.is('loading', loading),    ns.is('plain', plain),    ns.is('round', round),    ns.is('circle', circle),]"

其中 const ns = useNamespace('button'),而 useNamespace 是element-plus的class命名空间钩子函数 import { useNamespace, } from '@element-plus/hooks',下面具体学习 useNamespace

Class 命名实现

// hooks/use-namespace/index.ts

// 生成class命名
const _bem = (
  namespace: string, // 命名空间
  block: string, // 块名称
  blockSuffix: string, // 块名称后缀
  element: string, // 元素
  modifier: string // 修饰符
) => {
  let cls = `${namespace}-${block}` // 通过"-"连接块
  if (blockSuffix) {
    cls += `-${blockSuffix}` // 通过"-"连接块后缀
  }
  if (element) {
    cls += `__${element}` // 通过"__"连接元素
  }
  if (modifier) {
    cls += `--${modifier}` // // 通过"--"连接块修饰符
  }
  return cls
}

export const useNamespace = (block: string) => {
  const globalConfig = useGlobalConfig('namespace')
  const namespace = computed(() => globalConfig.value || defaultNamespace)
  // block
  const b = (blockSuffix = '') =>
    _bem(unref(namespace), block, blockSuffix, '', '')
  // element
  const e = (element?: string) =>
    element ? _bem(unref(namespace), block, '', element, '') : ''
  // modifier
  const m = (modifier?: string) =>
    modifier ? _bem(unref(namespace), block, '', '', modifier) : ''
  const be = (blockSuffix?: string, element?: string) =>
    blockSuffix && element
      ? _bem(unref(namespace), block, blockSuffix, element, '')
      : ''
  const em = (element?: string, modifier?: string) =>
    element && modifier
      ? _bem(unref(namespace), block, '', element, modifier)
      : ''
  const bm = (blockSuffix?: string, modifier?: string) =>
    blockSuffix && modifier
      ? _bem(unref(namespace), block, blockSuffix, '', modifier)
      : ''
  const bem = (blockSuffix?: string, element?: string, modifier?: string) =>
    blockSuffix && element && modifier
      ? _bem(unref(namespace), block, blockSuffix, element, modifier)
      : ''
  // 是非状态修饰
  const is: {
    (name: string, state: boolean | undefined): string
    (name: string): string
  } = (name: string, ...args: [boolean | undefined] | []) => {
    const state = args.length >= 1 ? args[0]! : true
    return name && state ? `${statePrefix}${name}` : '' // 如果state为false则返回空字符
  }
  return {
    namespace,
    b,
    e,
    m,
    be,
    em,
    bm,
    bem,
    is,
  }
}

示例:

<el-button type="primary" size="small" disabled>搜索</el-button>
  • ns.b(): el-button
  • ns.m(_type): el-button--primary 修饰符
  • ns.m(_size): el-button--small 修饰符
  • ns.is('disabled', _disabled): is-disabled 是非状态
  • ns.is('disabled', _disabled): ""
  • ns.is('loading', loading): ""
  • ns.is('plain', plain): ""
  • ns.is('round', round): ""
  • ns.is('circle', circle): ""

script 实现

<script lang="ts" setup>
// 引入钩子
import { Text, computed, inject, ref, useSlots } from 'vue'
import { ElIcon } from '@element-plus/components/icon'
import {
  useDisabled,
  useFormItem,
  useGlobalConfig,
  useNamespace,
  useSize,
} from '@element-plus/hooks'
import { buttonGroupContextKey } from '@element-plus/tokens'
import { buttonEmits, buttonProps } from './button'
import { useButtonCustomStyle } from './button-custom'

// 
defineOptions({
  name: 'ElButton',
})

// 接收父组件传递的数据(buttonProps写出去了)
const props = defineProps(buttonProps)
// 接收父组件传递的方法
const emit = defineEmits(buttonEmits)
// 引入插槽
const slots = useSlots()

const buttonGroupContext = inject(buttonGroupContextKey, undefined)
const globalConfig = useGlobalConfig('button')
const ns = useNamespace('button')
const { form } = useFormItem()
const _size = useSize(computed(() => buttonGroupContext?.size))
const _disabled = useDisabled()
const _ref = ref<HTMLButtonElement>()

const _type = computed(() => props.type || buttonGroupContext?.type || '')
const autoInsertSpace = computed(
  () => props.autoInsertSpace ?? globalConfig.value?.autoInsertSpace ?? false
)

// add space between two characters in Chinese
const shouldAddSpace = computed(() => {
  const defaultSlot = slots.default?.()
  if (autoInsertSpace.value && defaultSlot?.length === 1) {
    const slot = defaultSlot[0]
    if (slot?.type === Text) {
      const text = slot.children as string
      return /^\p{Unified_Ideograph}{2}$/u.test(text.trim())
    }
  }
  return false
})

const buttonStyle = useButtonCustomStyle(props)

const handleClick = (evt: MouseEvent) => {
  if (props.nativeType === 'reset') {
    form?.resetFields()
  }
  emit('click', evt)
}

// 显式指定组件实例暴漏出去的属性
defineExpose({
  /** @description button html element */
  ref: _ref,
  /** @description button size */
  size: _size,
  /** @description button type */
  type: _type,
  /** @description button disabled */
  disabled: _disabled,
  /** @description whether adding space */
  shouldAddSpace,
})
</script>
  • 引入需要的工具 import...
  • defineProps/defineEmits/inject
  • 定义变量,ref/coputed
  • 定义方法,handleClick
  • defineExpose,显示暴漏属性
// element-plus写法
const props = defineProps(buttonProps)

// 平常写法
const props = defineProps({
  size: {
      type: String,
      values: ['', 'default', 'small', 'large'],
      required: false,
  },
  disabled: Boolean,
  type: {
    type: String,
    values: buttonTypes,
    default: '',
  },
  icon: {
    type: iconPropType,
    default: '',
  },
  nativeType: {
    type: String,
    values: buttonNativeTypes,
    default: 'button',
  },
  loading: Boolean,
  loadingIcon: {
    type: iconPropType,
    default: () => Loading,
  },
  plain: Boolean,
  autofocus: Boolean,
  round: Boolean,
  circle: Boolean,
  color: String,
  dark: Boolean,
  autoInsertSpace: {
    type: Boolean,
    default: undefined,
  },
})

Prop 校验

  • 类型校验 type: Number/ Boolean / Array / Object / Date / Function / Symbol
  • 必填项校验 required: true | false
  • 默认值 default
  • values 是什么?可接收的值?