在日常的开发中,form 作为数据收集模块一定是不可避免会被使用到的,相信大家也对 form 的各种常规使用非常清楚,今天就从学习的角度分析和实现一个简单的 antd-form,帮助我们更加深入的了解其中的原理。
先来看一个没有 form 的场景:
import React, { useState } from 'react'
import { Input } from 'antd'
export default function NoFormPage() {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const setUtils = {
username: setUsername,
password: setPassword,
}
const onChange = (value, field) => {
setUtils[field](value)
}
const onSubmit = () => {
console.log('submit', username, password)
}
return (
<div className="container">
<h3 className="title">No Form</h3>
<Input placeholder="username" onChange={(e) => onChange(e.target.value, 'username')} value={username}></Input>
<Input placeholder="password" onChange={(e) => onChange(e.target.value, 'password')} value={password}></Input>
<button onClick={onSubmit}>submit</button>
</div>
)
}
乍一看仿佛没什么问题,但实际上这里存在几个问题:
- 每一个 Input 都需要手动配置数据,手动更改值
- 没有表单验证
这里我们看看 antd 是如何使用的:
import { Form, Input, Button } from 'antd';
const Demo = () => {
const onFinish = (values) => {
console.log('Success:', values);
};
const onFinishFailed = (errorInfo) => {
console.log('Failed:', errorInfo);
};
return (
<Form
name="basic"
onFinish={onFinish}
onFinishFailed={onFinishFailed}
>
<Form.Item
label="Username"
name="username"
rules={[
{
required: true,
message: 'Please input your username!',
},
]}
>
**<Input />**
</Form.Item>
<Form.Item
label="Password"
name="password"
rules={[
{
required: true,
message: 'Please input your password!',
},
]}
>
<Input.Password />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
};
从上面的代码可以看出,作为用户,我们需要关心的只有:
- 整个表单的数据(而非每一条数据)
- 每一条数据的验证规则
这里我们分析一下,先把大致的架子搭起来:
- Form 关心的主要是
onFinish
和onFinishFailed
两个方法 - Form.Item 则关心
name
和rules
这里我们把 Form.Item 提出来,于是架构如下:
// Form.js
import React from 'react'
export default function Form({ children }) {
const onSubmit = (e) =>{
e.preventDefault()
/* do submit */
}
return <form onSubmit={(e)=>onSubmit(e)}>{children}</form>
}
// FormItem
import { cloneElement } from 'react'
export default function FormItem({ children }) {
const getControlled = ({ name }) => {
return {
value: getFieldValue(name) /* get value */,
onChange: () => {
/* set value */
},
}
}
const returnNode = cloneElement(children, getControlled)
return returnNode
}
接下来需要考虑的就是:
- submit 的处理:这里肯定根据校验规则,通过则执行
onFinish
回调,否则执行onFinishFailed
回调 - 值的处理:因为我们关心的是整个 form 的值,显然这里可以单独抽离一个 store 类用于值得管理
数据处理
这里我们就利用自定义 hook 来完成逻辑的封装:
// useForm.js
class FormStore {
constructor() {
this.store = {}
this.callbacks = {}
}
/**
* get value
* @memberof FormStore
*/
getFieldValue() {}
/**
* set values
* @memberof FormStore
*/
setFieldsValue() {}
/**
* set callbacks: onFinish, onFinishFailed
* @memberof FormStore
*/
setCallbacks(callbacks) {
this.callbacks = {
...this.callbacks,
...callbacks,
}
}
/**
* validate
* @memberof FormStore
*/
validate() {}
/**
* do submit
* @memberof FormStore
*/
submit() {
let err = this.validate()
const { onFinish, onFinishFailed } = this.callbacks
if (err.length > 0) {
onFinishFailed(err, this.store)
} else {
onFinish(null, this.store)
}
}
getForm() {
return {
getFieldValue: this.getFieldValue,
setFieldsValue: this.setFieldsValue,
submit: this.submit,
}
}
}
export default function useForm() {
const formStore = new FormStore()
return [formStore.getForm()]
}
上面的逻辑,直接在 Form 中调用即可:
// Form.js
import React from 'react'
import useForm from './useForm'
export default function Form({ children, onFinish, onFinishFailed }) {
const [form] = useForm()
form.setCallbacks({
onFinish,
onFinishFailed,
})
const onSubmit = (e) => {
e.preventDefault()
form.submit()
}
return <form onSubmit={(e) => onSubmit(e)}>{children}</form>
}
这里存在一个小坑,那就是每次都会 new 一个新的 store,这显然不是我们想要的,那应该怎么办呢?
这里我们可以借助 react 提供的 useRef:
export default function useForm(form) {
const formRef = useRef()
if (!formRef.current) {
if (form) {
formRef.current = form
} else {
const formStore = new FormStore()
formRef.current = formStore
}
}
return [formRef.current]
}
接下来就是处理数据了,不过在这之前,我们可以看到,在 FormItem 中同样需要调用 formInstance 中的方法,这里我们可以借助 context 来实现数据通信:
import { createContext } from 'react'
const FormContext = createContext()
export default FormContext
然后在 Form 中提供数据,就可以在 FormItem 中使用了:
<form onSubmit={(e) => onSubmit(e)}>
<FormContext.Provider value={formInstance}>{children}</FormContext.Provider>
</form>
const getControlled = ({ name }) => {
return {
value: getFieldValue(name),
onChange: (e) => {
const newValue = e.target.value
setFieldsValue({ [name]: newValue })
},
}
}
- get 直接返回值即可
- set 首先需要更新 store 中的值,然后要重新进行 render
处理如下:
/**
* get value
* @memberof FormStore
*/
getFieldValue(name) {
const value = this.store[name]
return value
}
/**
* set values
* @memberof FormStore
*/
setFieldsValue(newStore) {
// set value
this.store = {
...this.store,
...newStore,
}
// render
}
那么问题就来到了如何进行 render。
这里可以借助 react 提供的 forceUpdate,但 setFieldsValue 在 useForm 中,要如何执行对应 FormItem 的 forceUpdate 呢?
解决方案是注册:在 FormItem 挂载的时候将其注册到 useForm 中就能获取到它的实例了。(另外需要注意的是,有注册,那么一定要在对应的时候有注销)
那么问题又来了,如果是 class 组件,直接将 this 注册过来就好了,但我们使用的是函数组件,是没有 this 的,应该怎么办呢?
这里可以稍微转化一下思路,我们需要用到的东西只有两个:
- name 值
- render 方法
那么我们将这两个东西注册过来不就好了吗?
// FormItem.js
useEffect(() => {
const unRegisterField = registerField({
name,
onValueChange,
})
return () => {
unRegisterField()
}
}, [onValueChange])
/**
* set values
* @memberof FormStore
*/
setFieldsValue = (newStore) => {
// set value
this.store = {
...this.store,
...newStore,
}
// render
this.fieldInstances.forEach((filed) => {
Object.keys(newStore).forEach((key) => {
if (filed.name === key) {
filed.onValueChange(newStore)
}
})
})
}
接下来同样是 this 的问题:如果我们使用的是 class 组件,直接在 onValueChange 中调用 this.forceUpdate 就可以重新渲染了,但这里我们使用的是函数组件,是没有 this 的,应该怎么办呢?
这里我们可以借助 useReducer 返回值的第二个,具体参考官网:有类似 forceUpdate 的东西吗?
const [ignored, forceUpdate] = React.useReducer((x) => x + 1, 0)
useEffect(() => {
const unRegisterField = registerField({
name,
rules,
- onValueChange,
+ onValueChange: forceUpdate,
})
return () => {
if (unRegisterField) unRegisterField()
}
}, [])
到这里,整个 store 的部分已经处理得差不多了,效果如下:
校验提交
这里其实没什么坑,值得一提的就是我们在注册的时候需要将 rules 也注册进来:
/**
* validate
* @memberof FormStore
*/
validate = () => {
const err = []
this.fieldInstances.forEach((field) => {
const { name, rules } = field
const value = this.getFieldValue(name)
let rule = rules && rules[0]
if (rule && rule.required && (value === undefined || value === '')) {
err.push({
[name]: rule.message,
value,
})
}
})
return err
}
检验之后,根据检验结果调用不同的回调即可:
/**
* do submit
* @memberof FormStore
*/
submit = () => {
let err = this.validate()
const { onFinish, onFinishFailed } = this.callbacks
if (err.length > 0) {
onFinishFailed(err, this.store)
} else {
onFinish(null, this.store)
}
}