跳到主要内容

实践篇-表单验证下

一 前言

上一章节主要讲了 Form 表单的设计原则,以及状态管理 FormStore 和自定义 hooks useForm 的编写,本章节将继续上一章节没有讲完的部分。

通过本章节的学习,你将收获以下知识点:

  • Form 设计及其编写。
  • FormItem 设计及其编写。

二 Form 编写

1 属性分析

属性设定

属性名称作用类型
form传入useForm 创建的 FormStore实例FormStore 实例对象
onFinish表单提交成功调用function ,一个参数,为表单的数据层
onFinishFailed表单提交失败调用function ,一个参数,为表单的数据层
initialValues设置表单初始化的值object

细节问题

  • Form 接收类似 onFinishonFinishFailed 监听回调函数。
  • Form 可以被 ref 标记,ref 可以获取 FormStore 核心方法。
  • Form 要保留原生的 form 属性,当 submit 或者 reset 触发,自动校验/重置。

2 代码实现

创建 context 保存 FormStore 核心 Api

import {  createContext  } from 'react'
/* 创建一个 FormContext */
const FormContext = createContext()

export default FormContext
  • 创建一个 context 用来保存 FormStore 的核心 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 ,里面保存着操纵表单状态的方法,比如 getFieldValuesetFieldsValue 等。
  • formInstance 抽离出 setCallback ,dispatch 等方法,得到 providerFormInstance ,因为这些 api 不期望直接给开发者使用。通过 forwardRef + useImperativeHandle 来转发 ref, 将 providerFormInstance 赋值给 ref , 开发者通过 ref 标记 Form ,本质上就是获取的 providerFormInstance 对象。
  • 通过 Context.Provider 将 formInstance 传递下去,提供给 FormItem 使用。
  • 创建原生 form 标签,绑定 React 事件 —— onResetonSubmit,在事件内部分别调用, 重置表单状态的 resetFields 和提交表单的 onSubmit方法。

三 FormItem 编写

接下来就是 FormItem 的具体实现细节。

1 属性分析

相比 antd 中的 FormItem ,属性要精简的多,这里我保留了一些核心的属性。

属性名称作用类型
name (重要属性)证明表单单元项的键 namestring
label表单标签属性string
height表单单元项高度number
labelWidthlable 宽度number
required是否必填boolean
trigger收集字段值变更的方法string , 默认为 onChange
validateTrigger验证校验触发的方法string,默认为 onChange
rules验证信息里面包括验证方法 rule 和 验证失败提示文案 message

2 代码实现

接下来就是 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 的流程比较复杂,接下来我将一一讲解其流程。

  • 第一步: FormItem 会通过 useContext 获取到表单实例下的方法。
  • 第二步: 创建一个 useState 作为 FormItem 的更新函数 onStoreChange。
  • 第三步: 在 useEffect 中调用 registerValidateFields 注册表单项。此时的 FormItem 的更新函数 onStoreChange 会传入到 FormStore 中,上一章节讲到过,更新方法最终会注册到 FormStore 的 control 属性下,这样 FormStore 就可以选择性的让对应的 FormItem 更新。在 useEffect 销毁函数中,解绑表单项。
  • 第四步: 让 FormItem 包裹的表单控件变成受控的, 通过 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 会下发验证状态(是否验证通过)。

完成更新/验证流程。

  • 第五步:渲染 LabelMessage UI 视图。

四 Index文件及其他组件

还有一些负责 UI 渲染的组件,以及表单控件,这里就简单介绍一下:

Label

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>
}
  • Label 的作用就是渲染表单的标签。

Message

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 === 'pendding' ){
showMessage = null
}else if( status === 'resolve' ){
showMessage = '校验通过'
color = 'green'
}
return <div className="form-message" >
<span style={{ color }} >{showMessage}</span>
</div>
}
  • message 显示表单验证的状态,比如失败时候的提示文案等,成功时候的提示文案。

Input

const Input = (props) => {
return <input
className="form-input"
{...props}
/>
}
  • Input 本质上就是 input 标签。

Select 组件

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 Select

Index文件

Index 文件对组件整理,并暴露给开发者使用。

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 Form

五 验证功能

验证 demo 编写

import 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

验证效果

接下来就是验证环节:

① 表单验证未通过

fail.gif

调用 submit ,验证失败的情况。

② 表单验证通过

success.gif

验证成功!

③ 获取表单的数据层

get.gif

通过 getFieldsValue 获取表单数据层。

④ 重置表单的数据层

reset.gif

通过 resetFields 重置表单。

⑤ 动态添加表单验证规则

dongtai.gif

通过 setFieldsValue 动态设置规则。

之前规则和提示文案 { rule: (value='') => value.length < 5 ,message:'简介不超过五个字符' }

动态设置规则 { rule: (value='') => value.length < 10, message:'简介不超过十个字符' }

六 总结

以上就是从 0 到 1 设计的表单验证系统,希望读者能够对着项目 demo 敲一遍,在实现过程中,我相信会有很多收获。