Element-Plus源码分析--form组件

先看看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)
}
相关推荐
Bellafu6663 小时前
selenium对每种前端控件的操作,python举例
前端·python·selenium
littleboyck4 小时前
VSCode 全自动调试Vue/React项目
前端·visual studio code
洛小豆4 小时前
她问我::is-logged 是啥?我说:前面加冒号,就是 Vue 在发暗号
前端·vue.js·面试
我有一棵树4 小时前
前端开发中 SCSS 变量与 CSS 变量的区别与实践选择,—— 两种变量别混为一谈
前端·css·scss
white-persist5 小时前
JWT 漏洞全解析:从原理到实战
前端·网络·python·安全·web安全·网络安全·系统安全
IT_陈寒5 小时前
React 性能优化:5个实战技巧让首屏加载提升50%,开发者亲测有效!
前端·人工智能·后端
rising start5 小时前
前端基础一、HTML5
前端·html·html5
鬼谷中妖6 小时前
JavaScript 循环与对象:深入理解 for、for...in、for...of、不可枚举属性与可迭代对象
前端
大厂码农老A6 小时前
你打的日志,正在拖垮你的系统:从P4小白到P7专家都是怎么打日志的?
java·前端·后端