1. 程式人生 > >如何實現Ant design表單組件封裝?

如何實現Ant design表單組件封裝?

() type 變化 參數傳遞 document true 業務 提交 data-

目標:自己實現一個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開發組件的原則是:把邏輯控制往上層提,低層級的組件盡量做成傻瓜組件,不接觸業務邏輯。

如何實現Ant design表單組件封裝?