实现一个简单的React表单双向绑定

1,885 阅读3分钟

一直在使用antdesign3.0的表单数据双向数据绑定,而antMobile的表单数据绑定很不习惯,所以想自己开发个类似antdesign的表单form.create双向绑定

高阶组件属性代理
  1. 需要通过高阶组件将表单方法注入props中
  2. 注入的方法getFieldDecorator,传入表单配置项目,没有做组件事件的拦截,只是简单的onchange方法重新定义,利用forceUpdate强制更新
  3. 注入的方法 validateFields, 提交表单的时候触发表单规则校验
  4. 注入的方法 resetFields, 表单重置
  5. 注入的方法 submitting, 表单是否处在正在提交中
  6. 注入的方法 getFieldValue, 获取某个表单项的值
  7. 高阶组件配置scrollIntoView来触发将表单提交异常时滚动表单项到视图中央,callback_onErr表单异常处理方法

import React  from "react";
import fieldStore from "./fieldStore";
import { comparisonObject } from "./utils/common";
class Form extends React.PureComponent { }
Form.create = (option = {}) => {
  const {
    scrollIntoView, // 表单提交异常时滚动表单项到视图中央
    callback_onErr, // 接收表单异常处理方法
  } = option;
  return (WrappedComponent) => {
    return class extends React.Component {
      constructor() {
        super();
        this.state = {};
        this.fieldStore = fieldStore()
      }
      /** 表单容器 */
      getFieldDecorator = (key, config = { rules: [], initialValue }) => {
        if (!key) throw Error("getFieldDecorator Error: lost formItem key");
        let rules = this.fieldStore.rules;
        let store = this.fieldStore.store;
        let initialMata = this.fieldStore.initialMata;
        if (rules[key] === undefined || (rules[key].length && !comparisonObject(rules[key][0], config.rules[0]))) {
          this.fieldStore.dispatchRules(key, config.rules);
        }
        if (!(key in initialMata)) {
          this.fieldStore.dispatchInitialMeta(key, config.initialValue);
        }
        return (ComponentF) => {
          const onChange = (v) => {
            if (ComponentF.props.onChange) ComponentF.props.onChange(v);
            const type = Object.prototype.toString.call(v);
            this.fieldStore.dispatchStore(key, type === "[object Object]" ? (v.target.value || e.target.checked) : v);
            this.forceUpdate(); // 强制属性刷新
          }
          const propsNew = {
            ...ComponentF.props,
            onChange: e => onChange(e),
            defaultValue: initialMata[key] || ComponentF.value,
          }
          let EleType = ComponentF.type.name;
          if (typeof ComponentF.type === 'function') propsNew.value = store[key] || initialMata[key] || ComponentF.value;
          if (EleType === "Switch") propsNew.checked = store[key] || initialMata[key] || ComponentF.checked;
          if (scrollIntoView) {
            propsNew.id = `${key}ByGetFieldDecorator`;
          }
          const newTree = React.cloneElement(ComponentF, propsNew, ComponentF.props.children);
          return newTree;
        }
      }
      /** 表单校验, 根据fieldStore的validateFields回调执行 */
      validateFieldsWraped = (submit) => {
        if (this.state.submitting) return;
        this.fieldStore.validateFields((errMess, store) => {
          if (scrollIntoView) {
            return document.querySelector(`#${errKey}ByGetFieldDecorator`).scrollIntoView({ block: "center", behavior: "smooth" });
          }
          if (callback_onErr && errMess) {
            return callback_onErr(errMess);
          }
          if (errMess) return console.error(errMessage);
          this.setState({ submitting: false })
          submit(errMess, store);
        })
      }
       /** 是否在执行中 */
      submitting = () => {
        return Boolean(this.state.submitting)
      }
      
      addtionaProps = () => {
        return {
          getFieldDecorator: this.getFieldDecorator,
          setFieldsValue: this.fieldStore.setFieldsValue,
          getFieldValue: this.fieldStore.getFieldValue,
          validateFields: this.validateFieldsWraped,
          resetFields: this.fieldStore.resetFields,
          store: this.fieldStore.store,
          submitting: this.submitting(),
        }
      }

      render() {
        var props = { form: this.addtionaProps() };
        return (
          <WrappedComponent
            {...this.props}
            {...props}
          />
        )
      }
    }
  }
}
fieldStore

将form.state独立到fieldStore中管理, validateFields根据创建的规则,校验store中的数据,

/**
 * 表单仓库
 */
class FieldStore {
  constructor(fields) {
    this.store = { ...fields };
    this.rules = {};
    this.initialMata = {};
  }
  /** 初始化表单值 */
  dispatchStore = (key, value) => {
    this.store[key] = value;
  }
  /** 初始化表单初始值 */
  dispatchInitialMeta = (key, value) => {
    this.initialMata[key] = value;
  }
   /** 更新表单规则 */
  dispatchRules = (key, rule) => {
    this.rules[key] = rule;
  }
  /** 返回表单数据,没有setState过则返回initialMata */
  getFieldValue = (key) => {
    return this.store[key] || this.initialMata[key];
  }
  /** 设置表单 */
  setFieldsValue = (props) => {
    this.store = {
      ...this.store,
      ...props,
    }
  }
   /** 清空数据 */
  resetFields = () => {
    this.store = { ...fields };
    this.rules = {};
    this.initialMata = {};
  }
  /** 表单校验 */
  validateFields = (submitting) => {
    let errFlag = false,
      errMessage = "",
      errKey = "",
      rules = this.rules;

    let store = Object.assign(this.initialMata, this.store);
    for (let i = 0; i < Object.keys(store).length; i++) {

      let item = store[Object.keys(store)[i]];
      if (rules && rules[Object.keys(store)[i]]) {
        let r = rules[Object.keys(store)[i]];

        if (r && r.length && r[0].required && (!item && item !== 0 && item !== false)) {
          errFlag = true;
          errMessage = r[0].message || "表单填写不完全";
          errKey = Object.keys(store)[i];
        } else if (r.length === 2 && r[1].validator) { // 自定义校验可正则
          function call(mess) {
            if (mess) {
              errMessage = mess || "表单项格式不正确";
              errFlag = true;
              errKey = Object.keys(store)[i];
            }
          }
          // 传入表单值、回调函数, 回调函数是否返回errMessage控制
          try {
            r[1].validator(item, call);
          } catch (error) {
            errMessage = error;
          }
        }
        if (errMessage) break;
      }
    }
    submitting(errMessage, store);
  }
}

export default function createFieldsStore(fields) {
  return new FieldStore(fields);
}
demo使用
import { Button, Toast, Picker, List } from "antd-mobile";
import Form from "./components/Form";
import React from "react";

@Form.create({
  callback_onErr: (mess) => {
    console.log(mess)
    Toast.fail(mess)
  }
})
class Demo extends React.Component {

  submit = () => {
    const { form: { validateFields } } = this.props;
    validateFields((err, fieldsValue) => {
      if (err) return;
      console.log(fieldsValue, "----------表单数据集合")
    })
  }
  render() {
    const { form: { getFieldDecorator, getFieldValue, store } } = this.props;
    return (
      <div>
        <div>滑动下拉选项:
          {
            getFieldDecorator("a", {
              initialValue: [1],
              rules: [{ required: true, message: "请选择滑动下拉选项" }],
            })(
              <Picker
                data={[{ label: "100", value: 100 }, { label: "200", value: 200 }]}
                cols={1}
              >
                <List.Item arrow="horizontal"></List.Item>
              </Picker>
            )
          }
        </div>
        <div> 滑动下拉选项===200 的时候我才有表单:
          {
            getFieldValue('a') == 200 ? getFieldDecorator("bbbbbbbbb", {
              initialValue: '1234567890',
              rules: [{ required: true, message: "此处不能为空" }],
            })(
              <input placeholder="请输入" />
            ) : ''
          }
        </div>
        <Button onClick={this.submit}>提交</Button>
      </div>
    )
  }
}

<<代码git地址>>

高阶组件的多层嵌套势必会影响整体页面的渲染性能,那么hooks呢,利用hooks去实现的话,很难将组件和表单规则绑定在一起: 高阶组件可以实现如下:

          {
            getFieldDecorator("bbbbbbbbb", {
              initialValue: '1234567890',
              rules: [{ required: true, message: "此处不能为空" }],
            })(
              <input placeholder="请输入" />
              )
          }

对于hooks的实现设想, 还是需要通过一层高阶组件

      const { formStore,FormBox } = useForm()
      function Index () {
         const [value, setValue] = React.useState(1234567890)
         reurn (
            <FormBox 
               formItem={(
                   <input placeholder="请输入" />
               )}
               initialValue={value}
               rules={[{ required: true, message: "此处不能为空" }]}
               formKey="phone"
            />
        )
      }