手把手,仿element-plus实现Form组件

1,344 阅读3分钟

手把手,学习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。

image.png

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以及一些变量跟函数。

image.png

代码继续往下看,有一个provide(上下文)。

image.png

把Form组件的props,以及一些方法传入子孙节点。

然后再看一下Form组件的validate方法。

image.png

这些都是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,接收上下文。

image.png

然后再看一下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进行校验。

image.png

image.png

手写源码

源码内容比较多,可以点击查看,已实现一个简易版本的Form表单,可进行提交以及规则校验。

实现思路均为源码阅读分析内容,在此基础上加了样式,以及其他一些基础功能的实现。

额外需要说明的一点就是——规则校验的时候,引入了async-validator校验库进行规则校验。

相应的说明可以查看官方文档。(这个我也是一边看,一边配置,总体来说还算是简单的)


如果这篇文章对你有所收获,欢迎点赞评论。