先看看el-form组件的简单使用示例:
js
<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>
其中:
- el-form组件 model接收数据源,rules接收校验规则。 form组件暴露一个validate方法 对formRef.value 对表单进行校验,校验时拿到el-form-item组件,并判断有没有props属性,有则校验。
- el-form-item 组件接收label,prop 有prop则会找对应的规则(form-item 绑定的rules或者是 form上的rules找到对应的props项)校验不通过显示错误信息
- el-input组件是单独的组件,el-form-item组件是留出了插槽放置 input,select等组件;在el-form中需对值进行校验时通知父元素 form-item做validate校验
分析源码
文件目录
js
├── packages
│ ├── components
│ │ ├── form
│ │ │ ├── __tests__ # 测试目录
│ │ │ ├── src # 组件入口目录
│ │ │ │ ├──hooks # 抽离方法
│ │ │ │ │ ├──index.ts # use-form-common-props,use-form-item 方法总出口
│ │ │ │ │ ├──use-form-common-props.ts # 公共方法useFormSize,处理布局inline或label
│ │ │ │ │ └── use-form-item.ts # 定义处理inputId的方法确保input与表单模型(`model`)以及验证规则(`rules`)正确关联
│ │ │ │ ├── constants.ts # 自定义共享的数据类型
│ │ │ │ └── form-item.ts # form-item组件属性与ts类型
│ │ │ │ └──form-item.vue # form-item组件模板内容
│ │ │ │ └── form-label-wrap.tsx #
│ │ │ │ └── form.ts #
│ │ │ │ └── form.vue #常量申明
│ │ │ │ └── types.ts #
│ │ │ │ └── utils.ts #组件公共方法
│ │ │ ├── style # 组件样式目录
│ │ │ └── index.ts # 组件入口文件
│ │ └── package.json
hooks(use-form-common-props.ts,use-form-item.ts )
use-form-common-props.ts 源码: 主要包括:useFormSize方法(处理布局inline或label位置),useFormDisabled方法(处理item元素的可用性)
js
export const useFormSize = (
fallback?: MaybeRef<ComponentSize | undefined>,
ignore: Partial<Record<'prop' | 'form' | 'formItem' | 'global', boolean>> = {}
) => {
const emptyRef = ref(undefined)
const size = ignore.prop ? emptyRef : useProp<ComponentSize>('size')
const globalConfig = ignore.global ? emptyRef : useGlobalSize()
const form = ignore.form
? { size: undefined }
: inject(formContextKey, undefined)
const formItem = ignore.formItem
? { size: undefined }
: inject(formItemContextKey, undefined)
return computed(
(): ComponentSize =>
size.value ||
unref(fallback) ||
formItem?.size ||
form?.size ||
globalConfig.value ||
''
)
}
export const useFormDisabled = (fallback?: MaybeRef<boolean | undefined>) => {
const disabled = useProp<boolean>('disabled')
const form = inject(formContextKey, undefined)
return computed(
() => disabled.value || unref(fallback) || form?.disabled || false
)
}
use-form-item.ts源码中主要处理input等form-item 内部元素的id方法,确保input与表单模型(model
)以及验证规则(rules
)正确关联
js
// Generate id for ElFormItem label if not provided as prop
// 加入接收参数没有id 则自动为其生成一个
onMounted(() => {
idUnwatch = watch(
[toRef(props, 'id'), disableIdGeneration] as any,
// 通过 disableIdGeneration 减少不必要的 ID 生成,提升渲染效率
([id, disableIdGeneration]: [string, boolean]) => {
const newId = id ?? (!disableIdGeneration ? useId().value : undefined)
if (newId !== inputId.value) {
if (formItemContext?.removeInputId) {
inputId.value && formItemContext.removeInputId(inputId.value)
if (!disableIdManagement?.value && !disableIdGeneration && newId) {
formItemContext.addInputId(newId)
}
}
inputId.value = newId
}
},
{ immediate: true }
)
})
constants.ts
定义抛出可共享的上下文
js
import type { InjectionKey } from 'vue' //创建响应式注入键类型
import type { FormContext, FormItemContext } from './types'
// 在组件间共享表单的上下文(例如表单验证规则、表单数据等)
// 定义一个类型 FormContext 来描述你想共享的数据结构。然后,你可以使用 Symbol 创建一个 InjectionKey
export const formContextKey: InjectionKey<FormContext> =
Symbol('formContextKey')
export const formItemContextKey: InjectionKey<FormItemContext> =
Symbol('formItemContextKey')
form-item.ts
主要定义form-item 组件用到的 属性类型 包括prop,rules,validateStatus(验证状态),inlineMessage,showMessage(错误信息)
form-item.vue
el-form-item 定义组件模板,与组件的一系列方法如:clearValidate,resetField,validate ,addInputId 等,并抛出部分属性(validateState,validateMessage,size)和方法(validate,clearValidate,resetField)
js
<template>
<div ref="formItemRef" :class="formItemClasses" :role="isGroup ? 'group' : undefined"
:aria-labelledby="isGroup ? labelId : undefined">
//label
<form-label-wrap :is-auto-width="labelStyle.width === 'auto'" :update-all="formContext?.labelWidth === 'auto'">
<component :is="labelFor ? 'label' : 'div'" v-if="hasLabel" :id="labelId" :for="labelFor" :class="ns.e('label')"
:style="labelStyle">
<slot name="label" :label="currentLabel">
{{ currentLabel }}
</slot>
</component>
</form-label-wrap>
<div :class="ns.e('content')" :style="contentStyle">
//留出插槽
<slot />
//错误提示
<transition-group :name="`${ns.namespace.value}-zoom-in-top`">
<slot v-if="shouldShowError" name="error" :error="validateMessage">
<div :class="validateClasses">
{{ validateMessage }}
</div>
</slot>
</transition-group>
</div>
</div>
</template>
****
onMounted的时候,直接调用父级传过来的addField方法,收集form-item组件实例。
Form组件通过provide传进去的方法,会把子孙节点的form-item组件实例都收集起来。
解决了form组件拿form-item组件数据的问题
onMounted(() => {
if (props.prop) {
formContext?.addField(context)
initialValue = clone(fieldValue.value)
}
})
***
***
//抛出属性与
defineExpose({
size: _size,
validateMessage,
validateState,
validate,
clearValidate,
resetField,
})
form.vue
template 部分直接接收一个默认插槽
js
<template>
<!-- template 部分直接接收一个默认插槽 -->
<form :class="formClasses">
<slot />
</form>
</template>
ts部分,则是定义了props,emit以及一些变量跟函数。 其中实现各form-item的值校验 的核心方法obtainValidateFields;通过对过滤得到带prop参数的item项进行循环执行validateField方法
js
import { filterFields, useFormLabelWidth } from './utils'
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
}
const validate = async (
callback?: FormValidateCallback
): FormValidationResult => validateField(undefined, callback)
// 通过计算传入的props会生成一个fields数组,然后遍历这个数组,依次触发validate函数。
// 而这个fields数组则是计算fields得到的
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('')
if (field.validateState === 'error') field.resetField()
} catch (fields) {
validationErrors = {
...validationErrors,
...(fields as ValidateFieldsError),
}
}
}
if (Object.keys(validationErrors).length === 0) return true
return Promise.reject(validationErrors)
}