Form表单组件是我们经常使用的一个组件。以antd库为例,我们通常会在登录场景中使用它。那么,如果要实现一个Form
组件,应该怎么实现呢?
需求分析
Target: 实现一个简易的Form表单组件
在一个表单组件中,我们需要哪些功能呢?让我们参考antd v4的文档来看一下:
4x.ant.design/components/...
● 数据收集
● 数据校验
● 响应式
● 表单提交
让我们来看一下使用的代码,可以了解到以下内容:
- 在Form组件中包裹了Form.Item组件。
- 可以设置Form的初始值。
- 有校验成功和校验失败时的回调函数。
- Form.Item中包含了校验规则和提示信息。
tsx
import { Button, Checkbox, Form, Input } from 'antd';
import React from 'react';
const App: React.FC = () => {
const onFinish = (values: any) => {
console.log('Success:', values);
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
return (
<Form
name="basic"
initialValues={{ remember: true }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
>
<Form.Item
label="Username"
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Input />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
};
export default App;
我们可以打开到对应的仓库,看下源码实现:
github.com/ant-design/...
简易实现
Happy path
- Form 使用一个
form
标签来包裹组件
tsx
export default function Form(props) {
const { children } = props
return <form>{children}</form>
}
- FormItem返回clone的节点
tsx
import React, { Component } from 'react'
export default class FormItem extends Component {
render() {
const { children } = this.props
const returnChildNode = React.cloneElement(children)
return returnChildNode
}
}
数据收集
当我们点击提交按钮时,可以获取到表单中的数据。那么如何实现数据的收集呢?
- 每个子组件独立管理数据 ×
- 父组件来统一收集管理 √
- 第三方收集 √
再想深一层,如果交给父组件来的统一管理,比如存在Form
表单的state
中,每次数据更新setState
会造成Form
中的组件重新渲染。
所以,我们选用第三种方式,使用一个数据仓库来独立管理。
- 构造一个FormStore
- 使用
useForm
来存值,保持唯一性
tsx
import { useRef } from 'react'
class FormStore {
constructor() {
this.store = {}
}
getFieldValue = (name) => {
return this.store[name]
}
getFieldsValue = () => {
return { ...this.store }
}
setFieldsValue = (newStore) => {
this.store = {
...this.store,
...newStore
}
}
submit = () => {
console.log('submit')
}
getForm = () => {
return {
getFieldsValue: this.getFieldsValue,
getFieldValue: this.getFieldValue,
setFieldsValue: this.setFieldsValue,
submit: this.submit
}
}
}
export default function useForm() {
const formRef = useRef()
if (!formRef.current) {
const formStore = new FormStore()
formRef.current = formStore.getForm()
}
return [formRef.current]
}
tsx
// context
import React from "react";
const FieldContext = React.createContext();
export default FieldContext;
- 在父组件使用Provider
tsx
import FieldContext from './FieldContext'
import useForm from './useForm'
export default function Form(props) {
const { children } = props
const [formInstance] = useForm()
const handleSubmit = (e) => {
e.preventDefault()
formInstance.submit()
}
return (
<form onSubmit={handleSubmit}>
<FieldContext.Provider value={formInstance}>
{children}
</FieldContext.Provider>
</form>
)
}
- 子组件使用
tsx
import React, { Component } from 'react'
import FieldContext from './FieldContext'
export default class FormItem extends Component {
static contextType = FieldContext
// 受控组件
getControl = () => {
const { getFieldValue, setFieldsValue } = this.context
const { name } = this.props
return {
value: getFieldValue(name),
onChange: (e) => {
const newValue = e.target.value
setFieldsValue({ [name]: newValue })
}
}
}
render() {
const { children } = this.props
const returnChildNode = React.cloneElement(children, this.getControl())
return returnChildNode
}
}
数据响应式
然而,还存在一个问题:数据已经改变,但组件没有触发更新。因此,我们需要添加触发更新的逻辑。
tsx
// useForm
class FormStore {
// ... 省略代码
constructor() {
// 存放FromItem实例
this.fieldEntities = []
}
registerFieldEntities = (entity) => {
this.fieldEntities.push(entity)
// unregister
return () => {
this.fieldEntities = this.fieldEntities.filter((item) => item !== entity)
delete this.store[entity]
}
}
// ... 省略代码
getForm = () => {
return {
getFieldsValue: this.getFieldsValue,
getFieldValue: this.getFieldValue,
setFieldsValue: this.setFieldsValue,
submit: this.submit,
registerFieldEntities: this.registerFieldEntities
}
}
}
- 在子组件挂载时注册,在卸载前注销
tsx
export default class FormItem extends Component {
// ... 省略代码
componentDidMount() {
const { registerFieldEntities } = this.context
this.unregister = registerFieldEntities(this)
}
componentWillUnmount() {
this.unregister()
}
onStoreChange = () => {
this.forceUpdate()
}
// ... 省略代码
}
- 当触发更新时,我们来刷新对应组件渲染
tsx
// useForm
class FormStore {
// ... 省略代码
setFieldsValue = (newStore) => {
this.store = {
...this.store,
...newStore
}
// 触发更新
this.fieldEntities.forEach((entity) => {
Object.keys(newStore).forEach((k) => {
if (k === entity.props.name) {
entity.onStoreChange()
}
})
})
}
// ... 省略代码
}
表单校验
我们先在store中收集验证后的回调函数
tsx
// useForm
class FormStore {
// ...省略
constructor() {
this.callbacks = {}
}
setCallbacks = (callbacks) => {
this.callbacks = {
...this.callbacks,
...callbacks
}
}
// ...省略
validate() {
let err = []
const fieldEntities = this.fieldEntities
fieldEntities.forEach((entity) => {
const { name, rules } = entity.props
const value = this.getFieldValue(name)
// warning: 只取了第一个校验规则
let rule = rules[0]
if (
rule &&
rule.required &&
(value === undefined || value.trim() === '')
) {
err.push({ [name]: rule.message, value })
}
})
return err
}
submit = () => {
console.log('submit')
let err = this.validate()
const { onFinish, onFinishFailed } = this.callbacks
if (err.length === 0) {
// 校验通过
onFinish(this.getFieldsValue())
} else {
// 校验不通过
onFinishFailed(err, this.getFieldsValue())
}
}
getForm = () => {
return {
getFieldsValue: this.getFieldsValue,
getFieldValue: this.getFieldValue,
setFieldsValue: this.setFieldsValue,
submit: this.submit,
registerFieldEntities: this.registerFieldEntities,
setCallbacks: this.setCallbacks
}
}
}
tsx
// Form
export default function Form(props) {
// ...省略
const { children, onFinish, onFinishFailed } = props
const [formInstance] = useForm()
formInstance.setCallbacks({
onFinish,
onFinishFailed
})
// ...省略
}

初始值的设定
首先,我们需要将form
实例暴露出去
tsx
// Form
export default function Form({ children, onFinish, onFinishFailed }, ref) {
React.useImperativeHandle(ref, () => formInstance)
// ...省略
}
tsx
// Rc-form
export default class MyRCFieldForm extends Component {
formRef = React.createRef()
componentDidMount() {
console.log('form', this.formRef.current)
this.formRef.current.setFieldsValue({ username: 'default' })
}
render() {
return (
<div>
<h3>MyRCFieldForm</h3>
<Form
ref={this.formRef}
onFinish={this.onFinish}
onFinishFailed={this.onFinishFailed}
></Form>
</div>
)
}
}
tsx
// useForm
export default function useForm(form) {
// 存值,在组件卸载之前指向的都是同一个值
const formRef = useRef()
if (!formRef.current) {
if (form) {
formRef.current = form
} else {
const formStore = new FormStore()
formRef.current = formStore.getForm()
}
}
return [formRef.current]
}
tsx
// rc-form index
import FormItem from './FormItem'
import _Form from './Form'
import React from 'react'
// 将form实例暴露出去
const Form = React.forwardRef(_Form)
Form.Item = FormItem
export { FormItem }
export default Form

总结
看到这里,说明你已经成功实现了Form组件。让我们回顾一下涉及的内容:
- 表单验证
- 数据状态管理
- 表单交互
- 表单提交
通过以上代码,我们可以得知antd的form表单是通过Provider Context来传递数据的。同时,还需要建立一个独立的store来收集数据状态,并处理表单验证和提交。通过useForm
的形式来保证了数据的唯一性。
参考来源: