可能开发者平时使用验证表单控件感觉挺便携方便的,那是因为在整个表单内部,已经为开发者做了大部分的‘脏活’,‘累活’,一个完整验证表单体系实际是很复杂的,整个流程可以分为,状态收集 ,状态管理 ,状态验证 , 状态下发 ,等诸多环节,所以在开发一套受宠于大众的表单控件,首先每一个环节设计是蛮重要的。接下来首先介绍一下,如何设计一套表单系统。
在设计之前,拿 antd 为例子,看一下一个基本的表单长什么样子 ( 可以称之为 Demo1 ) :
<Form onFinish={onFinish} >
<FormItem name="name" label="小册名称" >
<Input />
</FormItem>
<FormItem name="author" label="小册作者" >
<Input />
</FormItem>
<Button htmlType="submit" >确定</Button>
</Form>如上,一套表单系统分为 Form ,FormItem ,表单控件三部分构成,下面一一介绍三个部分作用以及应该如何设计。
Form 组件定位以及设计原则:
Form 的作用,管理整个表单的状态,这个状态包括具体表单控件的 value,以及获取表单,提交表单,重置表单,验证表单等方法。Form 不仅仅要管理状态,而且还要下发传递这些状态。把这些状态下发给每一个 FormItem ,由于考虑到 Form 和 FormItem 有可能深层次的嵌套,所以选择通过 React context 保存下发状态最佳。Form 满足上述两点功能之外,还要和原生的 form 的功能保持一致性。FormItem组件定位以及设计原则:
FormItem 包裹的组件,变成受控的,一方面能够自由传递值 value 给表单控件,另一方面,能够劫持表单控件的 change 事件,得到最新的 value ,上传状态。FormItem 还有一个作用就是要提供表单单元项的标签 label ,如果校验不通过的情况下,需要展示错误信息 UI 样式。表单控件设计(比如 Input ,Select 等):
FormItem 处理。保存信息: 首先最直接的是,需要保存表单的属性名 name ,和当前的属性值 value ,除此之外还要保存当前表单的验证规则 rule ,验证的提示文案 message ,以及验证状态 status。
我这里收到 Promise 的启发,引用了三种状态:
数据结构:
上面介绍了表单状态层保存的信息。接下来用什么数据结构保留这些信息。
/*
TODO: 数据结构
model = {
[name] -> validate = {
value -> 表单值 (可以重新设定)
rule -> 验证规则 ( 可以重新设定)
required -> 是否必添 -> 在含有 rule 的情况下默认为 true
message -> 提示消息
status -> 验证状态 resolve -> 成功状态 |reject -> 失败状态 | pending -> 待验证状态 |
}
}
*/model 为整个 Form 表单的数据层结构。name 为键,对应 FormItem 的每一个 name 属性,validate 为 name 属性对应的值,保存当前的表单信息,包括上面说到那几个重要信息。打个比方:上述 Demo1 中,最后存在 form 的数据结构如下所示:
model = {
name :{ /* 小册名称 formItem */
value: ...
rule:...
required:...
message:...
status:...
},
author:{ /* 小册作者 formItem */
value: ...
rule:...
required:...
message:...
status:...
}
}上面说到了整个表单的状态层,那么状态层保存在哪里呢 ?
状态层最佳选择就是保存在 Form 内部,可以通过 useForm 一个自定义 hooks 来维护和管理表单状态实例 FormStore 。
整个表单系统数据通信,还是从改变状态,触发校验两个方向入手。
当系统中一个控件比如 Input 值改变的时候,①可以是触发了 onChange 方法,首先由于 FormItem 控制表单控件,所以 FormItem 会最先感知到最新的 value ,②并通知给 Form 中的表单管理 FormStore , ③ FormStore 会更新状态,④ 然后把最新状态下发到对应的 FormItem ,⑤FormItem 接收到任务,再让 Input 更新最新的值,视觉感受 Input 框会发生变化 ,完成受控组件状态改变流程。
比如触发上述 Demo1 中 name 对应的 Input,其内部流程就是上文 ①~⑤ 描述的顺序。
表单校验有两种情况:
第一种: 可能是给 FormItem 绑定的校验事件触发,比如 onBlur 事件触发 ,而引起的对单一表单的校验。流程和上述改变状态相同类似。
第二种: 有可能是提交事件触发,或者手动触发校验事件,引起的整个表单的校验。流程首先触发 submit 事件,①然后通知给 Form 中 FormStore,② FormStore 会对整个表单进行校验,③然后把每个表单的状态,异步并批量下发到每一个 FormItem ,④ FormItem 就可以展示验证结果。
比如触发 Demo1 中的提交按钮,其流程就是上文 ①~④ 描述的顺序。
整个表单验证系统的设计阶段,从几个角度介绍了系统设计,当然其中还有很多没有提及细节,会在实现环节详细讲解,接下来就是具体功能的实现环节。
FormStore 是整个表单验证系统最核心的功能了,里面包括保存的表单状态 model, 以及管理这些状态的方法,这些方法有的是对外暴露的,开发者可以通过调用这些对外的 api 实现提交表单,校验表单 , 重置表单 等功能。 参考和对标 antd 的表单实现(早期版本本质上是 rc-form),罗列的方法如下。
以下接口提供给开发者使用。
| 对外接口名称 | 作用 | 参数说明 |
|---|---|---|
| submit | 提交表单 | 一个参数 cb,校验完表单执行,通过校验 cb 的参数为表单数据层,未通过校验 cb 参数为 false |
| resetFields | 重置表单 | 无参数 |
| setFields | 设置一组表单值 | 一个参数为 object, key ——为表单名称,value ——表单项,可以是值,校验规则,校验文案 ,例如 setFields({ name: { value : '《React进阶实践指南》' , author:'我不是外星人' } , }) 或者 setFields({ name:'《React进阶实践指南》', author:'我不是外星人' }) |
| setFieldsValue | 设置单一表单值 | 二个参数,第一个参数为 name 表单项名称,第二个参数 value ,设置表单的值 , 例如 setFieldsValue('author','我不是外星人') |
| getFieldValue | 获取对应字段名的值 | 一个参数,对应的表单项名称,例如 getFieldValue('name') |
| getFieldsValue | 获取整个表单的value | 无参数 |
| validateFields | 验证整个表单层 | 一个参数,回调函数,回调函数,参数为验证结果, |
以下接口提供给 Form 和 FormItem 使用。
| 接口名称 | 作用 | 参数说明 |
|---|---|---|
| setCallback | 注册绑定在 Form 上的事件 , 比如 onFinish | onFinishFailed | 一个参数,为一个对象,存放需要注册的事件。 |
| dispatch | 可以通过 dispatch 调用 FormStore 内部的方法 | 第一个参数是一个对象,里面 type 为调用的方法,其余参数依次为调用方法的参数。 |
| registerValidateFields | FormItem 注册表单单元项 | 三个参数,第一个参数单元项名称,第二个参数为,FormItem 的控制器,可以让 FormItem 触发更新,第三个参数,为注册的内容,比如 rule,message 等 |
| unRegisterValidate | 解绑注册的表单单元项 | 一个参数,表单单元项名称 |
上述介绍了需要完成的对外接口,接下来介绍一下 FormStore 保存的重要属性。
model : 首先 model 为整个表单状态层的核心,绑定单元项的内容都存在 model 中,上述已经介绍了。control :control 存放了每一个 FormItem 的更新函数,因为表单状态改变,Form 需要把状态下发到每一个需要更新的 FormItem 上。callback: callback 存放表单状态改变的监听函数。penddingValidateQueue:由于表单验证状态的下发是采用异步的,验证状态的更新任务会先缓存在该队列中,等待统一批量执行。接下来就是具体的代码实现和流程分析。
/* 对外接口 */
const formInstanceApi = [
'setCallback',
'dispatch',
'registerValidateFields',
'resetFields',
'setFields',
'setFieldsValue',
'getFieldsValue',
'getFieldValue',
'validateFields',
'submit',
'unRegisterValidate'
]
/* 判断是否是正则表达式 */
const isReg = (value) => value instanceof RegExp
class FormStore{
constructor(forceUpdate,defaultFormValue={}){
this.FormUpdate = forceUpdate /* 为 Form 的更新函数,目前没有用到 */
this.model = {} /* 表单状态层 */
this.control = {} /* 控制每个 formItem 的控制器 */
this.isSchedule = false /* 开启调度 */
this.callback = {} /* 存放监听函数 callback */
this.penddingValidateQueue = [] /* 批量更新队列 */
this.defaultFormValue = defaultFormValue /* 表单初始化的值 */
}
/* 提供操作form的方法 */
getForm(){
return formInstanceApi.reduce((map,item) => {
map[item] = this[item].bind(this)
return map
} ,{})
}
/* 创建一个验证模块 */
static createValidate(validate){
const { value, rule, required, message } = validate
return {
value,
rule: rule || (() => true),
required: required || false,
message: message || '',
status:'pending'
}
}
/* 处理回调函数 */
setCallback(callback){
if(callback) this.callback = callback
}
/* 触发事件 */
dispatch(action,...arg){
if(!action || typeof action !== 'object') return null
const { type } = action
if(~formInstanceApi.indexOf(type)){
return this[type](...arg)
}else if(typeof this[type] === 'function' ){
return this[type](...arg)
}
}
/* 注册表单单元项 */
registerValidateFields(name,control,model){
if(this.defaultFormValue[name]) model.value = this.defaultFormValue[name] /* 如果存在默认值的情况 */
const validate = FormStore.createValidate(model)
this.model[name] = validate
this.control[name] = control
}
/* 卸载注册表单单元项 */
unRegisterValidate(name){
delete this.model[name]
delete this.control[name]
}
/* 通知对应FormItem更新 */
notifyChange(name){
const controller = this.control[name]
if(controller) controller?.changeValue()
}
/* 重置表单 */
resetFields(){
Object.keys(this.model).forEach(modelName => {
this.setValueClearStatus(this.model[modelName],modelName,null)
})
}
/* 设置一组字段状态 */
setFields(object){
if( typeof object !== 'object' ) return
Object.keys(object).forEach(modelName=>{
this.setFieldsValue(modelName,object[modelName])
})
}
/* 设置表单值 */
setFieldsValue(name,modelValue){
const model = this.model[name]
if(!model) return false
if(typeof modelValue === 'object' ){ /* 设置表单项 */
const { message ,rule , value } = modelValue
if(message) model.message = message
if(rule) model.rule = rule
if(value) model.value = value
model.status = 'pending' /* 设置待验证状态 */
this.validateFieldValue(name,true) /* 如果重新设置了验证规则,那么重新验证一次 */
}else {
this.setValueClearStatus(model,name,modelValue)
}
}
/* 复制并清空状态 */
setValueClearStatus(model,name,value){
model.value = value
model.status = 'pending'
this.notifyChange(name)
}
/* 获取表单数据层的值 */
getFieldsValue(){
const formData = {}
Object.keys(this.model).forEach(modelName=>{
formData[modelName] = this.model[modelName].value
})
return formData
}
/* 获取表单模型 */
getFieldModel(name){
const model = this.model[name]
return model ? model : {}
}
/* 获取对应字段名的值 */
getFieldValue(name){
const model = this.model[name]
if(!model && this.defaultFormValue[name]) return this.defaultFormValue[name] /* 没有注册,但是存在默认值的情况 */
return model ? model.value : null
}
/* 单一表单单元项验证 */
validateFieldValue(name,forceUpdate = false){
const model = this.model[name]
if(!model) return null
/* 记录上次状态 */
const lastStatus = model.status
const { required, rule , value } = model
let status = 'resolve'
if(required && !value ){
status = 'reject'
}
else if(isReg(rule)){ /* 正则校验规则 */
status = rule.test(value) ? 'resolve' : 'reject'
}else if(typeof rule === 'function'){ /* 自定义校验规则 */
status = rule(value) ? 'resolve' : 'reject'
}
model.status = status
if(lastStatus !== status || forceUpdate ){
const notify = this.notifyChange.bind(this,name)
this.penddingValidateQueue.push( notify )
}
this.scheduleValidate()
return status
}
/* 批量调度验证更新任务 */
scheduleValidate(){
if(this.isSchedule) return
this.isSchedule = true
Promise.resolve().then(()=>{
/* 批量更新验证任务 */
unstable_batchedUpdates(()=>{
do{
let notify = this.penddingValidateQueue.shift()
notify && notify() /* 触发更新 */
}while(this.penddingValidateQueue.length > 0)
this.isSchedule = false
})
})
}
/* 表单整体验证 */
validateFields(callback){
let status = true
Object.keys(this.model).forEach(modelName=>{
const modelStates = this.validateFieldValue(modelName,true)
if(modelStates==='reject') status = false
})
callback(status)
}
/* 提交表单 */
submit(cb){
this.validateFields((res)=>{
const { onFinish, onFinishFailed} = this.callback
cb && cb(res)
if(!res) onFinishFailed && typeof onFinishFailed === 'function' && onFinishFailed() /* 验证失败 */
onFinish && typeof onFinish === 'function' && onFinish( this.getFieldsValue() ) /* 验证成功 */
})
}
}流程分析:
初始化流程
constructor ,FormStore 通过 new 方式实例化。实例化过程中会绑定 model , control 等属性。
getForm: 这里思考一下问题,就是需不需要把整个 FormStore 全部向 Form 组件暴露出去,答案是肯定不能这么做,因为如果 FormStore 整个实例暴露出去,就可以获取内部的状态 model 和 control 等重要模块,如果篡改模块下的内容,那么后果无法想象的,所以对外提供的只是改变表单状态的接口。通过 getForm 把重要的 API 暴露出去就好。getForm 通过数组 reduce 把对外注册的接口数组 formInstanceApi 一一绑定 this 然后形成一个对象,传递给 form 组件。
setCallback : 这个函数做的事情很简单,就是注册 callback 事件。在表单的一些重要阶段,比如提交成功,提交失败的时候,执行这些回调函数。
表单注册流程
static createValidate: 静态方法——创建一个验证 Validate ,也就是 model 下的每一个模块,主要在注册表单单元项的时候使用。
registerValidateFields :注册表单单元项,这个在 FormItem 初始化时候调用,把验证信息,验证文案,等信息,通过 createValidate 注册到 model 中, 把 FormItem 的更新函数注册到 control 中。
unRegisterValidate:在 FormItem 的生命周期销毁阶段执行,解绑上面 registerValidateFields 注册的内容。
表单状态设置,获取,重置
notifyChange:每当给表单单元项 FormItem 重新赋值的时候,就会执行当前 FormItem 的更新函数,派发视图更新。(这里可以提前透露一下,control 存放的就是每个 FormItem 组件的 useState 方法 )
setValueClearStatus: 重新设置表单值,并重置待验证状态 pendding,然后触发 notifyChange 促使 FormItem 更新。
setFieldsValue:设置一个表单值, 如果重新设置了验证规则,那么重新验证一次,如果只是设置了表单项的值,调用 setValueClearStatus 更新。
setFields: 设置一组表单值,本质上对每一个单元项触发 setFieldsValue。
getFieldValue:获取对应 name 的表单值,本质上就是获取 model 下对应模块的 value 值。
getFieldsValue:获取整个表单的数据层(分别获取每一个模块下的 value )。
getFieldModel:获取表单的模型,这个 api 设计为了让 UI 显示验证成功或者失败的状态,以及提示的文案。
resetFields:本质上就是调用 setValueClearStatus 重新设置每一个表单单元项的状态。
表单验证
validateFieldValue:验证表单的单元项,通过判断规则,如果规则是正则表达式那么触发正则 test 方法,如果是自定义规则,那么执行函数,返回值布尔值判断是否通过校验。如果状态改变,把当前更新任务放在 penddingValidateQueue 待验证队列中。为什么采用异步校验更新呢? 首先验证状态改变,带来的视图更新,不是那么重要,可以先执行更高优先级的任务,还有一点就是整个验证功能,有可能在异步情况下,表单会有多个表单单元项,如果直接执行更新任务,可能会让表单更新多次,所以放入penddingValidateQueue 在配合 unstable_batchedUpdates批量更新,统一更新这些状态。
scheduleValidate:scheduleValidate 执行会开启 isSchedule = true开关,如果有多个验证任务,都会放入 penddingValidateQueue ,最后统一执行一次任务处理逻辑。调用 Promise.resolve()和 unstable_batchedUpdates 异步批量更新 ,批量更新完毕,关闭开关 this.isSchedule = false 。
注:React 18+ 在 Promise/setTimeout 等异步上下文中也已默认自动批处理,
unstable_batchedUpdates仅作为演示批量思路保留(该 API 自 React 18 起标记为废弃,在 React 19 的 react-dom 中仍可用,但无需再手动包裹)。
validateFields: validateFields 会对每一个表单单元项触发 validateFieldValue ,然后执行回调函数,回调函数参数,代表验证是否通过,如果有一个验证不通过,那么整体就不通过验证。
submit:submit 本质就是调用 validateFields 验证这个表单,然后在 validateFields 回调函数中,触发对应的监听方法 callback , 成功触发 onFinish, 失败调用 onFinishFailed ,这些方法都是绑定在 Form 的回调函数。
上面的 FormStore 就是通过自定义 hooks —— useForm 创建出来的。useForm 可以独立使用,创建一个 formInstance ,然后作为 form 属性赋值给 Form 表单。 如果没有传递 默认会在 Form 里通过 useForm 自动创建一个。(参考 antd,用法一致 )。
代码实现:
function useForm(form,defaultFormValue = {}){
const formRef = React.useRef(null)
const [, forceUpdate] = React.useState({})
if(!formRef.current){
if(form){
formRef.current = form /* 如果已经有 form,那么复用当前 form */
}else { /* 没有 form 创建一个 form */
const formStoreCurrent = new FormStore(forceUpdate,defaultFormValue)
/* 获取实例方法 */
formRef.current = formStoreCurrent.getForm()
}
}
return formRef.current
}useForm 的逻辑实际很简单:
getForm ,把重要的 api 暴露出去。本阶段主要学习内容如下:
接下来将完成 Form 和 FormItem 组件的编写。
通过下面的学习,你将收获以下知识点:
属性设定
| 属性名称 | 作用 | 类型 |
|---|---|---|
form | 传入useForm 创建的 FormStore实例 | FormStore 实例对象 |
onFinish | 表单提交成功调用 | function ,一个参数,为表单的数据层 |
onFinishFailed | 表单提交失败调用 | function ,一个参数,为表单的数据层 |
initialValues | 设置表单初始化的值 | object |
细节问题
Form 接收类似 onFinish | onFinishFailed 监听回调函数。
Form 可以被 ref 标记,ref 可以获取 FormStore 核心方法。
Form 要保留原生的 form 属性,当 submit 或者 reset 触发,自动校验/重置。
创建 context 保存 FormStore 核心 Api。
import { createContext } from 'react'
/* 创建一个 FormContext */
const FormContext = createContext()
export default FormContextFormStore 的核心 API 。接下来就是重点 Form 编写
function Form ({
form,
onFinish,
onFinishFailed,
initialValues,
children
},ref){
/* 创建 form 状态管理实例 */
const formInstance = useForm(form,initialValues)
/* 抽离属性 -> 抽离 dispatch | setCallback 这两个方法不能对外提供。 */
const { setCallback, dispatch ,...providerFormInstance } = formInstance
/* 向 form 中注册回调函数 */
setCallback({
onFinish,
onFinishFailed
})
/* Form 能够被 ref 标记,并操作实例。 */
useImperativeHandle(ref,() => providerFormInstance , [])
/* 传递 */
const RenderChildren = <FormContext.Provider value={formInstance} > {children} </FormContext.Provider>
return <form
onReset={(e)=>{
e.preventDefault()
e.stopPropagation()
formInstance.resetFields() /* 重置表单 */
}}
onSubmit={(e)=>{
e.preventDefault()
e.stopPropagation()
formInstance.submit() /* 提交表单 */
}}
>
{RenderChildren}
</form>
}
export default forwardRef(Form)Form 实现细节分析:
首先通过 useForm 创建一个 formInstance ,里面保存着操纵表单状态的方法,比如 getFieldValue , setFieldsValue 等。
从 formInstance 抽离出 setCallback ,dispatch 等方法,得到 providerFormInstance ,因为这些 api 不期望直接给开发者使用。通过 forwardRef + useImperativeHandle 来转发 ref, 将 providerFormInstance 赋值给 ref , 开发者通过 ref 标记 Form ,本质上就是获取的 providerFormInstance 对象。(React 19 中函数组件也可以直接把 ref 作为 props 接收,不再需要 forwardRef 包裹,这里保留兼容写法,两种方式在 React 19 均有效。)
通过 Context.Provider 将 formInstance 传递下去,提供给 FormItem 使用。
创建原生 form 标签,绑定 React 事件 —— onReset 和 onSubmit,在事件内部分别调用, 重置表单状态的 resetFields 和提交表单的 submit方法。
接下来就是 FormItem 的具体实现细节。
相比 antd 中的 FormItem ,属性要精简的多,这里我保留了一些核心的属性。
| 属性名称 | 作用 | 类型 |
|---|---|---|
name (重要属性) | 作为表单单元项的键 name | string |
label | 表单标签属性 | string |
height | 表单单元项高度 | number |
labelWidth | label 宽度 | number |
required | 是否必填 | boolean |
trigger | 收集字段值变更的方法 | string , 默认为 onChange |
validateTrigger | 验证校验触发的方法 | string,默认为 onChange |
rules | 验证信息 | 里面包括验证方法 rule 和 验证失败提示文案 message |
接下来就是 FormItem 的代码实现。
function FormItem ({
name,
children,
label,
height = 50 ,
labelWidth,
required = false ,
rules = {},
trigger = 'onChange',
validateTrigger = 'onChange'
}){
const formInstance = useContext(FormContext)
const { registerValidateFields , dispatch , unRegisterValidate } = formInstance
const [ , forceUpdate ] = useState({})
const onStoreChange = useMemo(()=>{
/* 管理层改变 => 通知表单项 */
const onStoreChange = {
changeValue(){
forceUpdate({})
}
}
return onStoreChange
},[ formInstance ])
useEffect(()=>{
/* 注册表单 */
name && registerValidateFields(name,onStoreChange,{ ...rules , required })
return function(){
/* 卸载表单 */
name && unRegisterValidate(name)
}
},[ onStoreChange ])
/* 使表单控件变成可控制的 */
const getControlled = (child)=> {
const mergeChildrenProps = { ...child.props }
if(!name) return mergeChildrenProps
/* 改变表单单元项的值 */
const handleChange = (e)=> {
const value = e.target.value
/* 设置表单的值 */
dispatch({ type:'setFieldsValue' },name ,value)
}
mergeChildrenProps[trigger] = handleChange
if(required || rules ){
/* 验证表单单元项的值 */
mergeChildrenProps[validateTrigger] = (e) => {
/* 当改变值和验证表单,用统一一个事件 */
if(validateTrigger === trigger){
handleChange(e)
}
/* 触发表单验证 */
dispatch({ type:'validateFieldValue' },name)
}
}
/* 获取 value */
mergeChildrenProps.value = dispatch({ type:'getFieldValue' }, name) || ''
return mergeChildrenProps
}
let renderChildren
if(isValidElement(children)){
/* 获取 | 合并 | 转发 | => props */
renderChildren = cloneElement(children, getControlled(children))
}else{
renderChildren = children
}
return <Label
height={height}
label={label}
labelWidth={labelWidth}
required={required}
>
{renderChildren}
<Message
name={name}
{...dispatch({ type :'getFieldModel'},name)}
/>
</Label>
}FormItem 的流程比较复杂,接下来我将一一讲解其流程。
registerValidateFields 注册表单项。此时的 FormItem 的更新函数 onStoreChange 会传入到 FormStore 中,上一章节讲到过,更新方法最终会注册到 FormStore 的 control 属性下,这样 FormStore 就可以选择性的让对应的 FormItem 更新。在 useEffect 销毁函数中,解绑表单项。cloneElement 向表单控件( 比如 Input ) props 中,注册监听值变化的方法,默认为 onChange ,以及表单验证触发的方法 ,默认也是 onChange ,比如如下例子🌰: <FormItem
label="请输入小册名称"
labelWidth={150}
name="name"
required
rules={{
rule:/^[a-zA-Z0-9_\u4e00-\u9fa5]{4,32}$/,
message:'名称仅支持中文、英文字母、数字和下划线,长度限制4~32个字'
}}
trigger="onChange"
validateTrigger="onBlur"
>
<Input
placeholder="小册名称"
/>
</FormItem>如上,向 FormItem 中, 绑定监听变化的事件为 onChange,表单验证的事件为 onBlur 。
更新流程 :那么整个流程,当组件值改变的时候,会触发 onChange 事件,本质上被上面的 getControlled 拦截,实质用 dispatch 触发 setFieldsValue ,改变 FormStore 表单的值,然后 FormStore 会用 onStoreChange 下的 changeValue 通知当前 FormItem 更新,FormItem 更新通过 dispatch 调用 getFieldValue 获取表单的最新值,并渲染视图。这样完成整个受控组件状态更新流程。
验证流程: 当触发 onBlur 本质上用 dispatch 调用 validateFieldValue 事件,验证表单,然后 FormStore 会下发验证状态(是否验证通过)。
完成更新/验证流程。
Label 和 Message UI 视图。还有一些负责 UI 渲染的组件,以及表单控件,这里就简单介绍一下:
function Label({ children , label ,labelWidth , required ,height}){
return <div className="form-label"
style={{ height:height + 'px' }}
>
<div
className="form-label-name"
style={{ width : `${labelWidth}px` }}
>
{required ? <span style={{ color:'red' }} >*</span> : null}
{label}:
</div> {children}
</div>
}function Message(props){
const { status , message , required , name , value } = props
let showMessage = ''
let color = '#fff'
if(required && !value && status === 'reject' ){
showMessage = `${name} 为必填项`
color = 'red'
}else if(status === 'reject'){
showMessage = message
color = 'red'
}else if(status === 'pending' ){
showMessage = null
}else if( status === 'resolve' ){
showMessage = '校验通过'
color = 'green'
}
return <div className="form-message" >
<span style={{ color }} >{showMessage}</span>
</div>
}const Input = (props) => {
return <input
className="form-input"
{...props}
/>
}function Select({ children,...props }){
return <select {...props}
className="form-input"
>
<option label={props.placeholder}
value={null}
>{props.placeholder}</option>
{children}
</select>
}
/* 绑定静态属性 */
Select.Option = function ( props ){
return <option {...props}
className=""
label={props.children}
></option>
}
export default SelectIndex 文件对组件整理,并暴露给开发者使用。
import Form from './component/Form'
import FormItem from './component/FormItem'
import Input from './component/Input'
import Select from './component/Select'
Form.FormItem = FormItem
export {
Form,
Select,
Input,
FormItem
}
export default Formimport React , { useRef , useEffect } from 'react'
import Form , { Input , Select } from './form'
const FormItem = Form.FormItem
const Option = Select.Option
function Index(){
const form = useRef(null)
useEffect(()=>{
console.log(form.current,'form.current')
},[])
const handleClick = () => {
form.current.submit((res)=>{
console.log(res)
})
}
const handleGetValue = ()=>{
console.log( form.current , 'form.current ' )
}
return <div style={{ marginTop:'50px' }} >
<Form initialValues={{ author : '我不是外星人' }}
ref={form}
>
<FormItem
label="请输入小册名称"
labelWidth={150}
name="name"
required
rules={{
rule:/^[a-zA-Z0-9_\u4e00-\u9fa5]{4,32}$/,
message:'名称仅支持中文、英文字母、数字和下划线,长度限制4~32个字'
}}
validateTrigger="onBlur"
>
<Input
placeholder="小册名称"
/>
</FormItem>
<FormItem
label="作者"
labelWidth={150}
name="author"
required
validateTrigger="onBlur"
>
<Input
placeholder="请输入作者"
/>
</FormItem>
<FormItem label="邮箱"
labelWidth={150}
name="email"
rules={{ rule: /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/ ,message:'邮箱格式错误!' }}
validateTrigger="onBlur"
>
<Input
placeholder="请输入邮箱"
/>
</FormItem>
<FormItem label="手机"
labelWidth={150}
name="phone"
rules={{ rule: /^1[3-9]\d{9}$/ ,message:'手机格式错误!' }}
validateTrigger="onBlur"
>
<Input
placeholder="请输入邮箱"
/>
</FormItem>
<FormItem label="简介"
labelWidth={150}
name="des"
rules={{ rule: (value='') => value.length < 5 ,message:'简介不超过五个字符' }}
validateTrigger="onBlur"
>
<Input placeholder="输入简介" />
</FormItem>
<FormItem label="你最喜欢的前端框架"
labelWidth={150}
name="likes"
required
>
<Select defaultValue={null}
placeholder="请选择"
width={120}
>
<Option
value={1}
> React.js </Option>
<Option value={2} > Vue.js </Option>
<Option value={3} > Angular.js </Option>
</Select>
</FormItem>
<button className="searchbtn"
onClick={handleClick}
type="button"
>提交</button>
<button className="concellbtn"
type="reset"
>重置</button>
</Form>
<div style={{ marginTop:'20px' }} >
<span>验证表单功能</span>
<button className="searchbtn"
onClick={handleGetValue}
style={{ background:'green' }}
>获取表单数据层</button>
<button className="searchbtn"
onClick={()=> form.current.validateFields((res)=>{ console.log('是否通过验证:' ,res ) })}
style={{ background:'orange' }}
>动态验证表单</button>
<button className="searchbtn" onClick={() => { form.current.setFieldsValue('des',{
rule: (value='') => value.length < 10,
message:'简介不超过十个字符'
}) }}
style={{ background:'purple' }}
>动态设置校验规则</button>
</div>
</div>
}
export default Index接下来就是验证环节:
① 表单验证未通过
调用 submit ,验证失败的情况。
② 表单验证通过
验证成功!
③ 获取表单的数据层
通过 getFieldsValue 获取表单数据层。
④ 重置表单的数据层
通过 resetFields 重置表单。
⑤ 动态添加表单验证规则
通过 setFieldsValue 动态设置规则。
之前规则和提示文案 { rule: (value='') => value.length < 5 ,message:'简介不超过五个字符' }
动态设置规则 { rule: (value='') => value.length < 10, message:'简介不超过十个字符' }
以上就是从 0 到 1 设计的表单验证系统,希望读者能够对着项目 demo 敲一遍,在实现过程中,我相信会有很多收获。