likes
comments
collection
share

手写一个Form表单组件

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

前言

还记得刚工作那会,对于前端领域属于是只有两个不会------这个不会,那个也不会。那时的我当接触到antd的form表单的时候,那是一个惊艳,这组件真是太神奇啦!无论嵌套多少层都能直接控制到目标Item组件,收集到目标表单组件的值,而且还能通过传函数children给Item获取到那几个api(如getFieldValue等)。那么今天,我们就来揭开Form组件神秘的面纱,简单实现一个Form组件。

实现的例子

import Form from './my-form';

const Item = Form.Item;

const GrandChild = () => {
  return (
    <div>
      <Item name="description">
        <textarea placeholder="请输入描述" />
      </Item>
    </div>
  );
};

const Child = () => {
  return (
    <div>
      <GrandChild />
    </div>
  );
};

const App = () => {
  const [form] = Form.useForm();

  return (
    <>
      <Form
        form={form}
        onFinish={values => {
          console.log(values);
        }}
      >
        <Item name="username">
          <input placeholder="请输入用户名" />
        </Item>
        <Item name="password">
          <input placeholder="请输入密码" />
        </Item>
        <Child />
        <button>登录</button>
      </Form>
      <button
      onClick={() => {
        const username = form.getFieldValue('username');
        console.log(username);
      }}
      >
        获取用户名
      </button>
    </>
  );
};

export default App;

以上代码可以看出,最后一个description是嵌套了多层的。效果如下啦。

手写一个Form表单组件

无论嵌套多少层都能准确收集到表单数据,而且也能通过form实例获取到用户名。

useForm

我们在使用antd的Form组件的时候都会传一个useForm返回来的实例,其实这个实例就是一个实例化的类,所有的api以及表单值都会存储到这个实例里,所以在聊useForm前,我们先要来写一个表单仓库类,提供给useForm进行实例化。

这个类要有几个基本的方法,存储表单值,获取表单值,设置表单值。当然下面我写的这个类不止这几个基础方法,对于一些不清楚的方法,本文后面会逐一讲解。

import { useRef, useState } from "react";
class FormStore {
    constructor(forceUpdate) {
        // 存储的表单值
        this.store = {};
        // form组件的一些api存储对象,如onFiish
        this.callbacks = {};
        // 强制刷新组件
        this.forceUpdate = forceUpdate;
        // 注册的表单组件的集合
        this.fieldEntities = [];
    }
    
    // 注册表单
    registerField = (entity) => {
        this.fieldEntities.push(entity);
    }
    
    // 获取表单指定表单值
    getFieldValue = (key) => {
        return get(this.store, key);
    }
    
    // 设置指定表单值
    setFieldValue = (key, value) => {
        this.store[key] = value;
        // 先不用管这一行
        this.fieldEntities.forEach((item) => item.onStoreChange());
    }
    
    // callbacks是一个对象,一般里面会放很多原生form组件的api的方法,例如onFinish
    setCallbacks = (callbacks) => {
        this.callbacks = callbacks;
    }
    submit = () => {
        this.callbacks.onFinish(this.store);
    }
    getForm() {
        return {
            getFieldValue: this.getFieldValue,
            setFieldValue: this.setFieldValue,
            setCallbacks: this.setCallbacks,
            submit: this.submit,
            forceUpdate: this.forceUpdate,
            registerField: this.registerField,
        };
    }
}

到此这个类的基本功能都完成了,那么useForm其实就非常容易了,只是简单实例化一下,然后暴露这个实例就好了。

const useForm = (form) => {
    const formRef = useRef();
    const [, forceUpdate] = useState({});
    if (!formRef.current) {
      if (form) {
        formRef.current = form;
      } else {
        const formStore = new FormStore(forceUpdate);
        formRef.current = formStore.getForm();
      }
    }
    return [formRef.current];
};

export default useForm;

我们先来看一下这个实例吧!

// Form组件
const Form = () => {
  ......
};

Form.useForm = useForm;

// app
import Form from './my-from';

const App = () => {
  const [form] = Form.useFrom();
  console.log('form', form);
  
  return <div />;
};

手写一个Form表单组件

很明显实例化成功,该有的方法都有了,那么进行下一步吧。

Form组件

到了Form组件这一步,我们来回想下,我们使用Form和Item组件的时候,为什么Item组件尽管嵌套了很多层,或者说下钻了很多层,他都能精准的修改form实例里面的值呢?仔细一想,这是不是就是跨组件通讯?无论Item组件在何处,只要是Form的children,都能精准得调用到同一个form实例下的setFieldValue或者getFieldValue去设置或读取form实例收集到的表单值。

提示到这里,大家应该能想到了,那就是非常常见的context!所以Form组件其实就是非常简单,仅仅是将Item们包裹在一个context中,那么任务就完成啦。废话不多说,直接上代码。

import useForm from './useForm';
import FormContext from './formContext';

const Form = ({
  onFinish, children, form,
}) => {
    // 如果传了form实例,那就使用这个实例,没有就生成一个实例
    const [formInstance] = useForm(form);
    // 将onFinish保存起来,提交表单的时候调用,在上面的例子中就是登录按钮被点击时调用
    formInstance.setCallbacks({
        onFinish,
    });

    return (
        <form
            onSubmit={(event) => {
                event.preventDefault();
                event.stopPropagation();
                // 这个submit就是我们写的那个submit,只是包装了一层,把form收集到的values传给我们了
                formInstance.submit();
            }}
            // 这里是可拓展的,可以了解下原生form表单都有哪些api,一个一个拓展到这里,使用扩展omnSubmit一样的方式
        >
            // 这里就是精髓了,将实例通过context传给Item们
            <FormContext.Provider value={formInstance}>
                {children}
            </FormContext.Provider>
        </form>
    );
};

// 暴露useForm方法,使得用户能控制form实例
Form.useForm = useForm;

export default Form;

Form组件比较简单,通过代码中的注释应该很容易看懂,不做过多解释了。

Item组件

Item组件的作用其实就是将传进来的表单组件(如input)改为受控组件,劫持他们的get和set方法,改为form实例的get和set方法即可。在展示完整代码之前,先讲一个例子进行引导。聊聊受控组件。

  • 正常的受控组件
const ControlledInput = () => {
  const [value, setValue] = useState('');
  
  return (
    <input value={value} onChange={(e) => setValue(e.target.value)} />
  );
};
  • 使用form实例进行改装的受控组件
const form = {
  value: '',
  getValue() {
    return this.value;
  },
  setValue(val) {
    this.value = val;
  },
};
const ControlledInput = () => {
  
  return (
    <input value={form.value} onChange={(e) => form.setValue(e.target.value)} />
  );
};

手写一个Form表单组件

从图中可以看出,整个输入框没任何反应(我是在不断敲键盘输入的)。原因很简单,React是状态驱动视图的更新,我们没有定义任何状态,没有任何状态在改变,视图当然不会有任何变化,那么只要我们在值改变的时候改变一下状态就好了。进行一下改装。

const form = {
  value: '',
  getValue() {
    return this.value;
  },
  setValue(val, cb) {
    this.value = val;
    cb();
  },
};
const ControlledInput = () => {
  const [, setState] = useState({});
  const forceUpdate = () => setState({});
  return (
    <input value={form.value} onChange={(e) => form.setValue(e.target.value, forceUpdate)} />
  );
};

来看看效果。

手写一个Form表单组件

每次set值的时候,进行一个状态的改变,那么视图就正常了,其实这就是将正常的受控组件拆成了两步去完成,存储值为一步,更新视图为第二步。

那么,接下来我们来看看Item组件的实现吧。

import React from 'react';
import FormContext from './formContext';

class Item extends React.Component {
    // 这里就和函数组件的useContext一样,可以将contextType视为useContext(FormContext)的返回值
    static contextType = FormContext
    
    componentDidMount() {
        // 注册自己,其实就是将自己的方法都暴露给form实例,让他控制Item的更新
        this.context.registerField(this);
    }

    // 这就是第二步,更新视图,大家翻一下上面FormStore的setFieldValue就明白了
    onStoreChange = () => {
        this.forceUpdate();
    }
    // 将表单组件改为受控组件,劫持get和set
    getControlled = (childProps) => {
        const { name } = this.props;
        return {
            ...childProps,
            value: this.context.getFieldValue(name),
            onChange: (e) => {
                this.context.setFieldValue(name, e.target.value);
            },
        };
    }
    render() {
        const { children } = this.props;
        return React.cloneElement(children, this.getControlled(children.props));
    }
}

export default Item;

看到Item的代码,或许有人会发出唏嘘,都什么年代了,还在用类组件,但就目前我的浅显认知来看,这里不使用类组件还真不行,因为我们需要将Item身上的方法暴露出去,让form实例控制我们的视图更新,比如通过form.setFieldValue设置表单值的时候,可以精确到让指定Item更新视图,更比如我们常见的shouldUpdate,dependences等属性。当然本文并未实现这些功能,有兴趣的读者,可以试着自己去完成,只是做些判断和方法调用,并不难的。

那么,看到这里,相必大家也明白上文中这行代码的用处了,也不必过多解释了。

手写一个Form表单组件

那么以上就是这个简单的Form组件的完整代码的讲解了。至于完整的代码,结尾会给出。

结尾

本文通过一个例子引出Form组件各个组成部分并一一讲解了这些组成部分的作用与实现,那么到此,Form组件的神秘面纱就被我们完全揭开了。以下是本文的完整代码。

简单的Form组件