确定要实现的功能
import React, { useRef } from 'react'
import Form from './Form'
import FormItem from './FormItem'
import Input from './Input'
const FormContext = () => {
const formRef = useRef(null)
// 功能1:提交时获取所有表单数据
const submit = () => {
formRef.current.submitForm((formData) => {
console.log(formData)
})
}
// 功能2:重置表单数据
const reset = () => {
formRef.current.resetForm()
}
return (
<div className="form-context">
<Form ref={formRef}>
<FormItem name="name" label="用户名">
<Input />
</FormItem>
<FormItem name="password" label="密码">
<Input type="password" />
</FormItem>
</Form>
<div className="operate">
<button onClick={submit}>提交</button>
<button onClick={reset}>重置</button>
</div>
</div>
)
}
export default FormContext
- 功能1:点击提交按钮时,获取所有表单数据
- 功能2:点击重置按钮时,重置表单数据
这里为 Form
组件绑定了ref
,是为了调用 Form
组件内部的方法,目前 Form
组件对外暴露的方法有:
submitForm
resetForm
Form组件实现
这里有四个关注点
- 函数组件传入
ref
需要使用forwardRef
承接 - 函数组件对外暴露API需要使用
useImperativeHandle
进行处理 Form
组件只处理FormItem
子元素,其他元素自动忽略- 基于
FormContext
下发表单数据源以及数据源修改方法
import React, { createContext, forwardRef, useImperativeHandle, useState } from 'react'
// 基于 FormContext 下发表单数据源以及修改方法
export const FormContext = createContext({})
const Form = forwardRef((props, formRef) => {
// 统一管理表单数据源
const [formData, setFormData] = useState({})
// 对外暴露的API
useImperativeHandle(formRef, () => ({
// 表单提交
submitForm: (callback) => {
callback && callback({ ...formData })
},
// 表单重置
resetForm: () => {
let data = { ...formData }
Object.keys(data).forEach((item) => {
data[item] = ''
})
setFormData(data)
},
}))
// Form表单内的表单项修改统一的赋值方法
const handleChange = (name, value) => {
setFormData({
...formData,
[name]: value,
})
}
const renderContent = () => {
const renderChildren = []
React.Children.map(props.children, (child) => {
// child.type 子元素自身,检查其静态属性 displayName 是否满足条件
if (child.type.displayName === 'formItem') {
renderChildren.push(child)
}
})
return renderChildren
}
// 传入数据源以及数据源的修改方法,子孙后代都可读取 value 中的值
return <FormContext.Provider value={{ formData, handleChange }}>{renderContent()}</FormContext.Provider>
})
Form.displayName = 'form'
export default Form
FormItem 组件实现
这里有三个关注点
- 必传参数
name
与Form
中的数据源存在对应关系 FormItem
中只处理了Input
的子元素,其他元素自动忽略- 复写
FormContext.Provider
,为其增加name
参数的传递,用于标识更新表单数据源中哪一项
import React, { useContext } from 'react'
import { FormContext } from '../Form'
const FormItem = (props) => {
const context = useContext(FormContext)
const { children, name, label } = props
const renderContent = () => {
// 子元素检查
if (React.isValidElement(children) && children.type.displayName === 'input') {
return (
<div className="form-item">
<span className="form-item-label">{label}:</span>
{children}
</div>
)
}
return null
}
// 复写 FormContext.Provider,增加 name 参数的传递
return <FormContext.Provider value={{ ...context, name }}>{renderContent()}</FormContext.Provider>
}
FormItem.displayName = 'formItem'
export default FormItem
Input 组件实现
这里有两个注意点
- 获取
FormContext.Provider
提供提供的value
值 - 根据 name 属性,更新 Form 中的数据源
import React, { useContext } from 'react'
import { FormContext } from '../Form'
const Input = ({ type = 'text' }) => {
// 获取 `FormContext.Provider` 提供提供的 `value` 值
const context = useContext(FormContext)
const handleChange = (e) => {
// 根据 name 属性,更新 Form 中的数据源
context.handleChange(context.name, e.target.value)
}
return <input type={type} value={context.formData[context.name]} onChange={handleChange} />
}
Input.displayName = 'input'
export default Input
至此一个极简版本的 Form
表单组件便完成
涉及知识点补充
- React Context API
- React.isValidElement
- React.Children.map / React.Children.forEach / React.Children.toArray
- React.cloneElement
- React.createElement
- React Hooks