听说你熟练使用UI组件库,那讲下Form表单的校验原理吧!

214 阅读2分钟

前言

为了避免再被问住,咱就简单实现下吧!
先看下效果图

表单校验.gif

基本原理

基本思路:
表单校验有两种情景:1、整个表单form校验;2、表单项中input触发校验。因此整个表单校验核心是formitem的实现。只需将formitem中的validate方法提供给form和input,其余的事情如校验表单项、展示错误信息交给formitem去实现就好了。

核心概念:
通过provide/inject(响应式)来实现祖孙间通信;父组件provide传递;子孙组件inject接收

  • test-form: 1、提供检验rules、model;2、收集表单项实例
  • test-form-item: 1、提供prop并绑定model中对应的字段 2、提供校验方法validate 3、展示错误信息
  • test-input: 值变化执行test-form-item中的校验方法

表单原理图.png

代码实现

Form

  • provide:props、addField
  • 职责:提供rules、model以及收集fields以供整个表单校验
<script setup>
import { provide, reactive,toRefs } from 'vue'
const props = defineProps(['model','rules'])
// 存储表单字段信息
let fields = reactive([])
const addField = (field) => {
  fields.push(field)
}
// 表单整体检验方法
const validate = async (callback) => {
  // 存储校验不通过信息
  let validationErrors = {}
  for (const field of fields) {
    try {
      await field.validate('')
    } catch (fields) {
      validationErrors = {
        ...validationErrors,
        ...fields
      }
    }
  }
  const result = Object.keys(validationErrors).length === 0
  if (result) {
    callback?.(result)
    return Promise.resolve([result,null])
  }
  callback?.(result,validationErrors)
  return Promise.resolve([result,validationErrors])
}
defineExpose({
  validate
})

provide('formContentKey',
  reactive({
    ...toRefs(props),
    addField
  })
)
  
</script>

<template>
  <form>
    <slot />
  </form>
</template>

FormItem

  • inject: form组件中的props、addField
  • provide:props、validate
  • 职责:检验表单项、维护错误状态;通过addField给form提供validate,通过provide给input传递validate
<script setup>
  import { provide, reactive, toRefs, onMounted, inject, ref, computed } from 'vue'
  const formContext = inject('formContentKey', undefined)
  const props = defineProps(['prop','required'])
  const validateState = ref('')
  const validateMessage = ref('')
  // 表单项检验
  const validate = async (trigger) => {
    validateState.value = ''
    const modelName = props.prop
    const rules = formContext?.rules[modelName]
    const fieldValue = formContext?.model[modelName]
    // 根据rules进行校验(仅模拟)
    rules.map(rule => {
      if (validateState.value == 'error') return
      // 匹配对应的触发条件
      if (!(!trigger || rule.trigger.includes(trigger))) return
      if (rule.required) {
        if (fieldValue == '') {
          validateState.value = 'error'
          validateMessage.value = rule.message
        }
      } else if (rule.max && fieldValue.length > rule.max) {
        validateState.value = 'error'
        validateMessage.value = rule.message
      } else {
        validateState.value = 'success'
        validateMessage.value = ''
      }
    })
    if (validateState.value == 'success') return true
    return Promise.reject({prop: modelName,validateState: validateState.value, validateMessage: validateMessage.value})
  }
  const showError = computed(() => validateState.value === 'error')

  const context = reactive({
    ...toRefs(props),
    validate,
    validateState,
  })


  provide('formItemContextKey', context)

  onMounted(() => {
    if (props.prop) {
      // 将当前的表单域添加到全局的表单中
      formContext?.addField(context)
    }
  })
</script>


<template>
  <div :class="[{ isError: showError}]">
    <slot />
    <div v-if="showError" class="error">{{ validateMessage }}</div>
  </div>
</template>

<style>
.error {
  color: red;
  font-size: 12px;
  position: absolute;
  bottom: -20px;
}
.isError input{
  border: 1px solid red;
}
</style>

Input

  • inject: 接收formitem的validate
  • 职责:数据有变化触发validate
<script setup>
import { reactive, inject, watch } from 'vue'
const formItemContext = inject('formItemContextKey', undefined)
const emits = defineEmits(['update:modelValue']);
const props = defineProps(['modelValue'])
const model = reactive({
  inputValue: props.modelValue,
});
watch(
  () => props.modelValue,
  v => {
    model.inputValue = v;
  },
);
const onChange = (e) => {
  emits('update:modelValue', e.target.value);
  formItemContext.validate('change').catch(err => {});
}
</script>

<template>
  <input @input="onChange" :value="model.inputValue" />
</template>

<style scoped>
input {
  outline: 0;
}
</style>

Demo

<script setup>
import TestForm from './components/TestForm.vue'
import TestFormItem from './components/TestFormItem.vue'
import TestInput from './components/TestInput.vue'
import { reactive, ref } from 'vue'
const model = reactive({ input: 'too hot' })
const rules = reactive({
  input: [
    { required: true, message: '请输入内容', trigger: 'change' },
    { max: 5, message: '最长 5 个字符', trigger: 'change' },
  ],
})
const formRef = ref()
const submit = async () => {
  const [valid,fields] = await formRef.value.validate()
  if (valid) alert('验证通过')
  // formRef.value.validate((valid,fields)=>{
  //   if (valid) alert('验证通过')
  // })
}
</script>

<template>
  <div>
    <TestForm ref="formRef" :model="model" :rules=rules>
      <TestFormItem label="输入框" prop="input">
        <TestInput v-model="model.input" />
      </TestFormItem>
    </TestForm>
    <button style="margin-top: 20px" @click="submit">提交</button>
  </div>
</template>

参考文档

element-plus: Element Plus for Vue 3 桌面端组件库 (gitee.com)