经常开发中后台项目的同学肯定都经历过大型表单的折磨,几十个甚至上百个表单元素足以让我们欲仙欲死,这可真是个体力活。特别当你选择 React 作为技术框架的情况下,这满屏幕的 onChange
简直是一个噩梦。
当然我们还是有追求的,肯定不会屈服于此。社区内有很多的解决方案,比如双向绑定就是一个接受度很高的策略。这种方式也是我两年前惯用的手法,来看一段代码:
<Input
value={this.state.title}
maxLength={25}
onChange={Bind.valueChange.bind(this, 'title')}
placeholder='请输入标题' />
通过这种方式我们确实减少了大量的手写回调函数来绑定数据源,但是 onChang
这东西就像狗皮膏药一样依附在那里。
那有没有一种方式,能够完全解放这种重复代码,我们只需要通过配置数据源就可以达到数据收集的目的呢?
接下来我们开始来讨论一下极致的 React 表单解决方案:@monajs/react-form
import React from 'react'
import { Button, Input, Select } from 'antd'
import Form from '@monajs/react-form'
import FormItem from '@/component/form-item'
const { TextArea } = Input
const { Option } = Select
const Home = () => {
const formRef = React.createRef()
const getForm = () => {
const formData = formRef.current.getFormData()
const verifyInfo = formRef.current.getVerifyInfo()
console.log(formData)
console.log(verifyInfo)
}
return (
<Form ref={formRef}>
<FormItem bn='name' label='输入框' required>
<Form.Proxy
to={TextArea}
bn='name'
getValue={(val) => val.target.value} defaultValue='ss' />
</FormItem>
<FormItem bn='other' label='下拉框' desc='请选择' required>
<Form.Proxy
to={Select}
bn='other'
style={{ width: 300 }}
placeholder='请输入other'>
<Option key={'3'} value='3'>3</Option>
<Option key={'4'} value='4'>4</Option>
</Form.Proxy>
</FormItem>
<Button onClick={getForm}>提交</Button>
</Form>
)
}
export default Home
// 打印结果
{
name: 'fangke',
other: '3'
}
我们来分析一下上面的代码。
- 首先我插入了一个容器节点
Form
。 - 然后我们把 antd 的组件通过
Form.Proxy
架了一层代理,通过to
属性来声明代理路径,通过bn
属性来声明要绑定的数据源。 - 最后通过
Form
实例上的getFormData
来获取最终的表单数据对象。
这里我先阐述主链路,像Form
、Form.Proxy
、FormItem
、getValue
等到底是干什么的会在后面详细介绍。
顾名思义它是个容器,我们所有的表单元素都必须是它的子节点,然后我们可以通过节点实例来全局性的做一些操作,比如数据收集、错误收集和重置表单。
实际上我们在第2节中已经了解了如何来获取表单的所有绑定数据,使用姿势比较简单,这里就不再重复阐述。
const formData = formRef.current.getFormData()
console.log(formData)
//打印结果
{name: "sss", id: "11", scholl: "2", other: "4"}
一般返回的结果就是我们最终想要的数据结构,但是我们日常的需求中也难免会碰到很多层级很深的数据格式,这块我们会在 3.2.1 章节进行介绍。
只有当我们的表单元素中绑定了 verify
属性,我们才会对其进行数据校验,并进行最终校验未通过信息收集。具体 verify
是如何执行的,我们将在 3.2.2 章节进行介绍。
const verifyInfo = formRef.current.getVerifyInfo()
console.log(verifyInfo)
//打印结果
[
{id: 1, val: "1", vm: FormItemComponent, isEmptyVerify: true, verifyMsg: ƒ},
{id: 2, val: "s", vm: FormItemComponent, isRegVerify: true, verifyMsg: ƒ},
{id: 3, val: "4", vm: FormItemComponent, isFunctionVerify: true, verifyMsg: ƒ}
]
返回结果中包含了以下信息:
字段 | 说明 |
---|---|
id | 表单元素的唯一id |
val | 表单元素的返回值 |
vm | 表单元素的实例对象 |
isEmptyVerify | 校验类型是否为空校验 |
isRegVerify | 校验类型是否为正则校验 |
isFunctionVerify | 校验类型是否为函数校验 |
verifyMsg | 当校验未通过时,会通过该方法返回校验报错信息 |
- 注:通过校验返回的错误信息,我们可以进行一些自定义操作,比如通过表单实例(
vm
)返回到指定位置。
重置是表单操作中比较常见的功能,我们的组件设计当然也考虑到了这个场景。
formRef.current.reset()
通过上面的使用介绍,我们应该大致知道了我们是通过 bn
属性来进行数据绑定的,表单元素组件最终的返回值会被绑定到 bn
声明的字段上。
在多数情况下,我们的表单是一级结构,是扁平的,我们只需要给 bn
属性传递一个 key
值就可以实现,例如:
<Form.Proxy bn='name' to={Input} getValue={(val) => val.target.value} />
// 返回结果
{
name: "fangke"
}
针对一些层级比较深的 json 数据结构,我们支持 .
点运算符,我们来看一个例子:
<Form.Proxy bn='people.name' to={Input} getValue={(val) => val.target.value} />
<Form.Proxy bn='people.age' to={Input} getValue={(val) => val.target.value} />
<Form.Proxy bn='type' to={Input} getValue={(val) => val.target.value} />
// 返回
{
people: {
name: 'fangke',
age: 18
},
type: '贫民'
}
针对数组类型的数据结构,我们支持 []
运算符,我们来看一个例子:
<Form.Proxy bn='people[0]' to={Input} getValue={(val) => val.target.value} />
// 返回
{
people: ['fangke']
}
接下来我们看一下混合模式下的应用。
<Form.Proxy bn='CH.people[0].name' to={Input} getValue={(val) => val.target.value} />
<Form.Proxy bn='CH.type[0]' to={Input} getValue={(val) => val.target.value} />
<Form.Proxy bn='CH.father.name' to={Input} getValue={(val) => val.target.value} />
<Form.Proxy bn='CH.father.age' to={Input} getValue={(val) => val.target.value} />
// 返回
{
CH: {
people: [{
name: 'fangke'
}]
type: ['贫民'],
father: {
name: 'fangke',
age: 18
}
}
}
在 3.1.2 章节中我们提到过当表单元素组件传递了 verify
属性,我们就会对其开启校验,接下来我们来详细介绍一下。
我们支持三种形式的形式:
- 非空校验
<Form.Proxy bn='name' to={Input} verify verifyMsg='name不允许为空' getValue={(val) => val.target.value} />
当输入值为空时,则校验不通过,并且提示信息为 verifyMsg
属性绑定的"name不允许为空"。
- 正则校验
<Form.Proxy bn='mobile' to={Input} verify={/^1[3456789]\d{9}$/} verifyMsg='手机号格式不符合要求' getValue={(val) => val.target.value} />
当输入值不匹配正则表达式时,则校验不通过,并且提示信息为 verifyMsg
属性绑定的"手机号格式不符合要求"。
- 函数校验
<Form.Proxy bn='name' to={Input} verify={(val) => val === 'fangke'} verifyMsg='请输入fangke' getValue={(val) => val.target.value} />
当输入值通过 verify
方法返回 false
时,则校验不通过,并且提示信息为 verifyMsg
属性绑定的"请输入fangke"。
介绍完 3.2.1 大家肯定会有一个疑问,如果 verifyMsg
只支持传递字符串那我们如何进行个性化提示。
实际上我们的 verifyMsg
是支持函数形式的,我们可以根据输入值进行多形式提示。
<Form.Proxy to={Input} bn='name' getValue={(val) => val.target.value} verify={(val) => val === 'fangke'} verifyMsg={(verify) => verify.val} />
这个 demo 只有当你输入 “fangke” 时才不会提示,否则你输入什么就提示什么。
讲到这里,我们应该会有以下几个疑问:
问题一:Form.Proxy
到底是干什么的
我们先来设想一下,如果我们不用 Form.Proxy
来架设代理层,那么我们怎么让 Form
表单容器和表单元素组件建立联系,那么我们是不是就无法通过 Form
实例的 getFormData
方法来全局收集到所有的表单元素的输入值。
那我们就可以这么理解,通过 Form.Proxy
代理过后的组件就跟 Form
建立了通信,从而实现数据双向输送。
传递到 Form.Proxy
中的所有属性,都会透传到目标组件中(即 to
属性传递的组件),除了to
、verify
和verifyMsg
这些私有属性。
问题二:是不是所有的组件都可以用在这种模式下成为表单元素
只要组件支持 onChange
属性回调返回,那就可以通过 Form.Proxy
成为 Form
的表单元素。
问题三:为什么需要添加 getValue
属性
getValue
实际上是一种钩子形态,它让接入的组件可以更加灵活。
举个例子:
onChange = (e) => {
console.log('val:' + e.target.value)
}
...
<Input onChange={this.onChange}>
Input
组件的形参实际上是一个合成事件对象,并不是我们最终想要的数据结果,getValue
就提供了这么一种能力来帮我们返回最终想要的数据。
如果 onChange
的形参已经是我们最终想要的数据结果,那么 getValue
就可以省略,因为我们会默认处理。
通过 Form.Proxy
我们确实达到了目的,代码中再也不需要写一大堆的 onChange
来绑定数据,我们只需要简单的一个 bn
进行绑定就可以实现数据全量收集。
但是 Input
和 TextArea
上一大堆的 getValue
钩子,看着还是很难受,都是些重复代码。实际上, Form.Proxy
是针对一些自定义的组件而设计的,它适合于使用频率不高的组件。
像 Input
、TextArea
、Select
这些高频组件,我们推荐使用 withFormContext
进行一次封装,然后统一使用封装后的组件,看下面例子:
// input.jsx
import Form from '@monajs/react-form'
import { Input } from 'antd'
const { withFormContext } = Form
const TextArea = Input.TextArea
const I = withFormContext(Input, (val) => val.target.value)
I.TextArea = withFormContext(TextArea, (val) => val.target.value)
export default I
投入使用:
import React from 'react'
import { Button } from 'antd'
import Form from '@monajs/react-form'
import Input from './input.jsx'
const { TextArea } = Input
const Test = () => {
const formRef = React.createRef()
const getForm = () => {
const formData = formRef.current.getFormData()
console.log(formData)
}
return (
<Form ref={formRef}>
<TextArea bn='name' />
<Input bn='age' />
<Button onClick={getForm} >提交</Button>
</Form>
)
}
export default Test
// 打印结果
{
name: 'fangke',
age: 18
}
在 3.1.2 章节中我们介绍,通过 getVerifyInfo
方法我们可以获取到全量的校验未通过信息。那么我们能否实现一个实时报错的功能呢?
当然是可以,我们先来看一个封装好的实例,也就是我们 2 章节中使用的 FormItem
组件。
import React from 'react'
import PropTypes from 'prop-types'
import Form from '@monajs/react-form'
import { Row, Col } from 'antd'
import './index.less'
const DefaultFormWrap = (props) => {
const {
children = null,
verifyMsg = '',
required = false,
label = '',
desc = '',
className = '',
span = 6
} = props
return (
<Row className={['page-form-item', className]}>
<Col className={['label', { 'required': required }]} span={span}>{label}</Col>
<Col className='content' span={24 - span}>
{children}
<If condition={verifyMsg}>
<div className='error'>{verifyMsg}</div>
</If>
<If condition={!error && !verifyMsg && desc}>
<div className='desc' dangerouslySetInnerHTML={{ __html: desc }} />
</If>
</Col>
</Row>
)
}
DefaultFormWrap.propTypes = {
required: PropTypes.bool,
span: PropTypes.number,
label: PropTypes.string,
desc: PropTypes.string,
verifyMsg: PropTypes.string, // 附加属性
className: PropTypes.string,
children: PropTypes.node
}
export default Form.withVerifyContext(DefaultFormWrap)
实际上 FormItem
就是一个纯UI展示组件,通过 Form.withVerifyContext
高阶组件返回的组件会附加一个 verifyMsg
属性。如果校验未通过(实时进行:每次的 onChange
触发都会进行校验),就会收到校验未通过的提示信息,并做UI展示。
问题:我们如何让 FormItem
知道要提示哪一个表单元素的校验未通过信息
<FormItem bn='name' label='姓名' desc='请填写' required>
<Input bn='name' />
</FormItem>
我们通过 bn
属性来跟表单元素进行绑定。FormItem
会提示跟自身 bn
绑定值一致的表单元素的校验信息。
除了通过 Form.withVerifyContext
高阶组件来获取单个校验信息,我们还可以通过上下文实时获取批量校验未通过信息。
import Form from '@monajs/react-form'
const { FormVerifyContext } = Form
...
<FormVerifyContext.Consumer>
{(verifyInfo = {}) => (
...
)}
</FormVerifyContext.Consumer>
- 各种表单,特别是大型表单,能大幅减少重复代码量,并且能够快速搞定。
- 自定义表单系统,我们可以在这个组件的基础上,通过一份配置动态搭建出一个表单页面。
后续会推出 antd 的一套配套组件,因为是透传,所以跟 antd 的使用无异。