前言
對于網頁系統來說,表單提交是一種很常見的與用戶交互的方式,比如提交訂單的時候,需要輸入收件人、手機號、地址等信息,又或者對系統進行設置的時候,需要填寫一些個人偏好的信息。 表單提交是一種結構化的操作,可以通過封裝一些通用的功能達到簡化開發的目的。本文將討論Form表單組件設計的思路,并結合有贊的ZentForm組件介紹具體的實現方式。本文所涉及的代碼都是基于React v15的版本。
Form組件功能
一般來說,Form組件的功能包括以下幾點:
下面將對每個部分的實現方式做詳細介紹。
表單布局
常用的表單布局一般有3種方式:
行內布局
	
水平布局
	
垂直布局
	
實現方式比較簡單,嵌套css就行。比如form的結構是這樣:
<form class="form"> <label class="label"/> <field class="field"/></form>
對應3種布局,只需要在form標簽增加對應的class:
<!--行內布局--><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>
相應的,要定義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;  ...}表單字段封裝
字段封裝部分一般是對組件庫的組件針對Form再做一層封裝,如Input組件、Select組件、Checkbox組件等。當現有的字段不能滿足需求時,可以自定義字段。
表單的字段一般包括兩部分,一部分是標題,另一部分是內容。ZentForm通過getControlGroup這一高階函數對結構和樣式做了一些封裝,它的入參是要顯示的組件:
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等信息,是通過Field組件傳入的:
<Field  label="預約門店:"  name="dept"  component={CustomizedComp}  validations={{    required: true,  }}  validationErrors={{    required: '預約門店不能為空',  }}  required/>這里的CustomizedComp是通過getControlGroup封裝后返回的組件。
字段與表單之間的交互是一個需要考慮的問題,表單需要知道它包含的字段值,需要在適當的時機對字段進行校驗。ZentForm的實現方式是在Form的高階組件內維護一個字段數組,數組內容是Field的實例。后續通過操作這些實例的方法來達到取值和校驗的目的。
ZentForm的使用方式如下:
class FieldForm extends React.Component {  render() {    return (      <Form>        <Field          name="name"          component={CustomizedComp}      </Form>    )  }}export default createForm()(FieldForm);其中Form和Field是組件庫提供的組件,CustomizedComp是自定義的組件,createForm是組件庫提供的高階函數。在createForm返回的組件中,維護了一個fields的數組,同時提供了attachToForm和detachFromForm兩個方法,來操作這個數組。這兩個方法保存在context對象當中,Field就能在加載和卸載的時候調用了。簡化后的代碼如下:
/** * createForm高階函數 */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, {...});  }}當需要獲取表單字段值的時候,只需要遍歷fields數組,再調用Field實例的相應方法就可以:
/** * createForm高階函數 */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;  };}表單驗證&錯誤提示
表單驗證是一個重頭戲,只有驗證通過了才能提交表單。驗證的時機也有多種,如字段變更時、鼠標移出時和表單提交時。ZentForm提供了一些常用的驗證規則,如非空驗證,長度驗證,郵箱地址驗證等。當然還能自定義一些更復雜的驗證方式。自定義驗證方法可以通過兩種方式傳入ZentForm,一種是通過給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傳參的方式,驗證規則是共享的,而Field的屬性傳參是字段專用的。validationErrors指定校驗失敗后的提示信息。這里的錯誤信息會顯示在前面getControlGroup所定義HTML中{showError && (<p className="zent-form__error-desc">{props.error}</p>)}
ZentForm的核心驗證邏輯是createForm的runRules方法,
runRules = (value, currentValues, validations = {}) => {  const results = {    errors: [],    failed: [],  };  function updateResults(validation, validationMethod) {    // validation方法可以直接返回錯誤信息,否則需要返回布爾值表明校驗是否成功    if (typeof validation === 'string') {      results.errors.push(validation);      results.failed.push(validationMethod);    } else if (!validation) {      results.failed.push(validationMethod);    }  }  Object.keys(validations).forEach(validationMethod => {    ...    // 使用自定義校驗方法或內置校驗方法(可以按需添加)    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;};	默認的校驗時機是字段值改變的時候,可以通過Field的validateOnChange和validateOnBlur來改變校驗時機。
<Field  validateOnChange={false}  validateOnBlur={false}  validations={{    required: true,    matchRegex: /^[a-zA-Z]+$/  }}  validationErrors={{    required: '值不能為空',    matchRegex: '只能為字母' }}/>對應的,在Field組件中有2個方法來處理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);      }    ); };}	當觸發驗證的時候,ZentForm是會對表單對所有字段進行驗證,可以通過指定relatedFields來告訴表單哪些字段需要同步進行驗證。
表單提交
表單提交時,一般會經歷如下幾個步驟
ZentForm通過handleSubmit高階函數定義了上述幾個步驟,只需要傳入表單提交的邏輯即可:
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) {    // 不存在沒有進行過同步校驗的field    afterValidation();  } else {    zentForm.validateForm(true, afterValidation);  }}使用方式如下:
const { handleSubmit } = this.props;<Form onSubmit={handleSubmit(this.submit)} horizontal>ZentForm不足之處
ZentForm雖然功能強大,但仍有一些待改進之處:
結語
本文討論了Form表單組件設計的思路,并結合有贊的ZentForm組件介紹具體的實現方式。ZentForm的功能十分強大,本文只是介紹了其核心功能,另外還有表單的異步校驗、表單的格式化和表單的動態添加刪除字段等高級功能都還沒涉及到,感興趣的朋友可點擊前面的鏈接自行研究。
希望閱讀完本文后,你對React的Form組件實現有更多的了解,也歡迎留言討論。
新聞熱點
疑難解答