如何实现Ant design表单组件封装?
目标:自己实现一个antd表单组件
先看下Ant Design官网上给出的表单组件用法:
1 import React, { Component } from 'react' 2 import { Form, Icon, Input, Button } from 'antd' 3 4 function hasErrors(fieldsError) { 5 return Object.keys(fieldsError).some(field => fieldsError[field]) 6 } 7 8 class HorizontalLoginForm extends React.Component { 9 componentDidMount() { 10 // To disabled submit button at the beginning. 11 this.props.form.validateFields() 12 } 13 14 handleSubmit = e => { 15 e.preventDefault() 16 this.props.form.validateFields((err, values) => { 17 if (!err) { 18 console.log('Received values of form: ', values) 19 } 20 }) 21 }; 22 23 render() { 24 const { 25 getFieldDecorator, 26 getFieldsError, 27 getFieldError, 28 isFieldTouched 29 } = this.props.form 30 31 // Only show error after a field is touched. 32 const userNameError = 33 isFieldTouched('userName') && getFieldError('userName') 34 const passwordError = 35 isFieldTouched('password') && getFieldError('password') 36 return ( 37 <Form layout='inline' onSubmit={this.handleSubmit}> 38 <Form.Item 39 validateStatus={userNameError ? 'error' : ''} 40 help={userNameError || ''} 41 > 42 {getFieldDecorator('userName', { 43 rules: [{ required: true, message: 'Please input your username!' }] 44 })( 45 <Input 46 prefix={<Icon type='user' style={{ color: 'rgba(0,0,0,.25)' }} />} 47 placeholder='Username' 48 /> 49 )} 50 </Form.Item> 51 <Form.Item 52 validateStatus={passwordError ? 'error' : ''} 53 help={passwordError || ''} 54 > 55 {getFieldDecorator('password', { 56 rules: [{ required: true, message: 'Please input your Password!' }] 57 })( 58 <Input 59 prefix={<Icon type='lock' style={{ color: 'rgba(0,0,0,.25)' }} />} 60 type='password' 61 placeholder='Password' 62 /> 63 )} 64 </Form.Item> 65 <Form.Item> 66 <Button 67 type='primary' 68 htmlType='submit' 69 disabled={hasErrors(getFieldsError())} 70 > 71 Log in 72 </Button> 73 </Form.Item> 74 </Form> 75 ) 76 } 77 } 78 79 const WrappedHorizontalLoginForm = Form.create({ name: 'horizontal_login' })( 80 HorizontalLoginForm 81 ) 82 83 export default WrappedHorizontalLoginForm
组件功能分析:
-
-
2-表单提交时做表单项校验,全部校验成功则提示登录,否则提示校验失败
-
3-表单项增加前置图标
组件封装思路:
- 1-需要一个高阶函数hoc FormCreate,用来包装用户表单,增加数据管理能力;hoc需要扩展四个功能:getFieldDecorator, getFieldsError, getFieldError, isFieldTouched。获取字段包装器方法getFieldDecorator的返回值是个高阶函数,接收一个Input组件作为参数,返回一个新的组件。这就是让一个普通的表单项,变成了带有扩展功能的表单项(例如:增加该项的校验规则)
- 2-FormItem组件,负责校验及错误信息的展示,需要保存两个属性,校验状态和错误信息,当前校验通过时错误信息为空
- 3-Input组件,展示型组件,增加输入框前置icon
- 4-导出FormCreate装饰后的MForm组件,MForm组件负责样式布局以及提交控制
组件封装步骤:
-
-
2-写一个高阶组件FormCreate对MForm进行扩充,使MForm组件拥有数据管理的能力。
-
保存字段选项设置 this.options = {}; 这里不需要保存为state,因为我们不希望字段选项变化而让组件重新渲染
-
保存各字段的值 this.state = {}
-
定义方法 getFieldDecorator()(),第一个参数传递配置项,第二个参数传入Input组件;第一个参数包括:当前校验项、校验规则 'username',{rules:[require:true,message:'请输入用户名']}
-
在FormCreate中,克隆一份Input组件,并且定义Input的onChange事件。首先这里需要把已经存在的jsx克隆一份,并修改它的属性,直接修改属性是不允许的;这里在更高级别定义onChange事件,控制元素的值,这样当组件发生变化时,就不用进行组件之间的来回通信。数据变化交给容器型组件去做,低层级的组件只负责展示即可。
-
-
3-增加提交校验功能
-
4-增加FormItem组件,在表单项触发后做实时校验并提示错误信息
-
以下每一步骤都可以独立运行
-
step1 - 搭建基础代码
-
1 import React, { Component } from 'react' 2 3 class MForm extends Component { 4 render() { 5 return ( 6 <div> 7 用户名:<input type='text' /> 8 密码:<input type='password' /> 9 <button>Log in</button> 10 </div> 11 ) 12 } 13 } 14 15 export default MForm
-
1 import React, { Component } from 'react' 2 3 // hoc: 包装用户表单,增加数据管理能力及校验功能 4 const FormCreate = Comp => { 5 return class extends Component { 6 constructor(props) { 7 super(props) 8 this.options = {} // 保存字段选项设置 9 this.state = {} // 保存各字段的值 10 } 11 12 // 处理表单项输入事件 13 handleChange = e => { 14 const { name, value } = e.target 15 this.setState( 16 { 17 [name]: value 18 }, 19 () => { 20 // TODO: 处理状态变化后的校验 21 // 由于setState是异步的,所以这里需要在回调函数中处理后续操作 22 // 保证状态已经完成改变 23 } 24 ) 25 }; 26 27 getFieldDecorator = (field, option) => InputComp => { 28 this.options[field] = option 29 return ( 30 <div> 31 {/* 把已经存在的jsx克隆一份,并修改它的属性,直接修改属性是不允许的。 32 这里在更高级别定义onChange事件,控制元素的值,这样当组件发生变化时, 33 就不用进行组件之间的来回通信 */} 34 {React.cloneElement(InputComp, { 35 name: field, // 控件name 36 value: this.state[field] || '', // 控件值 37 onChange: this.handleChange // change事件处理 38 })} 39 </div> 40 ) 41 }; 42 render() { 43 return ( 44 <Comp {...this.props} getFieldDecorator={this.getFieldDecorator} /> 45 ) 46 } 47 } 48 } 49 50 @FormCreate 51 class MForm extends Component { 52 render() { 53 const { getFieldDecorator } = this.props 54 55 return ( 56 <div> 57 用户名:{getFieldDecorator('username', { 58 rules: [{ required: true, message: '请填写用户名' }] 59 })(<input type='text' />)} 60 密码:{getFieldDecorator('password', { 61 rules: [{ required: true, message: '请填写密码' }] 62 })(<input type='password' />)} 63 <button>Log in</button> 64 </div> 65 ) 66 } 67 } 68 69 export default MForm
-
import React, { Component } from 'react' // hoc: 包装用户表单,增加数据管理能力及校验功能 const FormCreate = Comp => { return class extends Component { constructor(props) { super(props) this.options = {} // 保存字段选项设置 this.state = {} // 保存各字段的值 } // 处理表单项输入事件 handleChange = e => { const { name, value } = e.target this.setState( { [name]: value }, () => { // 处理状态变化后的校验 // 由于setState是异步的,所以这里需要在回调函数中处理后续操作 // 保证状态已经完成改变 this.validateField(name) } ) }; // 表单项校验,可以引用async-validator库来做校验,这里为了简便直接做非空校验 validateField = field => { // this.options数据格式如下 ↓↓↓ // { // "username": { // "rules": [{ // "required": true, // "message": "请填写用户名" // }] // }, // "password": { // "rules": [{ // "required": true, // "message": "请填写密码" // }] // } // } const { rules } = this.options[field] const ret = rules.some(rule => { if (rule.required) { if (!this.state[field]) { this.setState({ [field + 'Message']: rule.message }) // this.state数据格式如下 ↓↓↓ // {"username":"","usernameMessage":"","password":"","passwordMessage":""} return true // 校验失败,返回true } } }) if (!ret) { // 校验成功,将错误信息清空 this.setState({ [field + 'Message']: '' }) } return !ret }; // 校验所有字段 validate = cb => { const rets = Object.keys(this.options).map(field => this.validateField(field) ) // 如果校验结果数组中全部为true,则校验成功 const ret = rets.every(v => v === true) cb(ret) }; getFieldDecorator = (field, option) => InputComp => { this.options[field] = option return ( <div> {/* 把已经存在的jsx克隆一份,并修改它的属性,直接修改属性是不允许的。 这里在更高级别定义onChange事件,控制元素的值, 这样当组件发生变化时,就不用进行组件之间的来回通信 */} {React.cloneElement(InputComp, { name: field, // 控件name value: this.state[field] || '', // 控件值 onChange: this.handleChange // change事件处理 })} </div> ) }; render() { return ( <Comp {...this.props} getFieldDecorator={this.getFieldDecorator} validate={this.validate} /> ) } } } @FormCreate class MForm extends Component { onSubmit = () => { this.props.validate(isValid => { if (isValid) { alert('校验成功,可以登录了') console.log(this.props.value) } else { alert('校验失败') } }) }; render() { const { getFieldDecorator } = this.props return ( <div> 用户名:{getFieldDecorator('username', { rules: [{ required: true, message: '请填写用户名' }] })(<input type='text' />)} 密码:{getFieldDecorator('password', { rules: [{ required: true, message: '请填写密码' }] })(<input type='password' />)} <button onClick={this.onSubmit}>Log in</button> </div> ) } } export default MForm
- step4 - 增加表单输入时实时校验并提示错误逻辑,封装FormItem组件来展示错误信息,封装Input组件,增加前缀图标。至此,整个MForm组件就编写完成了!
-
1 import React, { Component } from 'react' 2 import { Icon } from 'antd' 3 4 // hoc: 包装用户表单,增加数据管理能力及校验功能 5 const FormCreate = Comp => { 6 return class extends Component { 7 constructor(props) { 8 super(props) 9 this.options = {} // 保存字段选项设置 10 this.state = {} // 保存各字段的值 11 } 12 13 // 处理表单项输入事件 14 handleChange = e => { 15 const { name, value } = e.target 16 this.setState( 17 { 18 [name]: value 19 }, 20 () => { 21 // 处理状态变化后的校验 22 // 由于setState是异步的,所以这里需要在回调函数中处理后续操作 23 // 保证状态已经完成改变 24 this.validateField(name) 25 } 26 ) 27 }; 28 29 // 表单项校验,可以引用async-validator库来做校验,这里为了简便直接做非空校验 30 validateField = field => { 31 // this.options ↓↓↓ 32 // { 33 // "username": { 34 // "rules": [{ 35 // "required": true, 36 // "message": "请填写用户名" 37 // }] 38 // }, 39 // "password": { 40 // "rules": [{ 41 // "required": true, 42 // "message": "请填写密码" 43 // }] 44 // } 45 // } 46 const { rules } = this.options[field] 47 const ret = rules.some(rule => { 48 if (rule.required) { 49 if (!this.state[field]) { 50 this.setState({ 51 [field + 'Message']: rule.message 52 }) 53 // this.state ↓↓↓ 54 // {"username":"","usernameMessage":"","password":"","passwordMessage":""} 55 return true // 校验失败,返回true 56 } 57 } 58 }) 59 if (!ret) { 60 // 校验成功,将错误信息清空 61 this.setState({ 62 [field + 'Message']: '' 63 }) 64 } 65 return !ret 66 }; 67 68 // 校验所有字段 69 validate = cb => { 70 const rets = Object.keys(this.options).map(field => 71 this.validateField(field) 72 ) 73 // 如果校验结果数组中全部为true,则校验成功 74 const ret = rets.every(v => v === true) 75 cb(ret) 76 }; 77 78 getFieldDecorator = (field, option) => InputComp => { 79 this.options[field] = option 80 return ( 81 <div> 82 {/* 把已经存在的jsx克隆一份,并修改它的属性,直接修改属性是不允许的。 83 这里在更高级别定义onChange事件,控制元素的值, 84 这样当组件发生变化时,就不用进行组件之间的来回通信 */} 85 {React.cloneElement(InputComp, { 86 name: field, // 控件name 87 value: this.state[field] || '', // 控件值 88 onChange: this.handleChange, // change事件处理 89 onFocus: this.handleFocus 90 })} 91 </div> 92 ) 93 }; 94 95 // 控件获取焦点事件 96 handleFocus = e => { 97 const field = e.target.name 98 this.setState({ 99 [field + 'Focus']: true 100 }) 101 } 102 103 // 判断控件是否被点击过 104 isFieldTouched = field => !!this.state[field + 'Focus'] 105 106 // 获取控件错误提示信息 107 getFieldError = field => this.state[field + 'Message'] 108 109 render() { 110 return ( 111 <Comp 112 {...this.props} 113 getFieldDecorator={this.getFieldDecorator} 114 validate={this.validate} 115 isFieldTouched = {this.isFieldTouched} 116 getFieldError = {this.getFieldError} 117 /> 118 ) 119 } 120 } 121 } 122 123 class FormItem extends Component { 124 render() { 125 return ( 126 <div className='formItem'> 127 { this.props.children } 128 { this.props.validateStatus === 'error' && ( 129 <p style={ { color: 'red' } }>{ this.props.help}</p> 130 )} 131 </div> 132 ) 133 } 134 } 135 136 class Input extends Component { 137 render() { 138 return ( 139 <div> 140 {/* 前缀图标 */} 141 {this.props.prefix} 142 <input {...this.props} /> 143 </div> 144 ) 145 } 146 } 147 148 @FormCreate 149 class MForm extends Component { 150 onSubmit = () => { 151 this.props.validate(isValid => { 152 if (isValid) { 153 alert('校验成功,可以登录了') 154 console.log(this.props.value) 155 } else { 156 alert('校验失败') 157 } 158 }) 159 }; 160 render() { 161 const { getFieldDecorator, isFieldTouched, getFieldError } = this.props 162 const usernameError = isFieldTouched('username') && getFieldError('username') 163 const passwordError = isFieldTouched('password') && getFieldError('password') 164 165 return ( 166 <div> 167 <FormItem 168 validateStatus={ usernameError ? 'error' : '' } 169 help={usernameError || ''} 170 > 171 用户名:{getFieldDecorator('username', { 172 rules: [{ required: true, message: '请填写用户名' }] 173 })(<Input type='text' prefix={<Icon type='user' />} />)} 174 </FormItem> 175 <FormItem 176 validateStatus={ passwordError ? 'error' : '' } 177 help={passwordError || ''} 178 > 179 密码:{getFieldDecorator('password', { 180 rules: [{ required: true, message: '请填写密码' }] 181 })(<Input type='password' prefix={<Icon type='lock' />} />)} 182 </FormItem> 183 <button onClick={this.onSubmit}>Log in</button> 184 </div> 185 ) 186 } 187 } 188 189 export default MForm
- index.js
-
import React from 'react' import ReactDOM from 'react-dom' import MForm from './components/MForm' ReactDOM.render(<MForm />, document.querySelector('#root'))
最终效果:
总结:
- react的组件是自上而下的扩展,将扩展的能力由上往下传递下去,Input组件在合适的时间就可以调用传递下来的值。
- react开发组件的原则是:把逻辑控制往上层提,低层级的组件尽量做成傻瓜组件,不接触业务逻辑。