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
代表block
,m
代表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 是什么?可接收的值?