如何实现Ant design表单组件封装?

框架

浏览数:189

2019-6-8

AD:资源代下载服务

目标:自己实现一个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

组件功能分析:

  • 1-每个input输入框被触发后开始做非空校验并提示错误

  • 2-表单提交时做表单项校验,全部校验成功则提示登录,否则提示校验失败

  • 3-表单项增加前置图标

组件封装思路:

  • 1-需要一个高阶函数hoc FormCreate,用来包装用户表单,增加数据管理能力;hoc需要扩展四个功能:getFieldDecorator, getFieldsError, getFieldError, isFieldTouched。获取字段包装器方法getFieldDecorator的返回值是个高阶函数,接收一个Input组件作为参数,返回一个新的组件。这就是让一个普通的表单项,变成了带有扩展功能的表单项(例如:增加该项的校验规则)
  • 2-FormItem组件,负责校验及错误信息的展示,需要保存两个属性,校验状态和错误信息,当前校验通过时错误信息为空
  • 3-Input组件,展示型组件,增加输入框前置icon
  • 4-导出FormCreate装饰后的MForm组件,MForm组件负责样式布局以及提交控制

组件封装步骤:

  • 1-完成一个基础的组件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组件,在表单项触发后做实时校验并提示错误信息

代码:MForm.js 

  • 以下每一步骤都可以独立运行

  • 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
  • step2 – 用高阶组件FormCreate对最后导出的MForm组件进行能力扩充;通过表单项组件FormItem展示校验错误信息
  •  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
  • step3 – 增加点击提交按钮时校验表单项的逻辑
  • 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开发组件的原则是:把逻辑控制往上层提,低层级的组件尽量做成傻瓜组件,不接触业务逻辑。

 

作者:dora_zc