likes
comments
collection
share

手把手封装 Form 组件

作者站长头像
站长
· 阅读数 24

用了这么多年表单,从来没有自己封装过 Form 组件是不是?今天就写一个,免得面试尴尬!

原理

每个表单都有三部分组成,分别是:Form, Form.item,还有各种表单元素组成。表单元素输入的信息要全部汇总到Form表单,最后才能一起提交给后端。

每个表单项都有 value 和 onChange 参数,我们只要在 Item 组件里给 children 传入这俩参数,把值收集到全局的 Store 里。

这样在 Store 里就存储了所有表单项的值,在 submit 时就可以取出来传入 onFinish 回调。

并且,还可以用 async-validator 对表单项做校验,如果有错误,就把错误收集起来传入 onFinishFailed 回调。

整个表单流程就是这样的。

体验 Form 表单

进入antd。学习的第一步就是模仿,而不是创造,所以看看antd里面的form是咋搞的?

手把手封装 Form 组件 复制到我们的项目里面,体验一下

手把手封装 Form 组件

首先,Form 组件是个受控组件,我们要对组件里面的 value 处理之后,才能传给后端。所以说肯定要有个容器存储表单里面的所有value和error,所以咱们要建立一个数据存储中心,然后再去处理 Form 组件和 Form.Item 组件。

Form组件是一个form容器组件,而Form.Item是每个表单元素的包裹容器组件。

步骤一:创建store

在封装组建时候我们用Content来做表单的store已经完全够用。

npx create-vite
npm i
npm run dev

在src下面创建一个文件夹Form,创建FormContent.tsx是整个表单的store

// 组件 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 值进行校验。

手把手封装 Form 组件

步骤三: 完善表单的提交和校验功能

提交表单需要指向两个方法:提交成功后的回调函数onFinish,和提交失败后的回调函数:onFinishFailed。 失败以后,是不是应该有个盒子去收集错误,一个表单包含很多元素,每个元素都需要校验,是不是需要一个装校验的盒子。

用 useState 保存 values,用 useRef 保存 errors 和 validator。

为什么errors 和 validator不都用 useState 呢?

因为修改 state 调用 setState 的时候会触发重新渲染。而 ref 的值保存在 current 属性上,修改它不会触发重新渲染。errors、validator 这种就是不需要触发重新渲染的数据。然后 onValueChange 的时候就是修改 values 的值。submit 的时候调用 onFinish,传入 values,再调用所有 validator 对值做校验,如果有错误,调用 onFinishFailed 回调。

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 组件

手把手封装 Form 组件

第三步:封装Form.Item

Form.Item 应该包含 lable 和表单元素

手把手封装 Form 组件

表单最主要的功能就是把存储在context里面values里面具体的value值传递给具体的input是不是?

手把手封装 Form 组件

而每一个input都应该有自己的value,同时么每个 input 也应该有自己的error是不是?当校验的时候就会拿出error展示在 input 下面是不是?Form校验我们的用的是async-validator依赖

npm i async-validator -S

所以说在Form.Item组件里面,页面一家在就要做两件事,一个是处理value,一个是处理Error

手把手封装 Form 组件

最后处理下children,毕竟直接把input塞进去,并不能和他的父组件,爷爷组件建立关联。

valuePropName 默认是 value,当 checkbox 等表单项就要取 checked 属性,所以需要把他们统一化,然后才能传给input等表单元素。

手把手封装 Form 组件

手把手封装 Form 组件 这里 children 类型为 ReactElement 而不是 ReactNode。 因为 ReactNode 除了包含 ReactElement 外,还有 string、number 等,所以说:作为 Form.Item 组件的 children,只能是 ReactElement。所以如果没有传入 name 参数,那就直接返回 children。

手把手封装 Form 组件

然后 React.cloneElement 复制 chilren,额外传入 value、onChange 等参数:

手把手封装 Form 组件

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组件,这样一个拥有基础功能的表单组件就出来了。

手把手封装 Form 组件

测试看看:

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 组件

总结

一个基础形态的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组件还包含很多功能,我们只实现了他的基础功能,其他功能都是在这个基础功能上进行了对应的拓展。你要是有兴趣,不妨试试看。

转载自:https://juejin.cn/post/7393195128954830882
评论
请登录