用了这么多年表单,从来没有自己封装过 Form 组件是不是?今天就写一个,免得面试尴尬!
原理
每个表单都有三部分组成,分别是:Form, Form.item,还有各种表单元素组成。表单元素输入的信息要全部汇总到Form表单,最后才能一起提交给后端。
每个表单项都有 value 和 onChange 参数,我们只要在 Item 组件里给 children 传入这俩参数,把值收集到全局的 Store 里。
这样在 Store 里就存储了所有表单项的值,在 submit 时就可以取出来传入 onFinish 回调。
并且,还可以用 async-validator 对表单项做校验,如果有错误,就把错误收集起来传入 onFinishFailed 回调。
整个表单流程就是这样的。
体验 Form 表单
进入antd。学习的第一步就是模仿,而不是创造,所以看看antd里面的form是咋搞的?
复制到我们的项目里面,体验一下
首先,Form 组件是个受控组件,我们要对组件里面的 value 处理之后,才能传给后端。所以说肯定要有个容器存储表单里面的所有value和error,所以咱们要建立一个数据存储中心,然后再去处理 Form 组件和 Form.Item 组件。
Form组件是一个form容器组件,而Form.Item是每个表单元素的包裹容器组件。
步骤一:创建store
在封装组建时候我们用Content来做表单的store已经完全够用。
js
npx create-vite
npm i
npm run dev
在src下面创建一个文件夹Form,创建FormContent.tsx是整个表单的store
js
// 组件 Form 到 Form.Item 到各个表单元素,所有的值都需要收集到 Form 组件集中处理然后发送到后端去
import { createContext } from 'react';
export interface FormContextProps {
values?: Record<string, any>;
setValues?: (values: Record<string, any>) => void;
onValueChange?: (key: string, value: any) => void;
validateRegister?: (name:string, cb: Function) => void;
}
export default createContext<FormContextProps>({})
Form组件本来就是受控组件,我们要对它里面的value做处理以后才能发给后端,所有在stroe里面存数的都是受控组件需要的三个属性,包括一个校验属性:validateRegister。
步骤二: 封装Form组件
用FormContext.Provider包裹的组件,它里面的子子孙孙都用useContext拿到provide派发出去的value值。那value都应该有哪些值呢?从form 的功能入手,无非就是收集 formData值,然后校验一下发送后端么!所以应该有如下四个值: values: 装 form 值得盒子。 setValues:修改 values 值得方法。 onValueChange:监控values的方法。 validateRegister:是调用后端接口前,对 form 值进行校验。
步骤三: 完善表单的提交和校验功能
提交表单需要指向两个方法:提交成功后的回调函数onFinish,和提交失败后的回调函数:onFinishFailed。 失败以后,是不是应该有个盒子去收集错误,一个表单包含很多元素,每个元素都需要校验,是不是需要一个装校验的盒子。
用 useState 保存 values,用 useRef 保存 errors 和 validator。
为什么errors 和 validator不都用 useState 呢?
因为修改 state 调用 setState 的时候会触发重新渲染。而 ref 的值保存在 current 属性上,修改它不会触发重新渲染。errors、validator 这种就是不需要触发重新渲染的数据。然后 onValueChange 的时候就是修改 values 的值。submit 的时候调用 onFinish,传入 values,再调用所有 validator 对值做校验,如果有错误,调用 onFinishFailed 回调。
js
import React, { CSSProperties, useState, useRef, FormEvent, ReactNode } from 'react';
import FormContext from './FormContext';
export interface FormProps extends React.HTMLAttributes<HTMLFormElement> {
className?: string;
style?: CSSProperties;
onFinish?: (values: Record<string, any>) => void;
onFinishFailed?: (errors: Record<string, any>) => void;
initialValues?: Record<string, any>;
children?: ReactNode
}
const Form = (props: FormProps) => {
const {
children,
onFinish,
onFinishFailed,
initialValues,
} = props;
const [values, setValues] = useState<Record<string, any>>(initialValues || {});
const validatorMap = useRef(new Map<string, Function>());
const errors = useRef<Record<string, any>>({});
const onValueChange = (key: string, value: any) => {
values[key] = value;
}
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
for (let [key, callbackFunc] of validatorMap.current) {
if (typeof callbackFunc === 'function') {
errors.current[key] = callbackFunc();
}
}
const errorList = Object.keys(errors.current).map(key => {
return errors.current[key]
}).filter(Boolean);
if (errorList.length) {
onFinishFailed?.(errors.current);
} else {
onFinish?.(values);
}
}
const handleValidateRegister = (name: string, cb: Function) => {
validatorMap.current.set(name, cb);
}
return (
<FormContext.Provider
value={{
onValueChange,
values,
setValues: (v) => setValues(v),
validateRegister: handleValidateRegister
}}
>
<form onSubmit={handleSubmit}>{children}</form>
</FormContext.Provider>
);
}
export default Form;
校验顺序:
第三步:封装Form.Item
Form.Item 应该包含 lable 和表单元素
表单最主要的功能就是把存储在context
里面values
里面具体的value
值传递给具体的input
是不是?
而每一个input
都应该有自己的value
,同时么每个 input 也应该有自己的error
是不是?当校验的时候就会拿出error展示在 input 下面是不是?Form校验我们的用的是async-validator
依赖
js
npm i async-validator -S
所以说在Form.Item组件里面,页面一家在就要做两件事,一个是处理value,一个是处理Error
最后处理下children,毕竟直接把input塞进去,并不能和他的父组件,爷爷组件建立关联。
valuePropName 默认是 value,当 checkbox 等表单项就要取 checked 属性,所以需要把他们统一化,然后才能传给input等表单元素。
这里 children 类型为 ReactElement 而不是 ReactNode。 因为 ReactNode 除了包含 ReactElement 外,还有 string、number 等,所以说:作为 Form.Item 组件的 children,只能是 ReactElement。所以如果没有传入 name 参数,那就直接返回 children。
然后 React.cloneElement 复制 chilren,额外传入 value、onChange 等参数:
js
import React, { ReactNode, useState, ReactElement, useEffect,useContext,ChangeEvent } from 'react';
import Schema, { Rules } from 'async-validator';
import FormContext from './FormContext';
export interface ItemProps{
label?: ReactNode;
name?: string;
valuePropName?: string;
rules?: Array<Record<string, any>>;
children?: ReactElement
}
const Item = (props: ItemProps) => {
const {
label, //标题
children, //input之类
name, //form表单的name
valuePropName, //form 表单的value
rules, //form 表单的规则
} = props;
if(!name) {// 没有name,就说明他不是表单元素input之类
return children;
}
const [value, setValue] = useState<string | number | boolean>();//元素value
const [error, setError] = useState('');//元素error
const { onValueChange, values, validateRegister } = useContext(FormContext);//从context里面拿到的,表单里面的公用信息
// 校验
const handleValidate = (value: any) => {
let errorMsg = null;
if (Array.isArray(rules) && rules.length) {
const validator = new Schema({
[name]: rules.map(rule => {
return {
type: 'string',
...rule
}
})
});
validator.validate({ [name]:value }, (errors) => {
if (errors) {
if (errors?.length) {
setError(errors[0].message!);
errorMsg = errors[0].message;
}
} else {
setError('');
errorMsg = null;
}
});
}
return errorMsg;
}
useEffect(() => {
if (value !== values?.[name]) {
setValue(values?.[name]);
}
}, [values, values?.[name]])
useEffect(() => {
validateRegister?.(name, () => handleValidate(value));
}, [value]);
const getValueFromEvent = (e: ChangeEvent<HTMLInputElement>) => {
const { target } = e;
if (target.type === 'checkbox') {
return target.checked;
} else if (target.type === 'radio') {
return target.value;
}
return target.value;
}
//有的表单是设置 value 属性,有的是设置 checked 属性
const propsName: Record<string, any> = {};
if (valuePropName) {
propsName[valuePropName] = value;
} else {
propsName.value = value;
}
const childEle = React.Children.toArray(children).length > 1 ? children: React.cloneElement(children!, {
...propsName,
onChange: (e: ChangeEvent<HTMLInputElement>) => {
const value = getValueFromEvent(e);
setValue(value);
onValueChange?.(name, value);
handleValidate(value);
}
});
return (
<div>
<div>
{
label && <label>{label}</label>
}
</div>
<div>
{childEle}
{error && <div style={{color: 'red'}}>{error}</div>}
</div>
</div>
)
}
export default Item;
在Form下创建一个index。js文件,导出Form组件,这样一个拥有基础功能的表单组件就出来了。
测试看看:
js
import React from 'react';
//import type { FormProps } from 'antd';
import { Button, Checkbox, Input } from 'antd';
import Form from './Form'
const onFinish = (values: any) => {
console.log('Success:', values);
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
const App: React.FC = () => (
<Form
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
label="Password"
name="password"
rules={[{ required: true, message: 'Please input your password!' }]}
>
<Input.Password />
</Form.Item>
<Form.Item
name="remember"
valuePropName="checked"
>
<Checkbox>Remember me</Checkbox>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
export default App;
总结
一个基础形态的Form组件就出来了,他主要有三部分组成:
1. Store容器:
用来集中管理表单内的所有values值,修改values的方法,监控values的方法,还有检验value的方法。分别是:values,setValues,onValueChange,validateRegister。
2. Form组件:
他有4
个基础参数:
- children: 孩子
- onFinish: 提交成功后要做的事情
- onFinishFailed:提交失败后要做的事情
- initialValues:value的初始值
他有2
个主要任务:
- 用 Context.Provide包裹form组件,将context里面定义的四个值传给form的子孙。
- 提交表单后调用 onFinish 和 onFinishFailed 这两个回调函数。
3. Form.Item组件
他有五个基础参数如下:
- label, //标题
- children, //input之类
- name, //form表单的name
- valuePropName, //form 表单的value
- rules, //form 表单的规则
他有三个主要任务:
1.页面一进来就去把context里面存储的value,分发给各个input 2.在context里面存储了每个input的校验值,我们现在要根据name把对应input的校验函数拿出来。分发给对应的input。 3.我们要对他里面的input做处理,一个表单既有input又有checkbox,还有radio,还有select等等,所以他里面的值既有字符串,又有对象,还有布尔,我们都要对他们做处理以后才能包裹表单元素。
为了解释清晰,文章中所有的表单元素都用input替代了,但是在代码里面都做了对应处理。当然一个Form组件还包含很多功能,我们只实现了他的基础功能,其他功能都是在这个基础功能上进行了对应的拓展。你要是有兴趣,不妨试试看。