React 01 - 简单实现rc-field-form

先看下怎么使用rc-field-form,创建myRCFieldForm.js

import React, {Component, useEffect} from "react";
import Form, {Field} from "rc-field-form";
// import Form, {Field} from "../components/my-rc-field-form/";
import Input from "../components/Input";
const nameRules = {required: true, message: "请输入姓名!"};
const passworRules = {required: true, message: "请输入密码!"};
export default function MyRCFieldForm(props) {
  const [form] = Form.useForm();
  const onFinish = val => {
    console.log("onFinish", val); //sy-log
  };
  // 表单校验失败执行
  const onFinishFailed = val => {
    console.log("onFinishFailed", val); //sy-log
  };
  useEffect(() => {
    console.log("form", form); //sy-log
    //form.setFieldsValue({username: "default"});
  }, []);
  return (
    <div>
      <h3>MyRCFieldForm</h3>
      <Form form={form} onFinish={onFinish} onFinishFailed={onFinishFailed}>
        <Field name="username" rules={[nameRules]}>
          <Input placeholder="input UR Username" />
        </Field>
        <Field name="password" rules={[passworRules]}>
          <Input placeholder="input UR Password" />
        </Field>
        <button>Submit</button>
      </Form>
    </div>
  );
}

看下我们大概需要实现哪些API,有Form,Field,useForm等。那么我们先来搭一个简单的架子。

1.搭架子

在components里面新建一个目录my-rc-field-form,创建index.js,Form.js,Field.js,useForm.js。

Form.js(函数式组件)
import React from "react";

export default function Form({ children }) {
  return <form>{children}</form>;
}


Field.js
import React, {Component} from "react";

export default class Field extends Component {

  render() {
    const {children} = this.props;
    return children;
  }
}

useForm.js(自定义hook)
export default function useForm() {
  return [];
}
index.js
import _Form from "./Form";
import Field from "./Field";
import useForm from "./useForm";

const Form = _Form;

export {Field, useForm};

export default Form;

在myRCFieldForm.js引入我们组件的js文件

import React, {Component, useEffect} from "react";
// import Form, {Field} from "rc-field-form";
import Form, {Field} from "../components/my-rc-field-form/";

2.实现数据的存储

首先,实现Field.js,给input组件添加value属性和onChange事件

// Field.js
getControled = () => {
    return {
      value: '', //从store中取值
      onChange: e => {
        // 把新的参数值存到store中
        const newValue = e.target.value;
        console.log("newValue", newValue); //sy-log
      }
    };
  };
  render() {
    const {children} = this.props;
    const returnChildNode = React.cloneElement(children, this.getControled());
    return returnChildNode;
  }

解决input组件value的存储,如果把值存储在单个组件中,就没法进行通信,所以需要有一个store,可以存储所有组件的value。
在useForm.js,增加FormStore,用于存储表单的数据。

// useForm.js
import React, {useRef} from "react";
class FormStore {
  constructor(props) {
    this.store = {};
  }

  getFieldValue = name => {
    return this.store[name];
  };

  getFieldsValue = name => {
    return this.store;
  };

  // 修改store
  setFieldsValue = newStore => {
    this.store = {
      // ! 调整下顺序
      ...newStore,
      ...this.store
    };
  };


  getForm = () => {
    return {
      getFieldValue: this.getFieldValue,
      getFieldsValue: this.getFieldsValue,
      setFieldsValue: this.setFieldsValue
    };
  };
}

export default function useForm(form) {
  const formRef = useRef();
  if (!formRef.current) {
    if (form) {
      formRef.current = form;
    } else {
      const formStore = new FormStore();
      formRef.current = formStore.getForm();
    }
  }
  return [formRef.current];
}

store已经有了,那我们怎么让所有组件都可以共用这个store呢,答案就是context。
新建FieldContext.js

import React from "react";
const FieldContext = React.createContext();
export default FieldContext;

有了context,接下来改造form.js,让子组件都能共用store。

import React from "react";
import useForm from "./useForm";
import FieldContext from "./FieldContext";

export default function Form({form, children, onFinish, onFinishFailed}) {
  const [formInstance] = useForm(form);
  formInstance.setCallback({
    onFinish,
    onFinishFailed
  });
  return (
    <form
      onSubmit={e => {
        e.preventDefault();
        formInstance.submit();
      }}>
      <FieldContext.Provider value={formInstance}>
        {children}
      </FieldContext.Provider>
    </form>
  );
}

这时子组件也能访问store了,我们来改造Field.js,完成value的读取和设置。


export default class Field extends Component {
  static contextType = FieldContext;

  getControled = () => {
    const {getFieldValue, setFieldsValue} = this.context;
    const {name} = this.props;
    return {
      value: getFieldValue(name), //从store中取值
      onChange: e => {
        // 把新的参数值存到store中
        const newValue = e.target.value;
        setFieldsValue({[name]: newValue});
        console.log("newValue", newValue); //sy-log
      }
    };
  };
...
}

value的读取已经完成,但是组件不会更新,在设置完成后需要对组件进行更新。

3.状态更新

主要解决两个问题,一个是组件如何更新,一个是组件何时更新。

  • 组件如何更新,可以用forceUpdate
    给Field.js增加onStoreChange方法
//Field.js
...
  onStoreChange = () => {
    this.forceUpdate();
  };
...
  • 组件何时更新
    在调用sotre.setFieldsValue时,store通知对应的组件更新,这时,我们就需要用一个变量FieldEntities来保存所有的组件实例。在组件挂载的时候注册到store里面,当组件卸载的时候要把store对应的组件实例卸载掉。
//useForm.js
import React, {useRef} from "react";
class FormStore {
  constructor(props) {
    this.store = {};
    this.fieldEntities = [];
  }
// 注册组件实例,返回一个回调函数,用于删除组件实例
  registerField = field => {
    this.fieldEntities.push(field);
    return () => {
      this.fieldEntities = this.fieldEntities.filter(item => item != field);
      delete this.store[field.props.name];
    };
  };

  // 修改store
  setFieldsValue = newStore => {
    this.store = {
      // ! 调整下顺序
      ...newStore,
      ...this.store
    };
    // store已经更新,但是我们希望组件也跟着更新
    this.fieldEntities.forEach(enetity => {
      const {name} = enetity.props;
      Object.keys(newStore).forEach(key => {
        if (key === name) {
          enetity.onStoreChange();
        }
      });
    });
    console.log("store", this.store); //sy-log
  };
// Field.js
import React, {Component, cloneElement} from "react";
import FieldContext from "./FieldContext";

export default class Field extends Component {
  static contextType = FieldContext;
  componentDidMount() {
 // 注册组件实例
    this.cancelRegister = this.context.registerField(this);
  }

  componentWillUnmount() {
// 卸载组件实例
    if (this.cancelRegister) {
      this.cancelRegister();
    }
  }

4.实现表单提交和校验

给Form组件实现submit事件,由于表单的默认submit事件会刷新页面,所以需要阻止默认事件。由于数据都保存在store里面,需要对store的数据进行校验,我们把submit方法放在FormStore里面实现,校验完后还要执行回调方法,所以还要增加一个setCallback方法,用于保存成功和失败的回调函数。

//Form.js
import React from "react";
import useForm from "./useForm";
import FieldContext from "./FieldContext";

export default function Form({form, children, onFinish, onFinishFailed}) {
  const [formInstance] = useForm(form);
  formInstance.setCallback({
    onFinish,
    onFinishFailed
  });
  return (
    <form
      onSubmit={e => {
        e.preventDefault();
        formInstance.submit();
      }}>
      <FieldContext.Provider value={formInstance}>
        {children}
      </FieldContext.Provider>
    </form>
  );
}

FormStore的setCallback和submit的实现。

//useForm.js
import React, {useRef} from "react";
class FormStore {
  constructor(props) {
    this.store = {};
    this.fieldEntities = [];
    this.calllbacks = {}; // 保存成功和失败的回调函数
  }
...
// 设置回调方法
  setCallback = callback => {
    this.calllbacks = {
      ...this.calllbacks,
      ...callback
    };
  };
// 表单校验
  validate = () => {
    let err = [];
    // todo
    this.fieldEntities.forEach(entity => {
      const {name, rules} = entity.props;
      let value = this.store[name];
      let rule = rules && rules[0];
      if (rule && rule.required && (value === undefined || value === "")) {
        //  出错
        err.push({
          [name]: rules.message,
          value
        });
      }
    });
    return err;
  };
// 表单提交
  submit = () => {
    let err = this.validate();

    // 成功
    if (err.length === 0) {
      this.calllbacks.onFinish(this.store);
    } else if (err.length > 0) {
    // 失败
      this.calllbacks.onFinishFailed(err);
    }
  };
  getForm = () => {
    return {
      setCallback: this.setCallback,
      submit: this.submit,
      registerField: this.registerField,
      getFieldValue: this.getFieldValue,
      getFieldsValue: this.getFieldsValue,
      setFieldsValue: this.setFieldsValue
    };
  };
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342