手把手教你实现一个常用的 antd form 组件

发表于 2年以前  | 总阅读数:341 次

Antd Form相信大家并不陌生,在中后台业务中,表单页面经常用到,但是大家知道它是如何设计和实现的吗?本文并不涉及具体源码分析,而是手把手带你实现一个简易版的Antd Form。

1、Form组件解决的问题

我们从官网摘下来一段Form代码,可以很清晰的看出一个简单的表单,主要是为了统一收集和校验组件的值。

    <Form
      onFinish={(values) => {
        console.log('values', values)
      }}
    >
      <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>
        <Button type="primary" htmlType="submit">
          Submit
        </Button>
      </Form.Item>
    </Form>

那么它是如何做到统一收集和校验呢?原理很简单,只需要通过监听表单组件的onChange事件,获取表单项的 value,根据定义的校验规则对 value 进行检验,生成检验状态和检验信息,再通过setState驱动视图更新,展示组件的值以及校验信息即可。

2、Antd Form 是怎么实现的

要实现上面的方案需要解决这几个问题:

  • 如何实时收集组件的数据?
  • 如何对组件的数据进行校验?
  • 如何更新组件的数据?
  • 如何跨层级传递传递
  • 表单提交

接下来我们就带着这几个问题,一起来一步步实现

3、目录结构

1659421573127.jpg

  • src/index.tsx用于放测试代码
  • src/components/Form文件夹用于存放Form组件信息
  • interface.ts用于存放数据类型
  • useForm存放数据仓库内容
  • index.tsx导出Form组件相关
  • FiledContext存放Form全局context
  • Form外层组件
  • Filed内层组件

4、数据类型定义

本项目采用ts来搭建,所以我们先定义数据类型;

// src/components/Form/interface.ts

export type StoreValue = any;
export type Store = Record<string, StoreValue>;
export type NamePath = string | number;

export interface Callbacks<Values = any> {
  onFinish?: (values: Values) => void;
}

export interface FormInstance<Values = any> {
  getFieldValue: (name: NamePath) => StoreValue;
  submit: () => void;
  getFieldsValue: () => Values;
  setFieldsValue: (newStore: Store) => void;
  setCallbacks: (callbacks: Callbacks) => void;
}

5、数据仓库

因为我们的表单一定是各种各样不同的数据项,比如input、checkbox、radio等等,如果这些组件每一个都要自己管理自己的值,那组件的数据管理太杂乱了,我们做这个也就没什么必要性了。那要如何统一管理呢?其实就是我们自己定义一个数据仓库,在最顶层将定义的仓库操作和数据提供给下层。这样我们就可以在每层都可以操作数据仓库了。数据仓库的定义,说白了就是一些读和取的操作,将所有的操作都定义在一个文件,代码如下:

// src/components/Form/useForm.ts

import { useRef } from "react";
import type { Store, NamePath, Callbacks, FormInstance } from "./interface";

class FormStore {
  private store: Store = {};
  private callbacks: Callbacks = {};

  getFieldsValue = () => {
    return { ...this.store };
  };

  getFieldValue = (name: NamePath) => {
    return this.store[name];
  };

  setFieldsValue = (newStore: Store) => {
    this.store = {
      ...this.store,
      ...newStore,
    };
  };

  setCallbacks = (callbacks: Callbacks) => {
    this.callbacks = { ...this.callbacks, ...callbacks };
  };

  submit = () => {
    const { onFinish } = this.callbacks;
    if (onFinish) {
      onFinish(this.getFieldsValue());
    }
  };

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

当然,数据仓库不能就这么放着,我们需要把里面的内容暴露出去。这里用ref来保存,来确保组件初次渲染和更新阶段用的都是同一个数据仓库实例;

// src/components/Form/useForm.ts

export default function useForm<Values = any>(
  form?: FormInstance<Values>
): [FormInstance<Values>] {
  const formRef = useRef<FormInstance>();
  if (!formRef.current) {
    if (form) {
      formRef.current = form;
    } else {
      const formStore = new FormStore();
      formRef!.current = formStore.getForm();
    }
  }
  return [formRef.current];
}

6、实时收集组件的数据

我们先来定义一下表单的结构,如下代码所示:

// src/index.tsx

import React from "react";
import Form, { Field } from "./components/Form";

const index: React.FC = () => {
  return (
    <Form
      onFinish={(values) => {
        console.log("values", values);
      }}
    >
      <Field name={"userName"}>
        <input placeholder="用户名" />
      </Field>
      <Field name={"password"}>
        <input placeholder="密码" />
      </Field>
      <button type="submit">提交</button>
    </Form>
  );
};

export default index;

定义了数据仓库,就要想办法在每一层都要拥有消费它的能力,所以这里在最顶层用context来跨层级数据传递。通过顶层的form将数据仓库向下传递,代码如下:

// src/components/Form/Form.tsx

import React from "react";
import FieldContext from "./FieldContext";
import useForm from "./useForm";
import type { Callbacks, FormInstance } from "./interface";

interface FormProps<Values = any> {
  form?: FormInstance<Values>;
  onFinish?: Callbacks<Values>["onFinish"];
}

const Form: React.FC<FormProps> = (props) => {
  const { children, onFinish, form } = props;

  const [formInstance] = useForm(form);
  formInstance.setCallbacks({ onFinish });

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        formInstance.submit();
      }}
    >
      <FieldContext.Provider value={formInstance}>
        {children}
      </FieldContext.Provider>
    </form>
  );
};

export default Form;

子组件来做存与取的操作。这里有个疑问,为什么不直接在input、radio这些组件上直接加入存取操作,非得在外面包一层Field(在正式的antd中是Form.Item)呢?这是因为需要在它基础的能力上扩展一些能力。

// src/components/Form/Field.tsx

import React, { ChangeEvent } from "react";
import FieldContext from "./FieldContext";
import type { NamePath } from "./interface";

const Field: React.FC<{ name: NamePath }> = (props) => {
  const { getFieldValue, setFieldsValue } = React.useContext(FieldContext);
  const { children, name } = props;

  const getControlled = () => {
    return {
      value: getFieldValue && getFieldValue(name),
      onChange: (e: ChangeEvent<HTMLInputElement>) => {
        const newValue = e?.target?.value;
        setFieldsValue?.({ [name]: newValue });
      },
    };
  };
  return React.cloneElement(children as React.ReactElement, getControlled());
};

export default Field;

这样我们就完成了数据收集以及保存的功能了。

很简单吧,我们来试一下onFinish操作!

接下来我们继续完善其他的功能。

7、完善组件渲染

我们来修改一下Form的代码,加入一条设置默认值:


// src/index.tsx

import React, { useEffect } from "react";
import Form, { Field, useForm } from "./components/Form";

const index: React.FC = () => {
  const [form] = useForm();

  // 新加入代码
  useEffect(() => {
    form.setFieldsValue({ username: "default" });
  }, []);

  return (
     // ...省略...
  );
};

export default index;

来看一眼页面,发现我们设置的默认值并没有展示在表单中,但是我们提交的时候还是可以打印出数据的,证明我们的数据是已经存入到store中了,只是没有渲染到组件中,接下来我们需要做的工作就是根据store变化完成组件表单的响应功能。

我们在useForm中加入订阅和取消订阅功能代码;

 // 订阅与取消订阅
  registerFieldEntities = (entity: FieldEntity) => {
    this.fieldEntities.push(entity);
    return () => {
      this.fieldEntities = this.fieldEntities.filter((item) => item !== entity);
      const { name } = entity.props;
      name && delete this.store[name];
    };
  };

forceUpdate的作用是进行子组件更新;

 // src/components/Form/Field.tsx

 // ...省略...
 const [, forceUpdate] = React.useReducer((x) => x + 1, 0);

  useLayoutEffect(() => {
    const unregister =
      registerFieldEntities &&
      registerFieldEntities({
        props,
        onStoreChange: forceUpdate,
      });
    return unregister;
  }, []);

// ...省略...

当然光是注册是不够的,我们需要在设置值的时候完成响应;

 // src/components/Form/useForm.tsx 

  setFieldsValue = (newStore: Store) => {
    this.store = {
      ...this.store,
      ...newStore,
    };

    // 新加入代码
    // update Filed
    this.fieldEntities.forEach((entity) => {
      Object.keys(newStore).forEach((k) => {
        if (k === entity.props.name) {
          entity.onStoreChange();
        }
      });
    });
  };

我们来看一下效果,发现组件已经将值更新啦;

8、加入校验功能

到现在为止,我们发现提交表单还没有校验功能。表单校验通过,则执行onFinish。表单校验的依据就是Field的rules,表单校验通过,则执行onFinish,失败则执行onFinishFailed。接下来我们来实现一个简单的校验。

修改代码结构

import React, { useEffect } from "react";
import Form, { Field, useForm } from "./components/Form";

const nameRules = { required: true, message: "请输入姓名!" };
const passworRules = { required: true, message: "请输入密码!" };

const index: React.FC = () => {
  const [form] = useForm();

  useEffect(() => {
    form.setFieldsValue({ username: "default" });
  }, []);

  return (
    <Form
      onFinish={(values) => {
        console.log("values", values);
      }}
      onFinishFailed={(err) => {
        console.log("err", err);
      }}
      form={form}
    >
      <Field name={"username"} rules={[nameRules]}>
        <input placeholder="用户名" />
      </Field>
      <Field name={"password"} rules={[passworRules]}>
        <input placeholder="密码" type="password" />
      </Field>
      <button type="submit">提交</button>
    </Form>
  );
};

export default index;

添加validateField方法进行表单校验。注意:此版本校验只添加了required校验,后续小伙伴们可以根据自己的需求继续完善哦!

 // src/components/Form/useForm.tsx 

 // ...省略...
 validateField = () => {
    const err: any[] = [];
    this.fieldEntities.forEach((entity) => {
      const { name, rules } = entity.props;
      const value: NamePath = name && this.getFieldValue(name);
      let rule = rules?.length && rules[0];
      if (rule && rule.required && (value === undefined || value === "")) {
        name && err.push({ [name]: rule && rule.message, value });
      }
    });

    return err;
  };

我们只需要在form提交的时候判断一下就可以啦;

submit = () => {
    const { onFinish, onFinishFailed } = this.callbacks;
    // 调用校验方法
    const err = this.validateField();
    if (err.length === 0) {
      onFinish && onFinish(this.getFieldsValue());
    } else {
      onFinishFailed && onFinishFailed(err);
    }
  };

密码为空时的实现效果;

账号密码都不为空时的实现效果;

做到这里,我们已经基本实现了一个Antd Form表单了,但是细节功能还需要慢慢去完善,感兴趣的小伙伴们可以接着继续向下做!

9、总结

其实我们在看Antd Form源码的时候会发现它是基于rc-field-form来写的。所以想继续向下写的小伙伴可以下载rc-field-form源码,边学习边写,这样就可以事半功倍了,攻克源码!

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/lK6zuAy7jDkAfL8tQuOQzA

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237236次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8071次阅读
 目录