之間在一篇介紹過 Table 組件《React 實現一個漂亮的 Table》 的文章中講到過,在企業級後臺產品中,用的最多且複雜的組件主要包括 Table、 Form、 Chart,在處理 Table 的時候咱們遇到了不少問題。今天咱們這篇文章主要是分享一下 Form 組件,在業務開發中, 相對 Table 來講,Form 處理起來更麻煩,不是全部表單都像註冊頁面那樣簡單,它每每須要處理很是多的邏輯,好比:html
Form 做爲一個功能型組件,它須要解決的問題無非就是兩個:react
在 React 項目開發中沒有用過其餘 Form 相關的組件,可是這裏我又忍不住想和 Ant Design 的 Form 對比一下,如下是我在 antd 官網上的一個截圖:git
你們能夠經過以上圖片看到,我想輸入本身的中文名字都不能正常輸入,這裏主要存在兩個問題:github
onChange
事件。我在想這兩個問題不在組件上處理,在哪裏處理的呢?訪問地址: https://ant.design/components... 能夠試一下,也但願他們能夠解決掉這個問題。這個問題應該怎麼解決,我以前作過記錄: React 中, 在 Controlled(受控制)的文本框中輸入中文 onChange 會觸發屢次。正則表達式
咱們在設計的時候天然是解決了這些問題,預覽效果: https://rsuitejs.com/form-lib/ ,接下來看一下具體的設計。npm
針對前面提到 Form 須要解決的兩個問題(數據校驗和數據獲取),在設計的時候,咱們把這個兩個問題做爲兩個功能,獨立在不一樣的庫處理。api
這兩個庫能夠獨立使用,若是你有本身一套本身的 Form 組件,只是缺乏一個數據驗證的工具,那你能夠單獨把 rsuite-schema
拿過來使用。antd
分別看一下它們是怎麼工做的,form-lib
用起來比較簡單,提供了兩個組件:函數
<Form>
處理整個表單服務的邏輯<Field>
處理表單中各個交互型組件的邏輯,好比 input
,select
。看一個示例:工具
首先須要安裝 form-lib
npm i form-lib --save
import { Form, Field, createFormControl } from 'form-lib'; const SelectField = createFormControl('select'); const user = { name:'root', status:1 };
<Form data={user}> <Field name="name" /> <Field name="status" accepter={SelectField} > <option value={1}>啓用</option> <option value={0}>禁用</option> </Field> </Form>
在默認狀況下 Field 是一個文本輸入組件,若是你須要使用 HTML 表單中其餘的標籤,你能夠像上面示例同樣 經過 createFormControl(標籤名稱)
方法建立一個組件, 在把這個組件經過 accepter
屬性賦給Field
組件,這樣你就能像操做原生 HTML 同樣設置 Field
。 經過這種方式,後面在功能介紹的時候會講到怎麼把自定義組件放在 Field
中。
這裏存在一個疑問,<Field>
必須放在 <Form>
下面第一層嗎? 若是對佈局沒有要求,設計成這樣是處理起來最方便的,由於在 <Form>
中能夠直接經過 props.children
獲取到全部的 <Field>
,想怎麼處理均可以。
剛開始咱們是這樣的,後來在實際應用中發現,表單的佈局是有不少種,若是要設計成這樣,那確定就帶來一個問題,很差自定義佈局。 因此這裏 <Form>
與 <Field>
之間的通訊咱們用的是 React 的 context。 這樣的話你就能夠任意佈局:
<Form data={user}> <div className="row"> <Field name="name" /> </div> <div className="row"> <Field name="status" accepter={SelectField} > <option value={1}>啓用</option> <option value={0}>禁用</option> </Field> </div> </Form>
<Form>
與 <Field>
詳細 API 說明,參考如下表格
<Form>
Props :
名稱 | 類型 | 描述 |
---|---|---|
horizontal | bool | 設置表單內的元素左右兩欄佈局 |
inline | bool | 設置表單內元素在一行佈局 |
values | object | 表單的值 受控組件 |
defaultValues | object | 表單的初始默認值 非受控組件 |
model | Schema | rsuite-schema 對象 |
checkDelay | number | 數據校驗的時候,延遲處理,默認爲 500 毫秒 |
checkTrigger | string | 數據校驗的觸發類型,可選項: change 、blur 、null ,默認爲:change |
onChange | function(values:Object, event:Object) | 數據改變後的回調函數 |
onError | function(errors:Object) | 校驗出錯的回調函數 |
onCheck | function(errors:Object) | 數據校驗的回調函數 |
errors | object | 表單錯誤信息 |
<Field>
Props :
名稱 | 類型 | 描述 |
---|---|---|
name | string | 表單元素名稱 |
accepter | elementType | 受代理的組件 |
npm i rsuite-schema --save
在 rsuite-schema
主要有兩個對象
SchemaModel
用於定義數據模型。Type
用於定義數據類型,包括:
這裏的 Type 有點像 React 中 PropTypes 的定義。
一個示例:
const userModel = SchemaModel( username: StringType().isRequired('用戶名不能爲空'), email: StringType().isEmail('請輸入正確的郵箱'), age: NumberType('年齡應該是一個數字').range(18, 30, '年應該在 18 到 30 歲') });
這裏定義了一個 userModel
, 包含 username
、email
、age
3個字段, userModel
擁有了一個 check
方法, 當把數據扔進去後會返回驗證結果:
const checkResult = userModel.check({ username: 'foobar', email: 'foo@bar.com', age: 40 }) // checkResult 結果: /** { username: { hasError: false }, email: { hasError: false }, age: { hasError: true, errorMessage: '年應該在 18 到 30 歲' } } **/
StringType() .minLength(6,'不能少於 6 個字符') .maxLength(30,'不能大於 30 個字符') .isRequired('該字段不能爲空');
經過 addRule 函數自定義一個規則。
若是是對一個字符串類型的數據進行驗證,能夠經過 pattern 方法設置一個正則表達式進行自定義驗證。
const myModel = SchemaModel({ field1: StringType().addRule((value) => { return /^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/.test(value); }, '請輸入合法字符'), field2: StringType().pattern(/^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/, '請輸入合法字符') });
例如,要經過值的不一樣狀況,返回不一樣的錯誤信息,參考如下
const myModel = SchemaModel({ field1: StringType().addRule((value) => { if(value==='root'){ return { hasError: true, errorMessage:'不能是關鍵字 root' } }else if(!/^[a-zA-Z]+$/.test(value)){ return { hasError: true, errorMessage:'只能是英文字符' } } return { hasError: false } }) });
const userModel = SchemaModel({ username:StringType().isEmail('正確的郵箱地址').isRequired('該字段不能爲空'), tag: ArrayType().of(StringType().rangeLength(6, 30, '字符個數只能在 6 - 30 之間')), profile: ObjectType().shape({ email: StringType().isEmail('應該是一個 email'), age: NumberType().min(18, '年齡應該大於18歲') }) })
更多設置,能夠查看 rsuite-schema API 文檔
const userModel = SchemaModel({ username: StringType().isRequired('用戶名不能爲空'), email: StringType().isEmail('請輸入正確的郵箱'), age: NumberType('年齡應該是一個數字').range(18, 30, '年應該在 18 到 30 歲') });
<Form model={userModel}> <Field name="username" /> <Field name="email" /> <Field name="age" /> </Form>
把定義的的SchemaModel
對象賦給,<Form>
的 model
屬性,就把它們綁定起來了,<Field>
的 name
對應 SchemaModel
對象中的 key
。
以上的示例代碼是不完整的,沒有處理錯誤信息和獲取數據,只是爲了方便你們理解。完整的示例,能夠參考接下來的實踐與解決方案。
import React from 'react'; import { Form, Field, createFormControl } from 'form-lib'; import { SchemaModel, StringType } from 'rsuite-schema'; const TextareaField = createFormControl('textarea'); const SelectField = createFormControl('select'); const model = SchemaModel({ name: StringType().isEmail('請輸入正確的郵箱') }); class DefaultForm extends React.Component { constructor(props) { super(props); this.state = { values: { name: 'abc', status: 0 }, errors: {} }; this.handleSubmit = this.handleSubmit.bind(this); } handleSubmit() { const { values } = this.state; if (!this.form.check()) { console.error('數據格式有錯誤'); return; } console.log(values, '提交數據'); } render() { const { errors, values } = this.state; return ( <div> <Form ref={ref => this.form = ref} onChange={(values) => { console.log(values); this.setState({ values }); // 清除表單全部的錯誤信息 this.form.cleanErrors(); }} onCheck={(errors) => { this.setState({ errors }); }} values={values} model={model} > <div className="form-group"> <label>郵箱: </label> <Field name="name" className="form-control" /> <span className="help-block error" style={{ color: '#ff0000' }}> {errors.name} </span> </div> <div className="form-group"> <label>狀態: </label> <Field name="status" className="form-control" accepter={SelectField} > <option value={1}>啓用</option> <option value={0}>禁用</option> </Field> </div> <div className="form-group"> <label>描述 </label> <Field name="description" className="form-control" accepter={TextareaField} /> </div> <button onClick={this.handleSubmit}> 提交 </button> </Form> </div> ); } } export default DefaultForm;
在 rsuite
提供了不少 Form
相關的組件,好比 FormGroup
,FormControl
,ControlLabel
,HelpBlock
等等, 咱們經過一個例子看一下怎麼結合使用。
經過上一個例子中咱們能夠看到,沒有個 Field
中有不少公共部分,因此咱們能夠自定義一個無狀態組件 CustomField
,把 ControlLabel
,Field
,HelpBlock
這些表單元素都放在一塊兒。
import React from 'react'; import { Form, Field, createFormControl } from 'form-lib'; import { SchemaModel, StringType, ArrayType } from 'rsuite-schema'; import { FormControl, Button, FormGroup, ControlLabel, HelpBlock, CheckboxGroup, Checkbox } from 'rsuite'; const model = SchemaModel({ name: StringType().isEmail('請輸入正確的郵箱'), skills: ArrayType().minLength(1, '至少應該會一個技能') }); const CustomField = ({ name, label, accepter, error, ...props }) => ( <FormGroup className={error ? 'has-error' : ''}> <ControlLabel>{label} </ControlLabel> <Field name={name} accepter={accepter} {...props} /> <HelpBlock className={error ? 'error' : ''}>{error}</HelpBlock> </FormGroup> ); class DefaultForm extends React.Component { constructor(props) { super(props); this.state = { values: { name: 'abc', skills: [2, 3], gender: 0, status: 0 }, errors: {} }; this.handleSubmit = this.handleSubmit.bind(this); } handleSubmit() { const { values } = this.state; if (!this.form.check()) { console.error('數據格式有錯誤'); return; } console.log(values, '提交數據'); } render() { const { errors, values } = this.state; return ( <div> <Form ref={ref => this.form = ref} onChange={(values) => { this.setState({ values }); console.log(values); }} onCheck={errors => this.setState({ errors })} defaultValues={values} model={model} > <CustomField name="name" label="郵箱" accepter={FormControl} error={errors.name} /> <CustomField name="status" label="狀態" accepter={FormControl} error={errors.status} componentClass="select" > <option value={1}>啓用</option> <option value={0}>禁用</option> </CustomField> <CustomField name="skills" label="技能" accepter={CheckboxGroup} error={errors.skills} > <Checkbox value={1}>Node.js</Checkbox> <Checkbox value={2}>Javascript</Checkbox> <Checkbox value={3}>CSS 3</Checkbox> </CustomField> <CustomField name="gender" label="性別" accepter={RadioGroup} error={errors.gender} > <Radio value={0}>男</Radio> <Radio value={1}>女</Radio> <Radio value={2}>未知</Radio> </CustomField> <CustomField name="bio" label="簡介" accepter={FormControl} componentClass="textarea" error={errors.bio} /> <Button shape="primary" onClick={this.handleSubmit}> 提交 </Button> </Form> </div> ); } } export default DefaultForm;
若是一個組件不是原生表單控件,也不是 RSuite 庫中提供的基礎組件,要在 form-lib 中使用,應該怎麼處理呢?
只須要在寫組件的時候實現如下對應的 API:
接下來咱們使用 rsuite-selectpicker 做爲示例, 在 rsuite-selectpicker 內部已經實現了這些 API。
import React from 'react'; import { SchemaModel, NumberType } from 'rsuite-schema'; import { Button, FormGroup, ControlLabel, HelpBlock } from 'rsuite'; import Selectpicker from 'rsuite-selectpicker'; import { Form, Field } from 'form-lib'; const model = SchemaModel({ skill: NumberType().isRequired('該字段不能爲空') }); const CustomField = ({ name, label, accepter, error, ...props }) => ( <FormGroup className={error ? 'has-error' : ''}> <ControlLabel>{label} </ControlLabel> <Field name={name} accepter={accepter} {...props} /> <HelpBlock className={error ? 'error' : ''}>{error}</HelpBlock> </FormGroup> ); class CustomFieldForm extends React.Component { constructor(props) { super(props); this.state = { values: { skill: 3, }, errors: {} }; this.handleSubmit = this.handleSubmit.bind(this); } handleSubmit() { const { values } = this.state; if (!this.form.check()) { console.error('數據格式有錯誤'); return; } console.log(values, '提交數據'); } render() { const { errors, values } = this.state; return ( <div> <Form ref={ref => this.form = ref} onChange={(values) => { this.setState({ values }); console.log(values); }} onCheck={errors => this.setState({ errors })} defaultValues={values} model={model} > <CustomField name="skill" label="技能" accepter={Selectpicker} error={errors.skill} data={[ { label: 'Node.js', value: 1 }, { label: 'CSS3', value: 2 }, { label: 'Javascript', value: 3 }, { label: 'HTML5', value: 4 } ]} /> <Button shape="primary" onClick={this.handleSubmit}> 提交 </Button> </Form> </div> ); } } export default CustomFieldForm;
更多示例:參考
若是你在使用中存在任何問題,能夠提交 issues,若是你有什麼好的想法歡迎你 pull request,GitHub地址: