Den Form

为什么叫 Den Form ? 可能是因为 丹凤眼 非常迷人吧...

一个非常轻巧的 Form 实现, gzip 体积只有 3kb, 可以很方便跨越组件层级获取表单对象, 或者管理联动更新

大部分情况下,我们不需要表单组件,具体请阅读笔者的另一篇《我们或许不需要 React 的 Form 组件》

安装

ymzuiku/react-den-form
React中轻巧的Form实现. Contribute to ymzuiku/react-den-form development by creating an account on GitHub.
$ yarn add react-den-form 

基础使用

Form 组件会在有 field 属性的子组件上注入 onChange 事件, 并获取其中的值

field 属性是 Form 组件用于标记哪一类子组件需要被监管的字段, 并且也是用于校验更新的 key

field 可以重复, 但是如果两个子组件拥有相同的 field 值, 那么当此类 field 需要更新时, 两个子组件都会进行更新

import React from "react";
import Form from "react-den-form";

export default () => {
  return (
    <div>
      <Form onSubmit={({ data }) => console.log(data)}>
        <input field="userName" />
      </Form>
    </div>
  );
};

当我们输入数据后, 按回车键, onSubmit 方法打印如下:

{userName: "333"}

层级获取数据不需要进行额外处理

import React from "react";
import Form from "react-den-form";

export default () => {
  return (
    <div>
      <Form onChange={({ data }) => console.log(data)}>
        <div>
          <div>
            <div>
              <input field="userName" />
            </div>
            <input field="password" />
          </div>
        </div>
      </Form>
    </div>
  );
};

当我们输入数据时, onChange 方法打印如下:

{userName: "333", password: "555"}

Form 表单嵌套不需要处理

有时候, 我们会有一些页面结构让两个不同的表单进行嵌套, 如登录时, 验证码的输入框在用户名和密码中间, 而验证码有单独的请求.
当然, 我们可以更换实现方式, 但是对于这类场景, DenForm 默认处理了表单嵌套.

由于 Form 内部有一个 form 标签, 外层 onSubmit 会捕获所有子组件的 onSubmit 事件, 但是 data 数据只会捕获 当前层级内的 field 对象

export default () => {
  return (
    <div>
      {/* 此 Form 只会捕获 userName及password, code被子Form拦截了 */}
      <Form onSubmit={({ data }) => console.log("1", data)}>
        <input field="userName" />
        {/* 此 Form 只会捕获 age */}
        <Form onSubmit={({ data }) => console.log("2", data)}>
          <input field="code" />
          <button type="submit">此Submit会被最近的父级捕获</button>
        </Form>
        <input field="password" />
      </Form>
    </div>
  );
};

跨组件的值获取

  1. 为对象标记一个 toform 属性, 会为对象注入一个 ToForm 组件
  2. 然后使用 ToForm 组件在对象内部对表单进行概括
import React from "react";
import Form from "react-den-form";

// 此对象会被注入 ToForm 组件
function SubInput({ ToForm }) {
  return (
    <div>
      <div>
      <ToForm>
        <input field="subPassword" />
      </ToForm>
      </div>
    </div>
  );
}

export default () => {
  return (
    <div>
      <Form onChange={({ data }) => console.log(data)}>
        <div>
          <div>
            <div>
              <input field="userName" />
            </div>
            <input field="password" />
            <SubInput toform />
          </div>
        </div>
      </Form>
    </div>
  );
};

当我们输入数据时, onChange 方法打印如下:

{userName: "333", password: "555", subPassword: "666"}

自定义 Field 组件

如果我们自己定义的特殊组件, 需要满足两个条件:

  1. 组件外部的 props 需要设置 field 属性
  2. 组件内部需要使用 this.props.onChange 返回数据
import React from "react";
import Form from "react-den-form";

class SubInput extends React.Component {
  handleOnChange = e => {
    // 需要使用 this.props.onChange 返回数据
    this.props.onChange(e.target.value);
  };

  render() {
    return <input onChange={this.handleOnChange} />;
  }
}

export default () => {
  return (
    <div>
      <Form onChange={({ data }) => console.log(data)}>
        {/* 需要设置 field 属性 */}
        <SubInput field="userName" />
      </Form>
    </div>
  );
};

:art: 使用 onChangeGetter 获取自定义组件的值

以下标签, From 会自动识别 onChange 的返回值, 进行解析获取

import React from "react";
import Form from "react-den-form";

export default () => {
  return (
    <div>
      <Form onChange={(...args) => console.log(args)}>
        <input field="userName" />
        <textarea field="password" />
        <select field="loginType">
          <option value="signUp">Sign up</option>
          <option value="signIn">Sign in</option>
        </select>
      </Form>
    </div>
  );
};

我们自己定义的特殊组件, 如果它们的 onChange 的返回值结构不确定, 我们可以编写 onChangeGetter 属性:

import React from "react";
import Form from "react-den-form";

class SubInput extends React.Component {
  // 假定数据有一定的层级
  inputData = {
    value: ""
  };

  handleOnChange = e => {
    this.inputData.value = e.target.value;
    this.props.onChange(this.inputData);
  };

  render() {
    return <input onChange={this.handleOnChange} />;
  }
}

export default () => {
  return (
    <div>
      <Form onChange={({ data }) => console.log(data)}>
        <SubInput field="userName" onChangeGetter={e => e.value} />
      </Form>
    </div>
  );
};

onChangeGetter 的默认值相当于 onChangeGetter={e => e}

表单提交

以下三个情形为都会触发 Form 的 onSubmit 函数:

  • 包含 field 属性的对象中, 使用键盘的回车键
  • 包含 submit 属性, 点击(onClick)
  • 包含 type="submit" 属性的对象中, 点击(onClick)
import React from "react";
import Form from "react-den-form";

export default () => {
  return (
    <div>
      <Form onSubmit={({ data }) => console.log(data)}>
        <input field="userName" />
        <input submit field="password" />
        <button type="submit" />
      </Form>
    </div>
  );
};

异步请求提交

Form 表单内部并无封装请求行为, 请在 onSubmit 事件中自行处理, 如:

import React from "react";
import Form from "react-den-form";

function fetchLogin({ data }) {
  fetch("/api/login", { method: "post", body: JSON.stringify(data) })
    .then(res => {
      return res.json();
    })
    .then(data => {
      console.log(data);
    });
}

export default () => {
  return (
    <div>
      <Form onSubmit={fetchLogin}>
        <input field="userName" />
        <input field="password" />
      </Form>
    </div>
  );
};

上下文获取数据

我们为 Form 显式注入一个 data, 当数据变化时, data 的值也会变化, 这样可以在上下文获取 Form 的数据

// React.Component 版本
import React from "react";
import Form from "react-den-form";

export default class extends React.Component {
  data = {};

  render() {
    return (
      <div>
        <Form data={this.data}>
          <input field="userName" />
          <button onClick={() => console.log(this.data)}>show-data</button>
        </Form>
      </div>
    );
  }
}
// useHooks 版本
import React, { useState } from "react";
import Form from "react-den-form";

export default () => {
  const [data] = useState({});

  return (
    <div>
      <Form data={data}>
        <input field="userName" />
        <button onClick={() => console.log(data)}>show-data</button>
      </Form>
    </div>
  );
};

输入数据, 点击 button, data 数据打印如下:

{ userName: "dog" }

表单校验

表单校验是无痛的, 并且是高效的

我们给 input 组件添加 errorcheck 属性, 该属性可以是一个正则对象, 也可以是一个  函数, 如果 errorcheck 校验的结果为 false, 就会将其他 error 相关的属性赋予至组件中

如下代码, 如果 input 内容不包含 123, 字体颜色为红色:

import "./App.css";
import React from "react";
import Form from "react-den-form";

export default () => {
  return (
    <div>
      <Form>
        <input
          field="userName"
          errorcheck={/123/}
          errorstyle={{ color: "#f00" }}
        />
      </Form>
    </div>
  );
};

表单校验相关的 api:

prop类型用途errorcheck正则或函数若返回值为 false, 将其他 error Api 应用至组件中errorstylestyle 对象若校验为错误, 将 errorstyle 合并至 style 中errorclassclassName 字符串若校验为错误, 将 errorstyle 合并至 className 中errorpropsprops 对象若校验为错误, 将 errorprops 合并至整个 props 中

为什么是 errorcheck 而不是 errorCheck ? 这是因为 React 对 DOM 元素属性的定义为 lowercass:

Warning: React does not recognize the `errorCheck` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `errorcheck` instead. If you accidentally passed it from a parent component, remove it from the DOM element.

表单校验时进行特殊处理

如果我们有一个需求, 当表单校验错误时, 显示一个提示信息, 当表单校验通过时, 取消提示信息, 我们就需要对每次校验有差异时, 进行处理

使用 onChange 方法每次都会被执行, 可是我们只希望在表单校验结果有变化时进行提示

Form 提供了一个 onErrorCheck 的属性, 满足以上需求

import React from "react";
import Form from "react-den-form";

export default () => {
  return (
    <div>
      {/* 只有当 input内容校验结果发生变化时, onErrorCheck 才会执行 */}
      <Form onErrorCheck={({ isError, data }) => console.log(isError, data)}>
        <input
          field="userName"
          errorcheck={/123/}
          errorstyle={{ color: "#f00" }}
        />
      </Form>
    </div>
  );
};

联动

当我们修改一个对象时, 根据某些条件, 希望修改另一个对象的行为我们称之为联动

DenForm 的联动是高性能的, 仅更新需要更新的对象

我们可以在任何 Form 的回调函数中使用 update 进行更新某个被 field 捆绑的组件的 value 或者 props

下面这个例子: 1. 当在 password 输入时, 会将 userName 的 input 框内容改为 'new value'; 2. 当 userName 的 input 的内容包含 'aa' 时, 会将 password 的 value 和 style 进行修改;

import React from "react";
import Form from "react-den-form";

export default () => {
  return (
    <div>
      <Form
        onChange={({ data, field, update }) => {
          if (field === "password") {
            update({ userName: "new value" });
          }
          if (/aa/.test(data.userName)) {
            update({
              password: {
                value: "new value and style",
                style: { fontSize: 30 }
              }
            });
          }
        }}
      >
        <input field="userName" />
        <input field="password" />
      </Form>
    </div>
  );
};

性能开销

Form 存在的意义在于简化开发, 用计算机的时间换取开发者的时间, 所以会有一些性能开销.

但是 Form 的开销绝对不大, 因为 Form 内部更新时只会针对指定的子组件进行更新.

  1. 每个包含 field 属性的子组件都相当于一个受控组件, 当子组件 onChange 时, 此子组件会进行更新
  2. Form 组件声明或被外部更新时会去查询当前 JSX 对象中的所有子组件是否包含 field 或者 submit 属性, 如果包含, 则注入 onChange 或 onClick; 如果不希望 Form 被外部更新, 请声明 <Form shouldUpdate={false} >{...}</Form>

如果因为使用 Form 遇到了性能问题, 请检查以下情况:

  • 请减少 Form 内部子组件的个数, 最好不要超过 100 个
  • 在一个无限长的滚动列表外包裹 Form 时, 请尽量使用 react-virtualized 或 react-window 类型的虚拟 List 组件, 以减少 Form 包裹的内容个数
  • 如果 Form 子组件的个数过多时, 请确保 Form 组件不会由外部频繁更新, 或者添加 shouldUpdate={false} 至 Form 中: <Form shouldUpdate={false} >{...}</Form>

我们有理由相信, 在一个设计合理的应用中, 每个 Form 包裹的组件个数应该是有限的

支持哪些 React 渲染层 ?

此库支持所有 React 的渲染层, 如 ReactDOM, ReactNative, ReactVR, 但是非 ReactDOM 中, 需要初始化事件类型

如 ReactNative 中, 在项目之初设定:

import { immitProps } from "react-den-form";

// 设定 ReactNative 中的读取值和更新值的监听属性:
immitProps.value = "value";
immitProps.change = "onChange";
immitProps.click = "onPress";

API

Form API

IEvents API (回调函数的参数)

Form 组件回调函数的参数如下: ({isError, event, data, field, value, element, update }) => void

具体的 API 描述如下:

子组件关联参数

一个子组件可以被识别关联的参数如下

<input
  field="userName"
  submit
  type="submit"
  errorcheck={/123/}
  errorstyle={{ color: "#f00" }}
  errorclass="input-error-style"
  errorprops={{ disable: true }}
/>

具体的 API 描述如下:

以上就是全部, 希望 Den Form 能够帮到你解决 React 表单相关的痛点 :)