-
本篇文章通过对
Ant Design Form
组件的源码分析,实现一个简易版的表单组件,该组件可以实时进行数据校验和展示校验错误信息,如下图:
引入
AntDesign
创建一个基础表单,没有任何数据收集、数据校验相关逻辑
// src/components/CopyAntdForm.js
import React from 'react';
import {Input,Button} from 'antd';
import 'antd/dist/antd.css';
export default class CopyAntdForm extends React.Component {
render () {
return (
<div>
<Input></Input>
<Button>login</Button>
</div>
)
}
}
- 然后创建一个高阶组件扩展现有的基础表单,让表单具有事件处理、数据收集、数据校验的能力
// src/components/CopyAntdForm.js
import React from 'react';
import {Input,Button} from 'antd';
import 'antd/dist/antd.css';
// 高阶组件的基础结构,返回一个组件
function CopyFormCreate (Comp) {
return class extends React.Component {
render () {
return <Comp></Comp>
}
}
}
@CopyFormCreate
class CopyAntdForm extends React.Component {
render () {
return (
<div>
<Input></Input>
<Button>login</Button>
</div>
)
}
}
export default CopyAntdForm;
- 下一步开始设计这个高阶组件,我们对组件的扩展主要是对
Input
和整体表单的扩展,首先Input
要有onChange
的能力、有显示验证错误信息的能力等,所以先创建一个Input
包装器函数
// src/components/CopyAntdForm.js
function CopyFormCreate (Comp) {
return class extends React.Component {
// 创建Input包装器,将Input事件相关的处理委托给更高层级
// 接收2个参数,一个是Input类型,一个是校验规则
// 返回一个函数,这个函数又是一个高阶组件,接收传进来的Input组件
getFieldDec = (field, option) => {
return InputComp => (
// InputComp是虚拟DOM,扩展的时候不能直接修改原组件,要clone出新组件扩展
<div>
{React.cloneElement(InputComp, {
// 组件type, 区分用户名、密码、按钮等
name: field,
// Input绑定的值
value: this.state[field] || '',
// Input事件
onChange: this.handleChange
})}
</div>
)
}
render () {
return <Comp></Comp>
}
}
}
- 然后补充一下扩展后的
Input
组件需要的数据,完善整个包装器逻辑,在使用包装器函数初始化Input
组件的时候先保存下当前Input
的验证规则,从代码中可以看出我们保存Input
组件数据的形式都是以组件name
属性做区分的,分别使用this.options[field]
、this.state[field]
保存验证规则和绑定值
// src/components/CopyAntdForm.js
function CopyFormCreate (Comp) {
return class extends React.Component {
constructor(props) {
super(props);
// 验证规则、校验
this.options = {};
this.state = {};
}
// 创建Input包装器,将Input事件相关的处理委托给更高层级
// 接收2个参数,一个是Input类型,一个是校验规则
// 返回一个函数,这个函数又是一个高阶组件,接收传进来的Input组件
getFieldDec = (field, option) => {
// 保存当前输入项配置,验证规则
this.options[field] = option
return InputComp => (
// InputComp是虚拟DOM,扩展的时候不能直接修改原组件,要clone出新组件扩展
<div>
{React.cloneElement(InputComp, {
// 组件type, 区分用户名、密码、按钮等
name: field,
// Input绑定的值
value: this.state[field] || '',
// Input事件
onChange: this.handleChange
})}
</div>
)
}
render () {
return <Comp></Comp>
}
}
}
- 接下来继续补充
Input
组件需要的onChange
事件,也就是this.handleChange
,当Input
输入值更新时同时更新state
中的绑定值,这个高阶组件中的方法我们可以通过属性 的方式传递给最终返回的那个组件,也就是Comp
// src/components/CopyAntdForm.js
function CopyFormCreate (Comp) {
return class extends React.Component {
constructor(props) {
super(props);
// 验证规则、校验
this.options = {};
this.state = {};
}
handleChange = (e) => {
const {name, value} = e.target
// name属性是动态的
this.setState({[name]: value})
}
// 创建Input包装器,将Input事件相关的处理委托给更高层级
// 接收2个参数,一个是Input类型,一个是校验规则
// 返回一个函数,这个函数又是一个高阶组件,接收传进来的Input组件
getFieldDec = (field, option) => {
// 保存当前输入项配置,验证规则
this.options[field] = option
return InputComp => (
// InputComp是虚拟DOM,扩展的时候不能直接修改原组件,要clone出新组件扩展
<div>
{React.cloneElement(InputComp, {
// 组件type, 区分用户名、密码、按钮等
name: field,
// Input绑定的值
value: this.state[field] || '',
// Input事件
onChange: this.handleChange
})}
</div>
)
}
render () {
return <Comp getFieldDec={this.getFieldDec}></Comp>
}
}
}
- 完成这个包装器之后我们就可以使用了,从前面的代码可以看出我们用高阶组件强化了
CopyAntdForm
这个组件,所以在CopyAntdForm
中使用包装器的方法就是从props
中引入包装器并调用
// src/components/CopyAntdForm.js
@CopyFormCreate
class CopyAntdForm extends React.Component {
render () {
// 引入包装器
const {getFieldDec} = this.props
return (
<div>
{/* 调用包装器函数,把input类型、验证规则和Input组件传入 */}
{getFieldDec('uname', {
rules: [{required: true, msg: '用户名必填'}]
})(<Input></Input>)}
<Button>login</Button>
</div>
)
}
}
- 以上我们完成了数据收集的功能,下面开始数据校验相关逻辑,数据校验的逻辑主要分2部分:点击提交按钮的最终数据校验、
Input
组件数据改变时的单个实时校验,先完善一下输入框,新增一个密码框,然后补充点击提交按钮的逻辑结构
// src/components/CopyAntdForm.js
@CopyFormCreate
class CopyAntdForm extends React.Component {
// 登录按钮提交
onSubmit = () => {
console.log('submit')
}
render () {
const {getFieldDec} = this.props
return (
<div>
{getFieldDec("uname", {
rules: [{required: true, msg: '用户名必填'}]
}
)(<Input></Input>)}
{getFieldDec("pwd", {
rules: [{required: true, msg: '密码必填'}]
}
)(<Input type="password"></Input>)}
<Button onClick={this.onSubmit}>login</Button>
</div>
)
}
}
- 接着添加数据校验逻辑结构,当
Input
数据改变时需要做实时校验,这里有一个点要注意,setState
是异步调用的,分别展示下错误写法和正确写法
// src/components/CopyAntdForm.js
// 错误写法!!!!
handleChange = (e) => {
const {name, value} = e.target
console.log(name, value)
// name属性是动态的
this.setState({[name]: value})
// 错误写法!!!!
// setState是异步调用,紧跟其后执行验证方法有可能验证的数据还是没有改变的数据
this.validateField(name)
}
// 校验单个项
validateField = field => {
}
// src/components/CopyAntdForm.js
// 正确写法!!!!
handleChange = (e) => {
const {name, value} = e.target
console.log(name, value)
// name属性是动态的
this.setState({[name]: value}, () => {
// 确保值发生变化再校验
// 将调用验证方法写在setState的回调函数中
this.validateField(name)
})
}
// 校验单个项
validateField = field => {
}
- 补充校验单个项函数
// src/components/CopyAntdForm.js
// 校验单个项
validateField = field => {
// 1.获取校验规则
const rules = this.options[field].rules;
// 2.校验规则中任意一项失败,则返回false
const ret = !rules.some(rule => {
if(rule.required) {
if(!this.state[field]) {
// 校验失败
this.setState({
[field + 'Message']: rule.msg
})
return true;
}
}
})
if(ret) {
// 校验成功 清空错误信息
this.setState({
[field + 'Message']: ''
})
}
return ret;
}
- 补充完整整个校验逻辑,点击
login
按钮需要校验所有项,其实就是遍历所有项目分别进行单个校验,最终返回遍历校验的结果和所有表单数据,如果校验通过就可以直接提交了
// src/components/CopyAntdForm.js
// 校验所有项
validate = cb => {
// 遍历存储输入项配置及验证规则的数组,获取每一个Input组件的配置
const rets = Object.keys(this.options).map(field => {
this.validateField(field)
})
// 每一项都校验成功才能通过
const ret = rets.every(item => item === true)
cb(ret, this.state)
}
// src/components/CopyAntdForm.js
// 登录按钮提交
onSubmit = () => {
// 校验所有项
this.props.validate((isValid, data) => {
if(isValid) {
// 提交
console.log('登录', data)
}else {
alert('校验失败')
}
})
}
- 以上就完成所有校验相关逻辑了,最后补充下
Input
包装器,添加展示错误信息的部分
// src/components/CopyAntdForm.js
getFieldDec = (field, option) => {
this.options[field] = option
return InputComp => (
<div>
{React.cloneElement(InputComp, {
name: field,
value: this.state[field] || '',
onChange: this.handleChange
})}
{/* 校验的错误信息 */}
{this.state[field + 'Message'] && (
<p style={{color: 'red'}}>{this.state[field + 'Message']}</p>
)}
</div>
)
}
- 最后,贴一下 “实现Ant Design Form组件(简易版)” 的完整代码
import React from 'react';
import {Input,Button} from 'antd';
import 'antd/dist/antd.css';
function CopyFormCreate (Comp) {
return class extends React.Component {
constructor(props) {
super(props);
// 验证规则、校验
this.options = {};
this.state = {};
}
handleChange = (e) => {
const {name, value} = e.target
// console.log(name, value)
// name属性是动态的
this.setState({[name]: value}, () => {
// 确保值发生变化再校验
this.validateField(name)
})
}
// 校验单个项
validateField = field => {
// 1.获取校验规则
const rules = this.options[field].rules;
// 2.校验规则中任意一项失败,则返回false
const ret = !rules.some(rule => {
if(rule.required) {
if(!this.state[field]) {
// 校验失败
this.setState({
[field + 'Message']: rule.msg
})
return true;
}
}
})
if(ret) {
// 校验成功 清空错误信息
this.setState({
[field + 'Message']: ''
})
}
return ret;
}
// 校验所有项
validate = cb => {
// 遍历存储输入项配置及验证规则的数组,获取每一个Input组件的配置
const rets = Object.keys(this.options).map(field => {
this.validateField(field)
})
// 每一项都校验成功才能通过
const ret = rets.every(item => item === true)
cb(ret, this.state)
}
// 创建Input包装器,将Input事件相关的处理委托给更高层级
// 接收2个参数,一个是Input类型,一个是校验规则
// 返回一个函数,这个函数又是一个高阶组件,接收传进来的Input组件
getFieldDec = (field, option) => {
// 保存当前输入项配置,验证规则
this.options[field] = option
return InputComp => (
// InputComp是虚拟DOM,扩展的时候不能直接修改原组件,要clone出新组件扩展
<div>
{React.cloneElement(InputComp, {
// 组件type, 区分用户名、密码、按钮等
name: field,
// Input绑定的值
value: this.state[field] || '',
// Input事件
onChange: this.handleChange
})}
{/* 校验的错误信息 */}
{this.state[field + 'Message'] && (
<p style={{color: 'red'}}>{this.state[field + 'Message']}</p>
)}
</div>
)
}
render () {
return <Comp getFieldDec={this.getFieldDec} validate={this.validate}></Comp>
}
}
}
@CopyFormCreate
class CopyAntdForm extends React.Component {
// 登录按钮提交
onSubmit = () => {
// 校验所有项
this.props.validate((isValid, data) => {
if(isValid) {
// 提交
console.log('登录', data)
}else {
alert('校验失败')
}
})
}
render () {
// 引入包装器
const {getFieldDec} = this.props
return (
<div>
{/* 调用包装器函数,把input类型、验证规则和Input组件传入 */}
{getFieldDec('uname', {
rules: [{required: true, msg: '用户名必填'}]
})(<Input></Input>)}
{getFieldDec("pwd", {
rules: [{required: true, msg: '密码必填'}]
}
)(<Input type="password"></Input>)}
<Button onClick={this.onSubmit}>login</Button>
</div>
)
}
}
export default CopyAntdForm;