开始
Context API 的具体概念和用法请参考官方文档
HOC的基本概念也请自行查阅文档
本文要实现的目标是类似 antd4
的 form 表单
<Form
form={form}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
>
<Field name="username" rules={[xxx]}>
<Input placeholder={'xxx'}/>
</Field>
</Form>
注意这里的 form
是通过一个方法获得的
const [form] = Form.useForm()
构想
我们要以使用方法开始
根据使用 antd
的经验,有时我们可能会在 <Form>
和 <Field>
之间嵌套 <Row>
和 <Col>
来设置样式,但是此时 Form
表单依然是能接受到其中 <Field>
的值的
从使用方式可以看出,我们要解决跨层级通信的这个问题,这个时候使用 React 的 Context API 是最合适的。但是 Context API 有一个问题就是一旦触发更新,相关的所有组件都会更新,尽管很多时候只是操作一个表单元素,其他没变的表单元素也会触发更新,这样会造成很大程度的性能浪费
为了解决这个问题,我们可以将 Form 实例
单独提取出来,通过单例模式实现全局一个 form
,然后再通过观察者模式实现局部更新
以下描述顺序为从外向内
index.js
import _Form from './form'
import Field from './Field'
import useForm from './useForm'
const Form = _Form
Form.useForm = useForm
export {Field}
export default Form
Form.js
使用方式
<Form
form={form}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
>
<Field name="username" rules={[xxx]}>
<Input placeholder={'xxx'}/>
</Field>
</Form>
Form.js
import React from "react";
import FieldContext, { SubmitContext } from "./FieldContext";
export default function Form({ form, children, onFinish, onFinishFailed }) {
return (
<form>
<SubmitContext.Provider value={{ onFinish, onFinishFailed }}>
<FieldContext.Provider value={form}>
{children}
</FieldContext.Provider>
</SubmitContext.Provider>
</form>
);
}
FieldContext.js
import React from "react";
// 创建 Form Context对象
const FieldContext = React.createContext();
// 创建表单提交函数context
const SubmitContext = React.createContext();
export default FieldContext;
export {
SubmitContext
}
Field.js
使用方式
<Form
form={form}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
>
<Field name="username" rules={[xxx]}>
<Input placeholder={'xxx'}/>
</Field>
</Form>
在 Field.js
中要做以下几件事
- 给子组件赋予读写数据的能力
这里的子组件指的是各种表单组件 - 注册组件更新方法
- 在组件挂载时将组件注册到
form context
中
import React, { Component } from "react";
import FieldContext from "./FieldContext";
import { verifyForm } from './hoc';
@verifyForm
class Field extends Component {
static contextType = FieldContext;
componentDidMount() {
this.context.registerEntity(this);
}
onStoreChange = () => {
this.forceUpdate();
};
getCntrolled = () => {
const { getFieldValue, setFieldValue } = this.context;
const { name } = this.props;
return {
value: getFieldValue(name), //"username", 从formStore当中读取数据
onChange: e => {
const newValue = e.target.value;
// 使用验证函数
this.props.verify(e.target.value);
// 设置formStore的数据
setFieldValue({ [name]: newValue });
}
};
};
render() {
const { children } = this.props;
return React.cloneElement(children, this.getCntrolled());
}
}
export default Field;
我们通过 HOC
的形式向表单组件增加错误验证项
hoc.js
import React, { useCallback, useState } from 'react';
export function verifyForm(C) {
return function (props) {
const [errorMsgVisible, setErrorMsgVisible] = useState(false);
const verify = useCallback((newValue) => {
if (newValue === '') {
setErrorMsgVisible(true);
} else {
setErrorMsgVisible(false);
}
}, []);
return (
<div>
<C {...props} verify={verify} />
{
errorMsgVisible
? <p style={{ padding: '0 10px', color: 'red' }}>{props.rules[0] ? props.rules[0].message : '请输入参数'}</p>
: null
}
</div>
)
}
}
这里只是简单做了一下是否为空的验证,实际上应该要遍历用户传入的 rules
进行规则验证
useForm.js
注册并返回单个 Form 实例
- 注册组件方法
- 获取表单值的方法
- 改变表单值的方法
- 获取所有表单值的方法
import React from 'react';
// 存储Form的数据
class FormStore {
constructor() {
// 这里存储Form要处理的数据
this.store = {};
// 源码当中用的是数组
this.fieldEntities = {};
}
registerEntity = entity => {
this.fieldEntities = {
...this.fieldEntities,
[entity.props.name]: entity
};
};
getFieldValue = name => {
const v = this.store[name];
return v;
};
setFieldValue = newStore => {
// step1: 修改store数据
this.store = {
...this.store,
...newStore
};
// step2: 更新组件
Object.keys(newStore).forEach(name => {
this.fieldEntities[name].onStoreChange();
});
};
getAllValues = () => {
return {
values: this.store,
allFields: this.fieldEntities
};
}
getForm = () => {
return {
getFieldValue: this.getFieldValue,
setFieldValue: this.setFieldValue,
registerEntity: this.registerEntity,
getAllValues: this.getAllValues,
};
};
}
// 这里使用了单例模式
export default function useForm() {
const formRef = React.useRef();
if (!formRef.current) {
const formStore = new FormStore();
formRef.current = formStore.getForm();
}
return [formRef.current];
}
最后再补充一下关于提交表单的
先看一下使用方式
import React from "react";
import Form, { Field } from "../components/my-rc-field-form/";
import Input from "../components/Input";
import MyButton from '../components/MyButton';
const nameRules = { required: true, message: "请输入用户名!" };
const passwordRules = { required: true, message: "请输入密码!" };
export default function MyRCFieldForm(props) {
const [form] = Form.useForm();
const onFinish = val => {
console.log("onFinish", val);
};
// 表单校验失败执行
const onFinishFailed = val => {
console.log("onFinishFailed", val);
};
return (
<div>
<h3>MyRCFieldForm</h3>
<Form form={form} onFinish={onFinish} onFinishFailed={onFinishFailed}>
<Field name="username" rules={[nameRules]}>
<Input placeholder="input UR Username" />
</Field>
<Field name="password" rules={[passwordRules]}>
<Input placeholder="input UR Password" />
</Field>
<Field>
<MyButton htmlType="submit" context="Submit" />
</Field>
</Form>
</div>
);
}
MyButton.js
import React, { useCallback, useContext } from 'react';
import FieldContext, { SubmitContext } from './my-rc-field-form/FieldContext';
export default function MyButton(props) {
const fieldContext = useContext(FieldContext);
const submitContext = useContext(SubmitContext);
const handleClick = useCallback(() => {
if (props.htmlType === 'submit') {
const { values, allFields } = fieldContext.getAllValues();
let hasFullValues = true;
Object.keys(allFields).filter(f => {
if (f === 'undefined') return false;
if (!values[f]) hasFullValues = false;
return true;
});
if (hasFullValues) {
submitContext.onFinish(values);
} else {
submitContext.onFinishFailed(values);
}
}
}, [props.htmlType, fieldContext, submitContext]);
return (
<div onClick={handleClick}>
<div style={{
width: '80px',
padding: '5px 8px',
border: '1px solid black',
textAlign: 'center',
cursor: 'pointer',
userSelect: 'none',
}}>{props.context}</div>
</div>
)
}
在这里既要引入 form context
(用于获得表单数据)
也要引入 submit context
(用于获取表单提交方法)