React Form組件的實(shí)現(xiàn)封裝雜談
前言
對(duì)于網(wǎng)頁(yè)系統(tǒng)來(lái)說(shuō),表單提交是一種很常見的與用戶交互的方式,比如提交訂單的時(shí)候,需要輸入收件人、手機(jī)號(hào)、地址等信息,又或者對(duì)系統(tǒng)進(jìn)行設(shè)置的時(shí)候,需要填寫一些個(gè)人偏好的信息。 表單提交是一種結(jié)構(gòu)化的操作,可以通過(guò)封裝一些通用的功能達(dá)到簡(jiǎn)化開發(fā)的目的。本文將討論Form表單組件設(shè)計(jì)的思路,并結(jié)合有贊的ZentForm組件介紹具體的實(shí)現(xiàn)方式。本文所涉及的代碼都是基于React v15的版本。
Form組件功能
一般來(lái)說(shuō),F(xiàn)orm組件的功能包括以下幾點(diǎn):
- 表單布局
- 表單字段
- 封裝表單驗(yàn)證&錯(cuò)誤提示
- 表單提交
下面將對(duì)每個(gè)部分的實(shí)現(xiàn)方式做詳細(xì)介紹。
表單布局
常用的表單布局一般有3種方式:
行內(nèi)布局
水平布局
垂直布局
實(shí)現(xiàn)方式比較簡(jiǎn)單,嵌套css就行。比如form的結(jié)構(gòu)是這樣:
<form class="form"> <label class="label"/> <field class="field"/> </form>
對(duì)應(yīng)3種布局,只需要在form標(biāo)簽增加對(duì)應(yīng)的class:
<!--行內(nèi)布局--> <form class="form inline"> <label class="label"/> <field class="field"/> </form> <!--水平布局--> <form class="form horizontal"> <label class="label"/> <field class="field"/> </form> <!--垂直布局--> <form class="form vertical"> <label class="label"/> <field class="field"/> </form>
相應(yīng)的,要定義3種布局的css:
.inline .label { display: inline-block; ... } .inline .field { display: inline-block; ... } .horizontal .label { display: inline-block; ... } .horizontal .field { display: inline-block; ... } .vertical .label { display: block; ... } .vertical .field { display: block; ... }
表單字段封裝
字段封裝部分一般是對(duì)組件庫(kù)的組件針對(duì)Form再做一層封裝,如Input組件、Select組件、Checkbox組件等。當(dāng)現(xiàn)有的字段不能滿足需求時(shí),可以自定義字段。
表單的字段一般包括兩部分,一部分是標(biāo)題,另一部分是內(nèi)容。ZentForm通過(guò)getControlGroup這一高階函數(shù)對(duì)結(jié)構(gòu)和樣式做了一些封裝,它的入?yún)⑹且@示的組件:
export default Control => { render() { return ( <div className={groupClassName}> <label className="zent-form__control-label"> {required ? <em className="zent-form__required">*</em> : null} {label} </label> <div className="zent-form__controls"> <Control {...props} {...controlRef} /> {showError && ( <p className="zent-form__error-desc">{props.error}</p> )} {notice && <p className="zent-form__notice-desc">{notice}</p>} {helpDesc && <p className="zent-form__help-desc">{helpDesc}</p>} </div> </div> ); } }
這里用到的label和error等信息,是通過(guò)Field組件傳入的:
<Field label="預(yù)約門店:" name="dept" component={CustomizedComp} validations={{ required: true, }} validationErrors={{ required: '預(yù)約門店不能為空', }} required />
這里的CustomizedComp是通過(guò)getControlGroup封裝后返回的組件。
字段與表單之間的交互是一個(gè)需要考慮的問(wèn)題,表單需要知道它包含的字段值,需要在適當(dāng)?shù)臅r(shí)機(jī)對(duì)字段進(jìn)行校驗(yàn)。ZentForm的實(shí)現(xiàn)方式是在Form的高階組件內(nèi)維護(hù)一個(gè)字段數(shù)組,數(shù)組內(nèi)容是Field的實(shí)例。后續(xù)通過(guò)操作這些實(shí)例的方法來(lái)達(dá)到取值和校驗(yàn)的目的。
ZentForm的使用方式如下:
class FieldForm extends React.Component { render() { return ( <Form> <Field name="name" component={CustomizedComp} </Form> ) } } export default createForm()(FieldForm);
其中Form和Field是組件庫(kù)提供的組件,CustomizedComp是自定義的組件,createForm是組件庫(kù)提供的高階函數(shù)。在createForm返回的組件中,維護(hù)了一個(gè)fields的數(shù)組,同時(shí)提供了attachToForm和detachFromForm兩個(gè)方法,來(lái)操作這個(gè)數(shù)組。這兩個(gè)方法保存在context對(duì)象當(dāng)中,F(xiàn)ield就能在加載和卸載的時(shí)候調(diào)用了。簡(jiǎn)化后的代碼如下:
/** * createForm高階函數(shù) */ const createForm = (config = {}) => { ... return WrappedForm => { return class Form extends Component { constructor(props) { super(props); this.fields = []; } getChildContext() { return { zentForm: { attachToForm: this.attachToForm, detachFromForm: this.detachFromForm, } } } attachToForm = field => { if (this.fields.indexOf(field) < 0) { this.fields.push(field); } }; detachFromForm = field => { const fieldPos = this.fields.indexOf(field); if (fieldPos >= 0) { this.fields.splice(fieldPos, 1); } }; render() { return createElement(WrappedForm, {...}); } } } } /** * Field組件 */ class Field extends Component { componentWillMount() { this.context.zentForm.attachToForm(this); } componentWillUnmount() { this.context.zentForm.detachFromForm(this); } render() { const { component } = this.props; return createElement(component, {...}); } }
當(dāng)需要獲取表單字段值的時(shí)候,只需要遍歷fields數(shù)組,再調(diào)用Field實(shí)例的相應(yīng)方法就可以:
/** * createForm高階函數(shù) */ const createForm = (config = {}) => { ... return WrappedForm => { return class Form extends Component { getFormValues = () => { return this.fields.reduce((values, field) => { const name = field.getName(); const fieldValue = field.getValue(); values[name] = fieldValue; return values; }, {}); }; } } } /** * Field組件 */ class Field extends Component { getValue = () => { return this.state._value; }; }
表單驗(yàn)證&錯(cuò)誤提示
表單驗(yàn)證是一個(gè)重頭戲,只有驗(yàn)證通過(guò)了才能提交表單。驗(yàn)證的時(shí)機(jī)也有多種,如字段變更時(shí)、鼠標(biāo)移出時(shí)和表單提交時(shí)。ZentForm提供了一些常用的驗(yàn)證規(guī)則,如非空驗(yàn)證,長(zhǎng)度驗(yàn)證,郵箱地址驗(yàn)證等。當(dāng)然還能自定義一些更復(fù)雜的驗(yàn)證方式。自定義驗(yàn)證方法可以通過(guò)兩種方式傳入ZentForm,一種是通過(guò)給createForm傳參:
createForm({ formValidations: { rule1(values, value){ }, rule2(values, value){ }, } })(FormComp);
另一種方式是給Field組件傳屬性:
<Field validations={{ rule1(values, value){ }, rule2(values, value){ }, }} validationErrors={{ rule1: 'error1', rule2: 'error2' }} />
使用createForm傳參的方式,驗(yàn)證規(guī)則是共享的,而Field的屬性傳參是字段專用的。validationErrors指定校驗(yàn)失敗后的提示信息。這里的錯(cuò)誤信息會(huì)顯示在前面getControlGroup所定義HTML中{showError && (<p className="zent-form__error-desc">{props.error}</p>)}
ZentForm的核心驗(yàn)證邏輯是createForm的runRules方法,
runRules = (value, currentValues, validations = {}) => { const results = { errors: [], failed: [], }; function updateResults(validation, validationMethod) { // validation方法可以直接返回錯(cuò)誤信息,否則需要返回布爾值表明校驗(yàn)是否成功 if (typeof validation === 'string') { results.errors.push(validation); results.failed.push(validationMethod); } else if (!validation) { results.failed.push(validationMethod); } } Object.keys(validations).forEach(validationMethod => { ... // 使用自定義校驗(yàn)方法或內(nèi)置校驗(yàn)方法(可以按需添加) if (typeof validations[validationMethod] === 'function') { const validation = validations[validationMethod]( currentValues, value ); updateResults(validation, validationMethod); } else { const validation = validationRules[validationMethod]( currentValues, value, validations[validationMethod] ); } }); return results; };
默認(rèn)的校驗(yàn)時(shí)機(jī)是字段值改變的時(shí)候,可以通過(guò)Field的validateOnChange
和validateOnBlur
來(lái)改變校驗(yàn)時(shí)機(jī)。
<Field validateOnChange={false} validateOnBlur={false} validations={{ required: true, matchRegex: /^[a-zA-Z]+$/ }} validationErrors={{ required: '值不能為空', matchRegex: '只能為字母' }} />
對(duì)應(yīng)的,在Field組件中有2個(gè)方法來(lái)處理change和blur事件:
class Field extends Component { handleChange = (event, options = { merge: false }) => { ... this.setValue(newValue, validateOnChange); ... } handleBlur = (event, options = { merge: false }) => { ... this.setValue(newValue, validateOnBlur); ... } setValue = (value, needValidate = true) => { this.setState( { _value: value, _isDirty: true, }, () => { needValidate && this.context.zentForm.validate(this); } ); }; }
當(dāng)觸發(fā)驗(yàn)證的時(shí)候,ZentForm是會(huì)對(duì)表單對(duì)所有字段進(jìn)行驗(yàn)證,可以通過(guò)指定relatedFields
來(lái)告訴表單哪些字段需要同步進(jìn)行驗(yàn)證。
表單提交
表單提交時(shí),一般會(huì)經(jīng)歷如下幾個(gè)步驟
- 表單驗(yàn)證
- 表單提交
- 提交成功處理
- 提交失敗處理
ZentForm通過(guò)handleSubmit高階函數(shù)定義了上述幾個(gè)步驟,只需要傳入表單提交的邏輯即可:
const handleSubmit = (submit, zentForm) => { const doSubmit = () => { ... result = submit(values, zentForm); ... return result.then( submitResult => { ... if (onSubmitSuccess) { handleOnSubmitSuccess(submitResult); } return submitResult; }, submitError => { ... const error = handleSubmitError(submitError); if (error || onSubmitFail) { return error; } throw submitError; } ); } const afterValidation = () => { if (!zentForm.isValid()) { ... if (onSubmitFail) { handleOnSubmitError(new SubmissionError(validationErrors)); } } else { return doSubmit(); } }; const allIsValidated = zentForm.fields.every(field => { return field.props.validateOnChange || field.props.validateOnBlur; }); if (allIsValidated) { // 不存在沒(méi)有進(jìn)行過(guò)同步校驗(yàn)的field afterValidation(); } else { zentForm.validateForm(true, afterValidation); } }
使用方式如下:
const { handleSubmit } = this.props; <Form onSubmit={handleSubmit(this.submit)} horizontal>
ZentForm不足之處
ZentForm雖然功能強(qiáng)大,但仍有一些待改進(jìn)之處:
- 父組件維護(hù)了所有字段的實(shí)例,直接調(diào)用實(shí)例的方法來(lái)取值或者驗(yàn)證。這種方式雖然簡(jiǎn)便,但有違React聲明式編程和函數(shù)式編程的設(shè)計(jì)思想,并且容易產(chǎn)生副作用,在不經(jīng)意間改變了字段的內(nèi)部屬性。
- 大部分的組件重使用了shouldComponentUpdate,并對(duì)state和props進(jìn)行了深比較,對(duì)性能有比較大的影響,可以考慮使用PureComponent。
- 太多的情況下對(duì)整個(gè)表單字段進(jìn)行了校驗(yàn),比較合理的情況應(yīng)該是某個(gè)字段修改的時(shí)候只校驗(yàn)本身,在表單提交時(shí)再校驗(yàn)所有的字段。
- 表單提交操作略顯繁瑣,還需要調(diào)用一次handleSubmit,不夠優(yōu)雅。
結(jié)語(yǔ)
本文討論了Form表單組件設(shè)計(jì)的思路,并結(jié)合有贊的ZentForm組件介紹具體的實(shí)現(xiàn)方式。ZentForm的功能十分強(qiáng)大,本文只是介紹了其核心功能,另外還有表單的異步校驗(yàn)、表單的格式化和表單的動(dòng)態(tài)添加刪除字段等高級(jí)功能都還沒(méi)涉及到,感興趣的朋友可點(diǎn)擊前面的鏈接自行研究。
希望閱讀完本文后,你對(duì)React的Form組件實(shí)現(xiàn)有更多的了解,也歡迎留言討論。
相關(guān)文章
React項(xiàng)目中hook實(shí)現(xiàn)展示對(duì)話框功能
Modal(模態(tài)框)是 web 開發(fā)中十分常見的組件,即從頁(yè)面中彈出的對(duì)話框,下面這篇文章主要給大家介紹了關(guān)于React項(xiàng)目中hook實(shí)現(xiàn)展示對(duì)話框功能的相關(guān)資料,需要的朋友可以參考下2022-05-05react.js組件實(shí)現(xiàn)拖拽復(fù)制和可排序的示例代碼
這篇文章主要介紹了react.js組件實(shí)現(xiàn)拖拽復(fù)制和可排序的示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-08-08React使用useImperativeHandle自定義暴露給父組件的示例詳解
useImperativeHandle?是?React?提供的一個(gè)自定義?Hook,用于在函數(shù)組件中顯式地暴露給父組件特定實(shí)例的方法,本文將介紹?useImperativeHandle的基本用法、常見應(yīng)用場(chǎng)景,需要的可以參考下2024-03-03react實(shí)現(xiàn)導(dǎo)航欄二級(jí)聯(lián)動(dòng)
這篇文章主要為大家詳細(xì)介紹了react實(shí)現(xiàn)導(dǎo)航欄二級(jí)聯(lián)動(dòng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03使用 React 和 Threejs 創(chuàng)建一個(gè)VR全景項(xiàng)目的過(guò)程詳解
這篇文章主要介紹了使用 React 和 Threejs 創(chuàng)建一個(gè)VR全景項(xiàng)目的過(guò)程詳解,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04基于React實(shí)現(xiàn)表單數(shù)據(jù)的添加和刪除詳解
這篇文章主要給大家介紹了基于React實(shí)現(xiàn)表單數(shù)據(jù)的添加和刪除的方法,文中給出了詳細(xì)的示例供大家參考,相信對(duì)大家具有一定的參考價(jià)值,需要的朋友們下面來(lái)一起看看吧。2017-03-03