手把手封装 Form 组件

用了这么多年表单,从来没有自己封装过 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组件还包含很多功能,我们只实现了他的基础功能,其他功能都是在这个基础功能上进行了对应的拓展。你要是有兴趣,不妨试试看。

相关推荐
前端百草阁16 分钟前
【TS简单上手,快速入门教程】————适合零基础
javascript·typescript
彭世瑜17 分钟前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund40418 分钟前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish18 分钟前
Token刷新机制
前端·javascript·vue.js·typescript·vue
zwjapple18 分钟前
typescript里面正则的使用
开发语言·javascript·正则表达式
小五Five19 分钟前
TypeScript项目中Axios的封装
开发语言·前端·javascript
小曲程序19 分钟前
vue3 封装request请求
java·前端·typescript·vue
临枫54120 分钟前
Nuxt3封装网络请求 useFetch & $fetch
前端·javascript·vue.js·typescript
酷酷的威朗普21 分钟前
医院绩效考核系统
javascript·css·vue.js·typescript·node.js·echarts·html5
前端每日三省21 分钟前
面试题-TS(八):什么是装饰器(decorators)?如何在 TypeScript 中使用它们?
开发语言·前端·javascript