今天写(抄袭)一个 从0开始实现一个antd4 Form表单 的例子,这个是学完了 高老师的课程后的一个总结,只为了巩固自己学到的,代码地址。下面就正式开始吧
第一步、先搭建开发环境
(备注:可以不使用craco,直接使用create-react-app也可以)
1.使用create-react-app来创建一个项目,并安装craco
// 1.创建项目
npx create-react-app form-nut
cd form-nu
// 安装craco
yarn add antd @craco/craco craco-less
2.修改package.json
/* package.json */
"scripts": {
- "start": "react-scripts start",
- "build": "react-scripts build",
- "test": "react-scripts test",
+ "start": "craco start",
+ "build": "craco build",
+ "test": "craco test",
}
3.创建craco.config.js文件,并配置
/* craco.config.js */
// * 配置完成后记得重启下
const CracoLessPlugin = require("craco-less");
module.exports = {
babel: {
//用来支持装饰器
plugins: [["@babel/plugin-proposal-decorators", { legacy: true }]],
},
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: { javascriptEnabled: true },
},
modifyLessRule: function () {
return {
test: /\.less$/,
exclude: /node_modules/,
use: [
{ loader: "style-loader" },
{
loader: "css-loader",
options: {
modules: {
localIdentName: "[local]_[hash:base64:6]",
},
},
},
{ loader: "less-loader" },
],
};
},
},
},
],
};
第二步、初步实现
1.首先看下项目的目录
第一个红框是我们放组件的地方
第二个红框是使用的页面
第三个红框是在App.js中引用
2. 初步实现
// ./pages/Home/index.js
import React from 'react';
import Form, { Field } from '../../component/my-form';
import Input from '../../component/Input';
function FormNut() {
return (
<Form>
<Field>
<Input />
</Field>
<Field>
<Input />
</Field>
</Form>
);
}
export default FormNut;
// ../../component/Input.js
import React from 'react';
const Input = (props) => {
return <input {...props} />;
};
class CustomizeInput extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const { value, ...otherProps } = this.props;
return (
<div style={{ padding: 10 }}>
<Input style={{ outline: 'none' }} value={value} {...otherProps} />
</div>
);
}
}
export default CustomizeInput;
// ../../component/my-form/index.js
import _Form from './Form.js';
import Field from './Field.js';
const Form = _Form;
Form.Field = Field;
export default Form;
export { Field };
// ../../component/my-form/Form.js
import React from 'react';
function Form(props) {
const { children } = props;
return <form>{children}</form>;
}
export default Form;
// ../../component/my-form/Field.js
import React from 'react';
class Field extends React.Component {
getControlled = () => {
return {
value: '',
onChange: (e) => {
console.log();
},
};
};
render() {
const { children } = this.props;
// 这一步的作用是将this.getControlled()的属性赋给Field的children
const returnChildNode = React.cloneElement(children, this.getControlled());
return returnChildNode;
}
}
export default Field;
以上分别是页面代码,Input组件代码,Form的index文件,Form.js文件, Field.js组件的代码,下面会根据这些来逐步实现antd4的基本功能
第三步、状态数据传递
实现数据传递时我们首先要保存数据,这里选择使用context来作为数据仓库保存数据。
在antd3中我们如果要使用form的方法只能在类组件中用一个高阶组件来将改form实例传递到类组件中,使用方式为:
@Form()
class Demo extends Component {
....
}
在antd4中使用了Form的hooks,useForm来生成实例,并将改form实例传递给组件,这样就可以函数组件中使用form实例的方法。
首先我们创建一个useForm的文件
// ../../component/my-form/useForm.js
import React from 'react';
class FormStore {
constructor() {
// 这个是保存状态值的
this.store = {};
}
// form中获取某个表单项数据
getFieldValue = (name) => {
return this.store[name];
};
// form中获取所有表单项数据
getFieldsValue = () => {
return { ...this.store };
};
// form中设置表单值
setFieldsValue = (newStore) => {
this.store = { ...this.store, ...newStore };
};
getForm() {
// form暴露的方法
return {
getFieldValue: this.getFieldValue,
getFieldsValue: this.getFieldsValue,
setFieldsValue: this.setFieldsValue,
};
}
}
function useForm() {
// 使用ref是可以保证ref实时更新保持一致
const storeRef = React.useRef();
// 判断ref是否创建,如果创建了则直接返回,否则新创建一个
if (!storeRef.current) {
const formStore = new FormStore();
storeRef.current = formStore.getForm();
}
return [storeRef.current];
}
export default useForm;
修改../../conponent/my-form/index.js文件,将useForm按照antd4的导出方式导出
import _Form from './Form.js';
import Field from './Field.js';
import useForm from './useForm';
const Form = _Form;
Form.Field = Field;
Form.useForm = useForm;
export default Form;
export { Field, useForm };
然后在Form中使用,修改./pages/Home/index.js
import React from 'react';
import Form, { Field } from '../../component/my-form';
import Input from '../../component/Input';
function FormNut() {
const [form] = Form.useForm();
return (
<Form form={form}>
<Field>
<Input />
</Field>
<Field>
<Input />
</Field>
<button>submit</button>
</Form>
);
}
export default FormNut;
接下来我们要实现数据的传递,这里我们需要用到context,表单的状态值和属性通过context来传递。
首先在my-form组件库中新建一个存放context文件的地方,名字就起做FormContext.js吧,接下来我们在FormContext.js中创建context并导出
// @/conponent/my-form/FormContext.js
import React from 'react';
const FormContext = React.createContext();
export default FormContext;
接下来就是如何使用这个context了,首先要在Form.js中的children外包裹一层Provider,这样各个Field就可以通过context来获取form传进去的值了
// @/component/my-form/Form.js
import React from 'react';
import FormContext from './FormContext.js';
function Form(props) {
const { children, form } = props;
return (
<form
onSubmit={(e) => {
// 阻止默认提交事件
e.preventDefault();
}}
>
// 将form放到context中,子组件通过context可以进行访问
<FormContext.Provider value={form}>{children}</FormContext.Provider>
</form>
);
}
export default Form;
接下来在Field.js中使用消费context
import React from 'react';
import FormContext from './FormContext';
class Field extends React.Component {
// 消费context
static contextType = FormContext;
getControlled = () => {
return {
value: '',
onChange: (e) => {
console.log(e);
},
};
};
render() {
const { children } = this.props;
console.log(this.context, 'this.context');
// 这一步的作用是将this.getControlled()的属性赋给Field的children
const returnChildNode = React.cloneElement(children, this.getControlled());
return returnChildNode;
}
}
export default Field;
代码中写了之后,我们运行下代码,然后可以在控制台中看到打印的this.context信息如下:
这是
useForm.js中调用useForm方法后返回的属性
由于当前Field.js代码中给form表单项赋值的方法还没有完善,所以我们还需要继续优化Field.js文件,可以让我们在文本框输入内容,并且更新formStore中的值。现在讲Field.js文件中的代码修改为下面:
import React from 'react';
import FormContext from './FormContext';
class Field extends React.Component {
static contextType = FormContext;
getControlled = () => {
// name是表单项的name值
const { name } = this.props;
// 通过context我们可以拿到设置和获取store中的值,并将输入内容展示到表单上
const { getFieldValue, setFieldsValue } = this.context;
return {
// 获取值
value: getFieldValue(name),
onChange: (e) => {
// 修改值
setFieldsValue({ [name]: e.target.value });
},
};
};
render() {
const { children } = this.props;
console.log(this.context, 'this.context');
// 这一步的作用是将this.getControlled()的属性赋给Field的children
const returnChildNode = React.cloneElement(children, this.getControlled());
return returnChildNode;
}
}
export default Field;
接下来我们尝试在form加载页面使用 form.setFieldsValue来给表单设置初始值
// ./pages/Home/index.js
import React, { useEffect } from 'react';
import Form, { Field } from '../../component/my-form';
import Input from '../../component/Input';
function FormNut() {
const [form] = Form.useForm();
useEffect(() => {
console.log(form, 'form');
form.setFieldsValue({ name: 'default' });
}, []);
return (
<Form form={form}>
<Field name="name">
<Input />
</Field>
<Field name="age">
<Input />
</Field>
<button>submit</button>
</Form>
);
}
export default FormNut;
然后在useForm.js中的setFieldsValue方法中加入日志打印,打印表单的初始值store。
之后我们运行会发现,在使用form.setFieldsValue赋初始值后,日志是打印了,但是页面并没有给出初始值,所以我们这里需要对对应的form表单组件更新一下。react组件更新的方式有四种:ReactDOM.render、forceUpdate、setState、以及因为父组件而更新。这里因为我们的filed组件是类组件,所以如果需要使form的子组件更新最好就是使用forceUpdate。
要实现表单在调用setFieldsValue时组件更新我们需要在useForm中维护一个表单实例,里面放的是Form下的Field表单,然后我们修改useForm.js文件
// useForm.js
import { useRef } from 'react';
class FormStore {
constructor() {
this.store = {};
this.fieldEntetities = [];
}
// 注册实例
// 注册与取消实例
// 订阅与取消订阅
registerEntetities = (entity) => {
this.fieldEntetities.push(entity);
return () => {
this.fieldEntetities = this.fieldEntetities.filter(
(item) => item !== entity
);
delete this.store[entity.props.name];
};
};
getFieldValue = (name) => {
return this.store[name];
};
getFieldsValue = () => {
return { ...this.store };
};
// set
setFieldValue = (newStore) => {
// 1. 更新store
this.store = {
...this.store,
...newStore,
};
// 1.循环fieldEntetities
this.fieldEntetities.forEach((entity) => {
// 2.拿到表单更新项的key值
Object.keys(newStore).forEach((item) => {
// 3.判断是哪项表单要更新,仅更新当前的表单项,避免了所有的表单项更新
if (item === entity.props.name) {
entity.onStoreChange();
}
});
});
};
// 暴露方法
getForm = () => {
return {
getFieldValue: this.getFieldValue,
getFieldsValue: this.getFieldsValue,
setFieldValue: this.setFieldValue,
registerEntetities: this.registerEntetities,
};
};
}
export default function useForm() {
const formRef = useRef();
if (!formRef.current) {
const formStore = new FormStore();
formRef.current = formStore.getForm();
}
return [formRef.current];
}
其中this.fieldEntetities就是我们保存实例的数组,我们在这里暴露一个方法registerEntetities,这个是注册Filed自组件的,就是Form下放了几个Field,就会在这里放几个Field实例。因为我们实现了组件挂载时注册实例,所以当页面卸载的情况下我们也要卸载对应的Field组件,在Filed.js中使用的方法如下
// Field.js
import React from 'react';
import FormContext from './FormContext';
class Field extends React.Component {
static contextType = FormContext;
componentDidMount() {
// useForm中注册实例的方法
const { registerEntetities } = this.context;
// this代表的就是改filed的项,同时将返回值赋值给unregister方法,在卸载组件的生命周期中调用
this.unregister = registerEntetities(this);
}
// 组件卸载时执行该方法,目的是将该实例从fieldEntetities中删除,同时删除store中保存的值
componentWillUnmount() {
this.unregister();
}
// 这个是给useForm.js中调用setFieldsValue方法时调用的
onStoreChange = () => {
this.forceUpdate();
};
getControlled = () => {
// name是表单项的name值
const { name } = this.props;
// 通过context我们可以拿到设置和获取store中的值,并将输入内容展示到表单上
const { getFieldValue, setFieldsValue } = this.context;
return {
// 获取值
value: getFieldValue(name),
onChange: (e) => {
// 修改值
setFieldsValue({ [name]: e.target.value });
},
};
};
render() {
const { children } = this.props;
// 这一步的作用是将this.getControlled()的属性赋给Field的children
const returnChildNode = React.cloneElement(children, this.getControlled());
return returnChildNode;
}
}
export default Field;
现在我们试试就可以在form表单初始化的时候给赋值了,同时我们将每个实例放到一个数组中,每次改变仅仅更新那个改变的数组,其他的数组是不受影响的,不然如果表单里面内容太多的话,你更新一个表单项,所有的都会更新,这样使用感受就会很差,会感觉表单有些卡。接下来我们继续实现表单校验的功能
第四步、实现表单校验
接下来我们来实现表单校验的功能,就是在提交时,校验必填的表单是否已经填写,未填的打印出错误信息。
首先我们在页面调用form组件的代码中加入onFinish和onFinishFailed两个事件,分别表示校验成功和校验失败时需要执行的函数
// index.js
import React, { useEffect } from 'react';
import Form, { Field } from '../../component/my-form';
import Input from '../../component/Input';
function FormNut() {
const [form] = Form.useForm();
useEffect(() => {
console.log(form, 'form');
form.setFieldsValue({ name: 'default' });
}, []);
// 表单校验成功执行的事件
const onFinish = (value) => {
console.log(value);
};
// 表单校验失败执行的事件
const onFinishFailed = (err) => {
console.log(err);
};
return (
<Form form={form} onFinish={onFinish} onFinishFailed={onFinishFailed}>
<Field name="name" rules={[{ required: true, message: '必填项' }]}>
<Input />
</Field>
<Field name="age">
<Input />
</Field>
<button>submit</button>
</Form>
);
}
export default FormNut;
接下来,我们在Form.js中继续完善,代码如下:
// Form.js
import React from 'react';
import FieldContext from './FieldContext';
export default function Form({ children, form, onFinish, onFinishFaild }) {
// 调用userForm中的方法,将两个放useForm中
form.setCallbacks({
onFinish,
onFinishFaild,
});
return (
<form
onSubmit={(e) => {
// 为了阻止form的默认事件,以避免每次提交的时候都会自动刷新页面
e.preventDefault();
// 提交事件,后面会补充
form.submit();
}}
>
<FieldContext.Provider value={form}>{children}</FieldContext.Provider>
</form>
);
}
上面的代码中添加了form.setCallbacks和提交的时的form.submit代码,这个分别时将form中传入的onFinish和onFinishFailed函数添加进useForm中以及提交的事件代码
下面我们在useForm.js中添加他们,useForm.js代码如下
// useForm.js
import React from 'react';
class FormStore {
constructor() {
// 这个是保存状态值的
this.store = {};
// 存放Field实例的地方
this.fieldEntetities = [];
this.callBack = {};
}
registerEntetities = (entity) => {
this.fieldEntetities.push(entity);
return () => {
// 将卸载的field表单项从fieldEntetities中删除
this.fieldEntetities = this.fieldEntetities.filter(
(item) => item !== entity
);
// 删除store中该表单保存的值
delete this.store[entity.props.name];
};
};
// 将form的函数传入callback中,是在form文件中调用
setCallbacks = (newCallback) => {
this.callBack = { ...this.callBack, ...newCallback };
};
// 校验通过遍历所有表单项的实例,然后取出每个表单项的name和rules来进行判断,如果是必填且未填的则放到错误信息数组中,返回该数组
validate = () => {
const errorArr = [];
this.fieldEntetities.forEach((entity) => {
const { name, rules } = entity.props;
const value = this.getFieldValue(name);
if (rules && rules[0].required && !value) {
errorArr.push({ [name]: rules[0].message || '必填项', value: value });
}
});
return errorArr;
};
// 提交
submit = () => {
const { onFinish, onFinishFailed } = this.callBack;
const errorArr = this.validate();
if (errorArr.length) {
console.error(errorArr);
onFinishFailed(errorArr);
} else {
onFinish && onFinish(errorArr);
}
};
// form中获取某个表单项数据
getFieldValue = (name) => {
console.log(this.store[name], 'name');
return this.store[name];
};
// form中获取所有表单项数据
getFieldsValue = () => {
return { ...this.store };
};
// form中设置表单值
setFieldsValue = (newStore) => {
this.store = { ...this.store, ...newStore };
// 循环fieldEntetities
this.fieldEntetities.forEach((entity) => {
// 拿到改变的表单项value,然后将键名与表单项的实例的name对比,
// 如果对上了就表明当前改动的是这个表单项,然后调用该表单项的onStoreChange方法,进行强制更新
Object.keys(newStore).forEach((item) => {
if (item === entity.props.name) {
entity.onStoreChange();
}
});
});
};
getForm() {
return {
getFieldValue: this.getFieldValue,
getFieldsValue: this.getFieldsValue,
setFieldsValue: this.setFieldsValue,
registerEntetities: this.registerEntetities,
setCallbacks: this.setCallbacks,
submit: this.submit
};
}
}
function useForm() {
// 使用ref是可以保证ref实时更新保持一致
const storeRef = React.useRef();
// 判断ref是否创建,如果创建了则直接返回,否则新创建一个
if (!storeRef.current) {
const formStore = new FormStore();
storeRef.current = formStore.getForm();
}
return [storeRef.current];
}
export default useForm;
以上代码添加了this.callBack属性以及this.setCallBacks方法和this.validate以及this.submit方法,
其中this.callBack是存放传入form表单回调函数的,this.setCallBacks是存入的动作,this.validate是提交前对表单进行校验的,this.submit是提交动作
这是我们测试给表单设置必填项但是不填写时提交会发现,控制台打印出错误的log日志,如下:
总结
到这里就结束了,这个里面主要涵盖了状态管理,context,hook,自定义hook等知识点。不过这是我刚开始写的文章,有点乱。。。。可能过段时间我也看不懂了,哈哈哈哈