手把手,学习element-plus源码。
今天来实现Form组件。
文章还是分为3个部分,第一部分先分析一下实现思路,第二部分阅读element-plus源码,第三部分手写实现一个组件。
实现思路
日常使用中,表单组件应该是最常见的。
先看一下element-plus上的一段简单示例。
<el-form
ref="formRef"
:model="numberValidateForm"
label-width="100px"
:rules="rules"
>
<el-form-item
label="age"
prop="age"
>
<el-input
v-model.number="numberValidateForm.age"
type="text"
autocomplete="off"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm(formRef)">Submit</el-button>
<el-button @click="resetForm(formRef)">Reset</el-button>
</el-form-item>
</el-form>
一个完整的表单组件主要包含3部分,分别是el-form,el-form-item,el-input。
1.el-form
el-form组件,model接收数据源,rules接收校验规则。(rules也可以直接绑定在el-form-item单个组件上使用)
并且el-form组件会暴露出一个validate方法,我们通过formRef.value去进行整个表单的校验。校验的时候,需要拿到el-form内部的所有el-form-item组件,并判断有没有prop属性,如果有,则进行校验。
2.el-form-item
el-form-item组件接收label,跟prop,如果有prop,则会找对应的规则进行校验。(自身的rules字段,或者是el-form组件上的rules根据prop匹配找规则)
校验如果不通过,输入框变红色,然后出一个提示文字。
3.el-input
el-input 严格来说并不属于form组件,但是input也需要做校验,当值改变的时候,需要通知他父级上的el-form-item做validate校验。
这里的一个难点在于el-form如何拿到子节点的el-form-item对象,以及el-input如何通知父级的el-form-item进行校验。
在vue中可以通过eventBus的方式去做,那么在vue3中应该怎么做呢?
阅读源码
源码的阅读方式,以及打开方式,这里就不赘述了,我们直接看源码部分。
1.Form.vue
template部分,直接接收一个默认插槽。
ts部分,则是定义了props,emit以及一些变量跟函数。
代码继续往下看,有一个provide(上下文)。
把Form组件的props,以及一些方法传入子孙节点。
然后再看一下Form组件的validate方法。
这些都是validate方法,主要是处理不同的情况,比如校验特定字段。
核心方法是doValidateField。
const doValidateField = async (
props: Arrayable<FormItemProp> = []
): Promise<boolean> => {
if (!isValidatable.value) return false
const fields = obtainValidateFields(props)
if (fields.length === 0) return true
let validationErrors: ValidateFieldsError = {}
for (const field of fields) {
try {
await field.validate('')
} catch (fields) {
validationErrors = {
...validationErrors,
...(fields as ValidateFieldsError),
}
}
}
if (Object.keys(validationErrors).length === 0) return true
return Promise.reject(validationErrors)
}
const obtainValidateFields = (props: Arrayable<FormItemProp>) => {
if (fields.length === 0) return []
const filteredFields = filterFields(fields, props)
if (!filteredFields.length) {
debugWarn(COMPONENT_NAME, 'please pass correct props!')
return []
}
return filteredFields
}
通过计算传入的props会生成一个fields数组,然后遍历这个数组,依次触发validate函数。
而这个fields数组则是计算fields得到的。(fields在组件开始之前,定义为空数组)
Form组件先看到这,记住两件事: 1.provide传进去了一些属性和函数 2.组件校验实际是遍历fields,依次执行validate函数
2.form-item
跟Form对应,我们先看inject,接收上下文。
然后再看一下onMounted事件。
const context: FormItemContext = reactive({
...toRefs(props),
$el: formItemRef,
size: _size,
validateState,
labelId,
inputIds,
isGroup,
hasLabel,
addInputId,
removeInputId,
resetField,
clearValidate,
validate,
})
provide(formItemContextKey, context)
onMounted(() => {
if (props.prop) {
formContext?.addField(context)
initialValue = clone(fieldValue.value)
}
})
onMounted的时候,直接调用父级传过来的addField方法,收集form-item组件实例。
也就是说,Form组件通过provide传进去的方法,会把子孙节点的form-item组件实例都收集起来。
这也就解决了,form组件拿form-item组件数据的问题。
3.input
input组件也是类似,通过inject接收form-item的数据,当值改变时,则通知form-item进行校验。
手写源码
源码内容比较多,可以点击查看,已实现一个简易版本的Form表单,可进行提交以及规则校验。
实现思路均为源码阅读分析内容,在此基础上加了样式,以及其他一些基础功能的实现。
额外需要说明的一点就是——规则校验的时候,引入了async-validator校验库进行规则校验。
相应的说明可以查看官方文档。(这个我也是一边看,一边配置,总体来说还算是简单的)
如果这篇文章对你有所收获,欢迎点赞评论。