不一样的表单校验

3,088 阅读5分钟

常见的校验方式

当用户未按要求填写表单,点击进入下一步或者提交表单的时候,给用户进行错误提示。 如下图所示:

2022-06-19 15.24.47.gif

这种方式,大家应该都是比较熟悉了。只要对需要校验的字段写好校验规则。当点击 Submit 按钮时,触发表单的全局校验就行了。

少见的校验方式

本文主题

如果是下面这种校验方式,又该怎么办呢?

2022-06-19 15.30.33.gif

只有用户按要求输入后,下一步/提交的按钮才会高亮,才能放行。

!这里允许给大家一首歌的时间去思考 🤔

背景

这篇文章的由来,也是因为遇到了这样的需求。对比了几个国内和国外的 UI 组件库之后,都找不到解决方案,也不知道怎么去搜索这种校验方式的解决方案(至今为止也不知道 😂,蠢哭)

大多数的 UI 组件库,对表单实例对象都只提供了全局校验的方法。一调用就把所有需要校验的字段都给校验了一遍,这没毛病吖。但关键是同时把错误信息给提示出来了。

当存在多个为必填的字段信息,假如从第一个开始就填错了,但一下子给用户提示了所有的错误信息。这真是不单只把用户给整不会了,把我也整不会了 😂

解决方案

想了好久好久,终于想到了一个相对较好的解决方案,但就是需要写多亿点点的代码 🙅🏻‍♀️

解决方案如下:

  1. 手动编写校验器 validator
  2. 收集字段的校验状态, 对每个字段校验的过程进行管控
  3. 每个字段触发校验后,都对所有的字段进行过滤
  4. 如果发现存在状态为错误的字段,则停止校验

既然要管控字段的校验过程, 那 UI 组件 Form 组件提供的内置 Rules 规则铁定是不能进行单纯地写校验规则就完事儿了。而是使用 validator 代替校验规则,即自己写一个校验器。

当遇到手写的时候,千万不要恐惧!!! 希望大家能坚持看到最后

逻辑代码

注册校验函数

实现的功能

  • 收集字段状态
  • 修改字段状态
  • 触发校验函数
/** registerValid.ts **/

import { isFunction } from 'lodash-es'

type OptionCallback = () => void | Promise<any>

interface ValidCallback<T extends string> {
    // 启动校验
    launchValid: () => void
    // 获取校验对象
    getField: () => Record<T, boolean>
    // 修改字段状态,同时启动校验,即调用 launchValid 函数
    valid: (field: T, value: boolean) => void
    // 创建单个字段的校验修改器
    createCollector: (field: T) => [
        () => void,
        () => void
    ]
}

interface RegisterValid {
    <EE extends string = string>(
        initialValue: EE[] | Record<EE, boolean>,
        option: { success: OptionCallback, fail: OptionCallback }
    ): ValidCallback<EE>
}

/**
 * 注册校验函数
 * @param {any} keys  需要校验的字段 | 字段初始化状态
 * @param {any} option  成功校验函数和失败校验函数
 * @returns {any}
 */
const registerValid: RegisterValid = (keys, option) => {
    let validObj: any = keys
    if (isArray(keys)) {
        validObj = keys.reduce((prev, cur) => {
            Reflect.set(prev, cur, false)
            return prev
        }, {})
    }

    const launchValid = () => {
        let callback: any = null
        const validValues = Object.values(validObj)
        const result = validValues.filter(Boolean)
        // 结果数组 !== 所有字段的数量
        // 说明没有全部通过
        callback = result.length !== validValues.length ? option.fail : option.success
        callback && isFunction(callback) && callback()
    }

    return {
        launchValid,
        getField: () => validObj,
        valid (field, value) {
            Reflect.set(validObj, field, value)
            launchValid()
        },
        createCollector (field) {
            const errorFn = () => this.valid(field, false)
            const passFn = () => this.valid(field, true)
            return [errorFn, passFn]
        }
    }
}

export default registerValid

使用方法(假如现在要校验 name 与 post 字段)

import registerValid from '@/folder/registerValid'

// 模拟表单的校验状态
let validateStatus = false

// 假如现在需要校验账户和密码

// 1. 收集字段状态
// 初始化时 name 和 post 的字段的初始状态都为 false
const validObj = registerValid(['name', 'post'], {
    success() {
        // 当所有字段都通过校验后,就会触发该函数
        validateStatus = true
    },
    fail() {
        // 只要有一个字段出错,就会触发该函数
        validateStatus = false
    }
})

// 如果 name 拥有默认值,那么 name 的初始化状态应该为 true,而不是 false
// 在创建时可以传入一个对象
/**
    const validObj = registerValid({ name: true, post: false }, {
        success() {
            // 当所有字段都通过校验后,就会触发该函数
            validateStatus = true
        },
        fail() {
            // 只要有一个字段出错,就会触发该函数
            validateStatus = false
        }
    })
**/

// 2. 修改字段状态
// 修改 name 字段的状态为 true
// 内部同时会触发 validObj.launchValid 函数
validObj.valid('name', true)

// 3. 触发校验函数
// 获取当前所有字段的状态, 返回值为一个对象
const statusObj = validObj.getField()

应用到实际的表单校验中去

// 使用普通的校验规则方式
const rules = {
    name: {
        required: true, message: 'name 字段为必填'
    },
    post: {
        ...
    }
}

// 使用校验收集的方式
const rules = {
    name: { validator: nameValidator },
    post: { validator: postValidator }
}

function nameValidator() {}

function postValidator() {}

自定义校验器写好了,但是一些像 required, 数据类型,最大值,最小值这些非定制化的校验规则在实际的业务开发中也手写的话,时间成本也太高了。这里第一时间想到的是使用和 UI 组件库绑定的一些规则校验器,比如说 element-ui(async-validate), arco-design(b-validate)等等等.

b-validator 为例,对其进行封装:

注意:这不是一个 hook, 只是实在不知道该怎么命名了

b-validate 的规则请参考它的文档

/** useBv.ts **/

import { Schema, SchemaType, SchemaRuleType } from 'b-validate/es'

/**
 * 使用 bv 校验器
 * @param {string} value
 * @param {SchemaRuleType[]} rules
 * @returns {any}
 */
const useBv = (value: string, rules: SchemaRuleType[]): Promise<string | null> => {
    return new Promise((resolve) => {
        const schemaRule: SchemaType = {
            bv: rules
        }
        const schema = new Schema(schemaRule)

        schema.validate({ bv: value }, (errors: any) => {
            resolve(errors != null ? errors.bv.message : errors)
        })
    })
}

export default useBv

它所完成的事有:

  1. 对传入的 value,根据 rules 规则进行校验
  2. 以 Promise 的形式返回校验结果,如果校验通过返回的结果为 undefined

完整的一个校验 demo

import registerValid from '@/folder/registerValid'
import useBv from '@/folder/useBv'

// 表单的校验状态
const formValidateStatus = false
// 表达初始化的值
const form = {
    name: '',
    post: ''
}
// 校验规则
const rules = {
    name: { validator: nameValidator },
    post: { validator: postValidator }
}

// 创建状态收集器
const validObj = registerValid(['name', 'post'], {
    success() { formValidateStatus = true },
    fail() { formValidateStatus = false }
})

// 自定义 name 校验器
async function nameValidator(value: string, callback: (error?: any) => void) {
    const [error, pass] = validObj.createCollector('name')
    const result = await useBv(value, [
        { type: 'string', required: true, message: 'name 为必填项' }
    ])
    
    if (result) {
        error()
        return callback(result)
    }
    
    pass()
}

// 自定义 post 校验器
function postValidator(value: string, callback: (error?: any) => void) {
    const [error, pass] = validObj.createCollector('post')
    const result = await useBv(value, [
        { type: 'string', required: true, message: 'post 为必填项' }
    ])
    
    if (result) {
        error()
        return callback(result)
    }
    
    pass()
}

在线 Demo

点击这里👈🏻