之间在一篇介绍过 Table 组件《React 实现一个漂亮的 Table》 的文章中讲到过,在企业级后台产品中,用的最多且复杂的组件主要包括 Table、 Form、 Chart,在处理 Table 的时候咱们遇到了不少问题。今天咱们这篇文章主要是分享一下 Form 组件,在业务开发中, 相对 Table 来讲,Form 处理起来更麻烦,不是全部表单都像注册页面那样简单,它每每须要处理很是多的逻辑,好比:html
Form 做为一个功能型组件,它须要解决的问题无非就是两个:react
在 React 项目开发中没有用过其余 Form 相关的组件,可是这里我又忍不住想和 Ant Design 的 Form 对比一下,如下是我在 antd 官网上的一个截图:git
你们能够经过以上图片看到,我想输入本身的中文名字都不能正常输入,这里主要存在两个问题:github
onChange
事件。我在想这两个问题不在组件上处理,在哪里处理的呢?访问地址: ant.design/components/… 能够试一下,也但愿他们能够解决掉这个问题。这个问题应该怎么解决,我以前作过记录: React 中, 在 Controlled(受控制)的文本框中输入中文 onChange 会触发屡次。正则表达式
咱们在设计的时候天然是解决了这些问题,预览效果: rsuitejs.com/form-lib/ ,接下来看一下具体的设计。npm
针对前面提到 Form 须要解决的两个问题(数据校验和数据获取),在设计的时候,咱们把这个两个问题做为两个功能,独立在不一样的库处理。api
这两个库能够独立使用,若是你有本身一套本身的 Form 组件,只是缺乏一个数据验证的工具,那你能够单独把 rsuite-schema
拿过来使用。bash
分别看一下它们是怎么工做的,form-lib
用起来比较简单,提供了两个组件:antd
<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>
与 <Field>
详细 API 说明,参考如下表格
<Form>
Props :
名称 | 类型 | 描述 |
---|---|---|
horizontal | bool | 设置表单内的元素左右两栏布局 |
inline | bool | 设置表单内元素在一行布局 |
values | object | 表单的值 受控组件 |
defaultValues | object | 表单的初始默认值 非受控组件 |
model | Schema | rsuite-schema 对象 |
checkDelay | number | 数据校验的时候,延迟处理,默认为 500 毫秒 |
checkTrigger | string | 数据校验的触发类型,可选项: change 、blur 、null ,默认为:change |
onChange | function(values:Object, event:Object) | 数据改变后的回调函数 |
onError | function(errors:Object) | 校验出错的回调函数 |
onCheck | function(errors:Object) | 数据校验的回调函数 |
errors | object | 表单错误信息 |
<Field>
Props :
名称 | 类型 | 描述 |
---|---|---|
name | string | 表单元素名称 |
accepter | elementType | 受代理的组件 |
npm i rsuite-schema --save复制代码
在 rsuite-schema
主要有两个对象
SchemaModel
用于定义数据模型。Type
用于定义数据类型,包括:
这里的 Type 有点像 React 中 PropTypes 的定义。
一个示例:
const userModel = SchemaModel(
username: StringType().isRequired('用户名不能为空'),
email: StringType().isEmail('请输入正确的邮箱'),
age: NumberType('年龄应该是一个数字').range(18, 30, '年应该在 18 到 30 岁')
});复制代码
这里定义了一个 userModel
, 包含 username
、email
、age
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 文档
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
提供了不少 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;复制代码
若是一个组件不是原生表单控件,也不是 RSuite 库中提供的基础组件,要在 form-lib 中使用,应该怎么处理呢?
只须要在写组件的时候实现如下对应的 API:
接下来咱们使用 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地址: