文档章节

小而美的 React Form 组件

漆工
 漆工
发布于 2017/09/10 10:36
字数 3085
阅读 357
收藏 5

背景

之间在一篇介绍过 Table 组件《React 实现一个漂亮的 Table》 的文章中讲到过,在企业级后台产品中,用的最多且复杂的组件主要包括 Table、 Form、 Chart,在处理 Table 的时候我们遇到了很多问题。今天我们这篇文章主要是分享一下 Form 组件,在业务开发中, 相对 Table 来说,Form 处理起来更麻烦,不是所有表单都像注册页面那样简单,它往往需要处理非常多的逻辑,比如:

  • 需要处理数据验证逻辑。
  • 在一个项目中的很少有可以复用的表单,每个表单的逻辑都需要单独处理。
  • 在表单中往往存在需要自定义的组件,比如:Toggle、Tree、Picker 等等。

Form 作为一个功能型组件,它需要解决的问题无非就是两个:

  • 把一个数据对象扔给它,它能够在让 Form 内的交互型组件获取到数据并展示出来,同时这些组件上的数据也能反过来让 Form 组件获取到。
  • 对数据的有效性进行验证。

在 React 项目开发中没有用过其他 Form 相关的组件,但是这里我又忍不住想和 Ant Design 的 Form 对比一下,以下是我在 antd 官网上的一个截图:

antd form

大家可以通过以上图片看到,我想输入自己的中文名字都不能正常输入,这里主要存在两个问题:

  • onChange 被触发了多次,在一次中文未完整的输入前,不应该触发 onChange 事件。
  • 另外,中文在文本框内就不能正常显示。

我在想这两个问题不在组件上处理,在哪里处理的呢?访问地址: https://ant.design/components/form-cn/ 可以试一下,也希望他们可以解决掉这个问题。这个问题应该怎么解决,我之前做过记录: React 中, 在 Controlled(受控制)的文本框中输入中文 onChange 会触发多次

我们在设计的时候自然是解决了这些问题,预览效果: https://rsuitejs.com/form-lib/ ,接下来看一下具体的设计。

设计

针对前面提到 Form 需要解决的两个问题(数据校验和数据获取),在设计的时候,我们把这个两个问题作为两个功能,独立在不同的库处理。

  • form-lib 处理表单数据初始化,数据获取。
  • rsuite-schema 定义数据模型,做数据有效性验证。

这两个库可以独立使用,如果你有自己一套自己的 Form 组件,只是缺少一个数据验证的工具,那你可以单独把 rsuite-schema 拿过来使用。

Form 定义一个表单

分别看一下它们是怎么工作的,form-lib 用起来比较简单,提供了两个组件:

  • <Form> 处理整个表单服务的逻辑
  • <Field> 处理表单中各个交互型组件的逻辑,比如 input,select

看一个示例:

安装

首先需要安装 form-lib

npm i form-lib --save

示例代码

import { Form, Field, createFormControl } from 'form-lib';
const SelectField = createFormControl('select');

const user = {
  name:'root',
  status:1
};

<Form data={user}>
  <Field name="name"   />
  <Field name="status" accepter={SelectField} >
    <option value={1}>启用</option>
    <option value={0}>禁用</option>
  </Field>
</Form>

在默认情况下 Field 是一个文本输入组件,如果你需要使用 HTML 表单中其他的标签,你可以像上面示例一样 通过 createFormControl(标签名称) 方法创建一个组件, 在把这个组件通过 accepter 属性赋给Field 组件,这样你就能像操作原生 HTML 一样设置 Field。 通过这种方式,后面在功能介绍的时候会讲到怎么把自定义组件放在 Field 中。

自定义布局

这里存在一个疑问,<Field> 必须放在 <Form> 下面第一层吗? 如果对布局没有要求,设计成这样是处理起来最方便的,因为在 <Form> 中可以直接通过 props.children 获取到所有的 <Field>,想怎么处理都可以。 刚开始我们是这样的,后来在实际应用中发现,表单的布局是有很多种,如果要设计成这样,那肯定就带来一个问题,不好自定义布局。 所以这里 <Form><Field> 之间的通信我们用的是 React 的 context。 这样的话你就可以任意布局:

<Form data={user}>
  <div className="row">
    <Field name="name"   />
  </div>
  <div className="row">
    <Field name="status" accepter={SelectField} >
      <option value={1}>启用</option>
      <option value={0}>禁用</option>
    </Field>
  </div>
</Form>

Form Props

<Form><Field> 详细 API 说明,参考以下表格

<Form> Props :

名称类型描述
horizontalbool设置表单内的元素左右两栏布局
inlinebool设置表单内元素在一行布局
valuesobject表单的值 受控组件
defaultValuesobject表单的初始默认值 非受控组件
modelSchemarsuite-schema 对象
checkDelaynumber数据校验的时候,延迟处理,默认为 500 毫秒
checkTriggerstring数据校验的触发类型,可选项: changeblurnull,默认为:change
onChangefunction(values:Object, event:Object)数据改变后的回调函数
onErrorfunction(errors:Object)校验出错的回调函数
onCheckfunction(errors:Object)数据校验的回调函数
errorsobject表单错误信息

<Field> Props :

名称类型描述
namestring表单元素名称
accepterelementType受代理的组件

Schema 定义一个数据模型

安装

npm i rsuite-schema --save

rsuite-schema 主要有两个对象

  • SchemaModel 用于定义数据模型。
  • Type 用于定义数据类型,包括:
    • StringType
    • NumbserType
    • ArrayType
    • DateType
    • ObjectType
    • BooleanType

这里的 Type 有点像 React 中 PropTypes 的定义。

示例代码

一个示例:

const userModel = SchemaModel(
    username: StringType().isRequired('用户名不能为空'),
    email: StringType().isEmail('请输入正确的邮箱'),
    age: NumberType('年龄应该是一个数字').range(18, 30, '年应该在 18 到 30 岁')
});

这里定义了一个 userModel, 包含 usernameemailage 3个字段, userModel 拥有了一个 check 方法, 当把数据扔进去后会返回验证结果:

const checkResult = userModel.check({
    username: 'foobar',
    email: 'foo@bar.com',
    age: 40
})

// checkResult 结果:
/**
  {
      username: { hasError: false },
      email: { hasError: false },
      age: { hasError: true, errorMessage: '年应该在 18 到 30 岁' }
  }
**/

多重验证

StringType()
  .minLength(6,'不能少于 6 个字符')
  .maxLength(30,'不能大于 30 个字符')
  .isRequired('该字段不能为空');

自定义验证

通过 addRule 函数自定义一个规则。 如果是对一个字符串类型的数据进行验证,可以通过 pattern 方法设置一个正则表达式进行自定义验证。

const myModel = SchemaModel({
    field1: StringType().addRule((value) => {
        return /^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/.test(value);
    }, '请输入合法字符'),
    field2: StringType().pattern(/^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/, '请输入合法字符')
});

自定义动态错误信息

例如,要通过值的不同情况,返回不同的错误信息,参考以下

const myModel = SchemaModel({
    field1: StringType().addRule((value) => {
        if(value==='root'){
          return {
            hasError: true,
            errorMessage:'不能是关键字 root'
          }
        }else if(!/^[a-zA-Z]+$/.test(value)){
          return {
            hasError: true,
            errorMessage:'只能是英文字符'
          }
        }

        return {
            hasError: false
        }
    })
});

复杂结构数据验证

const userModel = SchemaModel({
  username:StringType().isEmail('正确的邮箱地址').isRequired('该字段不能为空'),
  tag: ArrayType().of(StringType().rangeLength(6, 30, '字符个数只能在 6 - 30 之间')),
  profile: ObjectType().shape({
    email: StringType().isEmail('应该是一个 email'),
    age: NumberType().min(18, '年龄应该大于18岁')
  })
})

更多设置,可以查看 rsuite-schema API 文档

Form 与 Schema 的结合

const userModel = SchemaModel({
    username: StringType().isRequired('用户名不能为空'),
    email: StringType().isEmail('请输入正确的邮箱'),
    age: NumberType('年龄应该是一个数字').range(18, 30, '年应该在 18 到 30 岁')
});
<Form model={userModel}>
  <Field name="username" />
  <Field name="email" />
  <Field name="age" />
</Form>

把定义的的SchemaModel对象赋给,<Form>model 属性,就把它们绑定起来了,<Field>name 对应 SchemaModel 对象中的 key

以上的示例代码是不完整的,没有处理错误信息和获取数据,只是为了方便大家理解。完整的示例,可以参考接下来的实践与解决方案。

实践与解决方案

一个完整的示例

import React from 'react';
import { Form, Field, createFormControl } from 'form-lib';
import { SchemaModel, StringType } from 'rsuite-schema';

const TextareaField = createFormControl('textarea');
const SelectField = createFormControl('select');

const model = SchemaModel({
  name: StringType().isEmail('请输入正确的邮箱')
});

class DefaultForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      values: {
        name: 'abc',
        status: 0
      },
      errors: {}
    };
    this.handleSubmit = this.handleSubmit.bind(this);
  }
  handleSubmit() {
    const { values } = this.state;
    if (!this.form.check()) {
      console.error('数据格式有错误');
      return;
    }
    console.log(values, '提交数据');
  }
  render() {
    const { errors, values } = this.state;
    return (
      <div>
        <Form
          ref={ref => this.form = ref}
          onChange={(values) => {
            console.log(values);
            this.setState({ values });
            // 清除表单所有的错误信息
            this.form.cleanErrors();
          }}
          onCheck={(errors) => {
            this.setState({ errors });
          }}
          values={values}
          model={model}
        >
          <div className="form-group">
            <label>邮箱: </label>
            <Field name="name" className="form-control" />
            <span className="help-block error" style={{ color: '#ff0000' }}>
              {errors.name}
            </span>
          </div>

          <div className="form-group">
            <label>状态: </label>
            <Field name="status" className="form-control" accepter={SelectField} >
              <option value={1}>启用</option>
              <option value={0}>禁用</option>
            </Field>
          </div>

          <div className="form-group">
            <label>描述 </label>
            <Field name="description" className="form-control" accepter={TextareaField} />
          </div>
          <button onClick={this.handleSubmit}> 提交 </button>
        </Form>
      </div>
    );
  }
}

export default DefaultForm;

在 rsuite 中的应用

rsuite 提供了很多 Form 相关的组件,比如 FormGroup,FormControl,ControlLabel,HelpBlock 等等, 我们通过一个例子看一下怎么结合使用。

通过上一个例子中我们可以看到,没有个 Field 中有很多公共部分,所以我们可以自定义一个无状态组件 CustomField,把 ControlLabel,Field,HelpBlock 这些表单元素都放在一起。


import React from 'react';
import { Form, Field, createFormControl } from 'form-lib';
import { SchemaModel, StringType, ArrayType } from 'rsuite-schema';
import {
  FormControl,
  Button,
  FormGroup,
  ControlLabel,
  HelpBlock,
  CheckboxGroup,
  Checkbox
} from 'rsuite';

const model = SchemaModel({
  name: StringType().isEmail('请输入正确的邮箱'),
  skills: ArrayType().minLength(1, '至少应该会一个技能')
});

const CustomField = ({ name, label, accepter, error, ...props }) => (
  <FormGroup className={error ? 'has-error' : ''}>
    <ControlLabel>{label} </ControlLabel>
    <Field name={name} accepter={accepter} {...props} />
    <HelpBlock className={error ? 'error' : ''}>{error}</HelpBlock>
  </FormGroup>
);

class DefaultForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      values: {
        name: 'abc',
        skills: [2, 3],
        gender: 0,
        status: 0
      },
      errors: {}
    };
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleSubmit() {
    const { values } = this.state;
    if (!this.form.check()) {
      console.error('数据格式有错误');
      return;
    }
    console.log(values, '提交数据');
  }

  render() {
    const { errors, values } = this.state;
    return (
      <div>
        <Form
          ref={ref => this.form = ref}
          onChange={(values) => {
            this.setState({ values });
            console.log(values);
          }}
          onCheck={errors => this.setState({ errors })}
          defaultValues={values}
          model={model} >

          <CustomField
            name="name"
            label="邮箱"
            accepter={FormControl}
            error={errors.name}
          />

          <CustomField
            name="status"
            label="状态"
            accepter={FormControl}
            error={errors.status}
            componentClass="select"
          >
            <option value={1}>启用</option>
            <option value={0}>禁用</option>
          </CustomField>

          <CustomField
            name="skills"
            label="技能"
            accepter={CheckboxGroup}
            error={errors.skills}
          >
            <Checkbox value={1}>Node.js</Checkbox>
            <Checkbox value={2}>Javascript</Checkbox>
            <Checkbox value={3}>CSS 3</Checkbox>
          </CustomField>

          <CustomField
            name="gender"
            label="性别"
            accepter={RadioGroup}
            error={errors.gender}
          >
            <Radio value={0}>男</Radio>
            <Radio value={1}>女</Radio>
            <Radio value={2}>未知</Radio>
          </CustomField>

          <CustomField
            name="bio"
            label="简介"
            accepter={FormControl}
            componentClass="textarea"
            error={errors.bio}
          />
          <Button shape="primary" onClick={this.handleSubmit}> 提交 </Button>
        </Form>
      </div>
    );
  }
}
export default DefaultForm;

自定义 Field

如果一个组件不是原生表单控件,也不是 RSuite 库中提供的基础组件,要在 form-lib 中使用,应该怎么处理呢? 只需要在写组件的时候实现以下对应的 API:

  • value : 受控时候设置的值
  • defalutValue: 默认值,非受控情况先设置的值
  • onChange: 组件数据发生改变的回调函数
  • onBlur: 在失去焦点的回调函数(可选)

接下来我们使用 rsuite-selectpicker 作为示例, 在 rsuite-selectpicker 内部已经实现了这些 API。

import React from 'react';
import { SchemaModel, NumberType } from 'rsuite-schema';
import { Button, FormGroup, ControlLabel, HelpBlock } from 'rsuite';
import Selectpicker from 'rsuite-selectpicker';
import { Form, Field } from 'form-lib';

const model = SchemaModel({
  skill: NumberType().isRequired('该字段不能为空')
});

const CustomField = ({ name, label, accepter, error, ...props }) => (
  <FormGroup className={error ? 'has-error' : ''}>
    <ControlLabel>{label} </ControlLabel>
    <Field name={name} accepter={accepter} {...props} />
    <HelpBlock className={error ? 'error' : ''}>{error}</HelpBlock>
  </FormGroup>
);

class CustomFieldForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      values: {
        skill: 3,
      },
      errors: {}
    };
    this.handleSubmit = this.handleSubmit.bind(this);
  }
  handleSubmit() {
    const { values } = this.state;
    if (!this.form.check()) {
      console.error('数据格式有错误');
      return;
    }
    console.log(values, '提交数据');
  }
  render() {
    const { errors, values } = this.state;
    return (
      <div>
        <Form
          ref={ref => this.form = ref}
          onChange={(values) => {
            this.setState({ values });
            console.log(values);
          }}
          onCheck={errors => this.setState({ errors })}
          defaultValues={values}
          model={model}
        >


          <CustomField
            name="skill"
            label="技能"
            accepter={Selectpicker}
            error={errors.skill}
            data={[
              { label: 'Node.js', value: 1 },
              { label: 'CSS3', value: 2 },
              { label: 'Javascript', value: 3 },
              { label: 'HTML5', value: 4 }
            ]}
          />
          <Button shape="primary" onClick={this.handleSubmit}> 提交 </Button>
        </Form>
      </div>
    );
  }
}

export default CustomFieldForm;

更多示例:参考

如果你在使用中存在任何问题,可以提交 issues,如果你有什么好的想法欢迎你 pull request,GitHub地址:

© 著作权归作者所有

共有 人打赏支持
漆工
粉丝 8
博文 9
码字总数 8238
作品 1
杨浦
前端工程师
加载中

评论(1)

红薯
红薯
代码放到码云呗,我们来推荐
OSC 招聘 0x17 期:你会因为什么选择一家公司呢?

你会因为什么选择一家公司呢? 福利待遇好? 公司前景好? 牛逼的团队? 高大上的办公环境? 今天的小而美我们给大家带来的都是行业内有前途和钱途的公司,总有一种理由让你想去试试。 以下是...

oschina
2016/07/27
3.1K
7
OSC招聘::那些小而美的公司 0x5期

2016年3月21日英特尔前 CEO 安迪·格鲁夫 过世,享年 79 岁。没错,就是那位带领英特尔以「为 PC 时代的到来出一份力」的理念推出了 386 和 Pentium 处理器的人! 在1968年,安迪·格鲁夫成为...

翟志军
2016/03/23
4.2K
11
0x7.times {OSC招聘.那些小而美的公司}

以下是本期推荐的小而美的公司: 达达[上海] Sense Time 达达上线于2014年6月,专注于用移动和众包的方式解决O2O最大的痛点:最后三公里物流。 采取众包模式,达达在兼职配送员和有配送需求的...

oschina
2016/04/06
3.4K
6
func OSC招聘(0x6期) -> 那些小而美的公司

陌生人,我也为你祝福 愿你有一个灿烂的前程 愿你有情人终成眷属 愿你在尘世获得幸福 我只愿面朝大海,春暖花开 ——海子 《面朝大海,春暖花开》 以下是本期推荐的小而美的公司: SenseTim...

翟志军
2016/03/30
4.3K
10
fib(OSC招聘-小而美的公司推荐){fib(0x5-1) + fib(0x5-2)}

《人类简史》中,作者说人类以为自己驯化了小麦,但其实是小麦驯化了智人。如果我们用小麦这个观点来看程度猿发展史,那就是我们程序猿以为我们驾驭了编程语言、编程工具,其实是这些东西驾驭...

翟志军
2016/03/16
4.4K
11

没有更多内容

加载失败,请刷新页面

加载更多

将桌面捕获到虚拟摄像头

当然你可以直接用现成的虚拟摄像头软件实现这个功能。不过当初我开发这个插件的原因是,需要在Flash产品里面共享桌面,如果此时需要引导用户安装一个第三方的虚拟摄像头体验不好,所以公司希...

一个灰
2分钟前
0
0
Linux 配置网络绑定

1. 常见的网卡绑定驱动模式: mod=0 (balance-rr) Round-robin 衡抡循环策略 特点: 传输数据包顺序是依次传输(即:第1个包走eth0,下一个包就走eth1.一直循环下去,直到最后一个传输完毕),...

JeremyTown
11分钟前
0
0
VS code 编辑器使用技能

VS code 文档:https://code.visualstudio.com/docs/getstarted/locales 1、安装中文扩展包 打开扩展包按钮(最左上角从上往下数第5个按钮或者快捷键 Ctrl + Shift + X) 搜索语言包categor...

削个椰子皮_给个梨
20分钟前
0
0
Django 2.1.2项目中创建一个应用

Django 2.1.2项目中创建一个应用: 1.新建一个应用(app), 名称叫 learn python manage.py startapp learn # learn 是一个app的名称 2.在learn应用中修改视图文件views.py: # Create your vi...

MichaelShu
21分钟前
0
0
Swagger中配置了@ApiModelProperty的allowableValues属性但不显示的问题

现在用Swagger来生成API文档的例子已经非常多了,今天碰到开发同事问了一个问题,帮着看了一下,主要还是配置方法的问题,所以记录一下。如果您也碰到了同样的问题,希望本文对您有用。 问题...

程序猿DD
今天
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部