深入Element Plus:让自定义组件继承表单的 size 属性
前言
最近在开发一个较为复杂的表单页面时,我遇到一个场景:页面中包含了一些自定义封装的组件,需要让这些组件的 size 属性(用于控制尺寸)能够自动继承自父级 el-form 组件的设置。
我们知道,在 Element Plus 中,只要为 <el-form> 组件设置了 size 属性(如 large 或 small),其内部的 el-input、el-button 等官方组件都能自动继承这个尺寸。 但对于我们自己开发的组件,例如一个自定义的 <Icon> 组件,它默认是无法感知到 el-form 的 size 设置的。
那么,如何才能让我们的自定义组件也具备这种“继承”能力,与整个表单风格保持统一呢?
实现原理:探索 Element Plus 的源码
通过阅读 Element Plus 的源码,我发现其内部是借助 Vue 3 的 provide 和 inject API 来实现跨组件通信和属性继承的。 这种机制允许祖先组件向其所有后代组件注入依赖,无论层级有多深。
1. el-form 的数据提供 (Provide)
el-form 组件作为“提供者”,会将其自身的上下文(包含 props、事件发射器和各种内部方法)通过 provide 函数向下传递。这个上下文被一个名为 formContextKey 的唯一 InjectionKey 标记。
// Element Plus 源码片段:el-form 提供上下文
import { formContextKey } from './constants'
// ...
provide(
formContextKey,
reactive({
...toRefs(props), // 包含了 size, disabled 等属性
emit,
// ... 其他方法
resetFields,
clearValidate,
validateField,
})
)2. 子组件的数据注入 (Inject)
Element Plus 的表单子组件(如 el-input)则通过 inject 来获取这个上下文。官方为了逻辑复用,将其封装成了一个 useFormSize Hook。
这个 Hook 的逻辑非常严谨,它会按照 自身 props.size > el-form-item 的 size > el-form 的 size > 全局配置的 size 的优先级来确定最终的组件尺寸。
// Element Plus 源码片段:useFormSize Hook
import { formContextKey, formItemContextKey } from './constants'
import { useGlobalSize } from '@element-plus/hooks'
export const useFormSize = (...) => {
const size = useProp<ComponentSize>('size')
const globalConfig = useGlobalSize()
const form = inject(formContextKey, undefined)
const formItem = inject(formItemContextKey, undefined)
return computed(
(): ComponentSize =>
// 优先级从高到低
size.value || // 1. 组件自身的 size 属性
formItem?.size || // 2. form-item 的 size 属性
form?.size || // 3. form 的 size 属性
globalConfig.value || // 4. 全局配置
''
)
}实践教程:在自定义组件中实现继承
理解了原理之后,我们就可以在自己的组件中轻松实现相同的效果。我们不需要像官方 useFormSize Hook 那样处理复杂的优先级,通常只需要获取 el-form 的上下文即可。
下面是一个简单的实现步骤:
1. 导入 formContextKey
首先,从 element-plus 中导入 formContextKey。这是一个 InjectionKey,可以确保类型安全。
2. 定义组件 props
在自定义组件中,我们仍然需要定义自己的 size prop,以允许用户进行强制覆盖。
3. 注入并计算最终尺寸
使用 inject 获取 el-form 的上下文,并创建一个计算属性 componentSize。这个计算属性的逻辑是:
- 如果用户为当前组件明确传入了
sizeprop,则优先使用该 prop。 - 否则,尝试从注入的
formContext中获取size。 - 如果两者都不存在,则回退到一个默认值(如
'default')。
完整代码示例 (MyIcon.vue)
<script setup lang="ts">
import { computed, inject } from 'vue';
import { formContextKey } from 'element-plus';
import type { ComponentSize } from 'element-plus';
// 1. 定义 props,允许用户覆盖
const props = defineProps<{
size?: ComponentSize;
}>();
// 2. 注入来自 el-form 的上下文
const formContext = inject(formContextKey, undefined);
// 3. 计算最终生效的 size
// 优先级:props.size > formContext.size > 'default'
const componentSize = computed<ComponentSize>(
() => props.size ?? formContext?.size ?? 'default'
);
// 你可以在模板中根据 componentSize 的值来应用不同的样式
// 例如::style="{ fontSize: componentSize === 'large' ? '24px' : '16px' }"
</script>
<template>
<i :class="`my-icon--${componentSize}`">
<!-- 图标内容 -->
...
</i>
</template>
<style>
.my-icon--large {
font-size: 20px;
}
.my-icon--default {
font-size: 16px;
}
.my-icon--small {
font-size: 12px;
}
</style>通过以上方式,我们的自定义组件就“学会”了如何继承 el-form 的 size 属性,实现了与 Element Plus 生态更深度、更一致的集成。
总结
通过 provide 和 inject,我们可以安全地访问 Element Plus 内部组件的上下文,实现自定义组件与官方组件的无缝协作。这种方法不仅限于 size 属性,同样适用于 disabled 等其他可以在 el-form 上统一设置的属性,极大地提高了自定义表单组件的可复用性和一致性。
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据