小而美的 React Form 組件

来源:http://www.cnblogs.com/guoxiaoming/archive/2017/09/09/7499173.html
-Advertisement-
Play Games

背景 之間在一篇介紹過 Table 組件《 React 實現一個漂亮的 Table 》 的文章中講到過,在企業級後臺產品中,用的最多且複雜的組件主要包括 Table、Form、Chart,在處理 Table 的時候我們遇到了很多問題。今天我們這篇文章主要是分享一下 Form 組件,在業務開發中, 相 ...


背景

之間在一篇介紹過 Table 組件《 React 實現一個漂亮的 Table 》 的文章中講到過,在企業級後臺產品中,用的最多且複雜的組件主要包括 Table、Form、Chart,在處理 Table 的時候我們遇到了很多問題。今天我們這篇文章主要是分享一下 Form 組件,在業務開發中, 相對 Table 來說,Form 處理起來更麻煩,不是所有表單都像註冊頁面那樣簡單,它往往需要處理非常多的邏輯,比如:

  • 需要處理數據驗證邏輯。
  • 在一個項目中的很少有可以復用的表單,每個表單的邏輯都需要單獨處理。
  • 在表單中往往存在需要自定義的組件,比如:Toggle、Tree、Picker 等等。

Form 作為一個功能型組件,它需要解決的問題無非就是兩個:

  • 把一個數據對象扔給它,它能夠在讓 Form 內的交互型組件獲取到數據並展示出來,同時這些組件上的數據也能反過來讓 Form 組件獲取到。
  • 對數據的有效性進行驗證。

在 React 項目開發中沒有用過其他 Form 相關的組件,但是這裡我又忍不住想和 Ant Design 的 Form 對比一下,以下是我在 antd 官網上的一個截圖:

antd form

大家可以通過以上圖片看到,我想輸入自己的中文名字都不能正常輸入,這裡主要存在兩個問題:

  • onChange 被觸發了多次,在一次中文未完整的輸入前,不應該觸發 onChange 事件。
  • 另外,中文在文本框內就不能正常顯示。

我在想這兩個問題不在組件上處理,在哪裡處理的呢?訪問地址: https://ant.design/components/form-cn/ 可以試一下,也希望他們可以解決掉這個問題。這個問題應該怎麼解決,我之前做過記錄: [React 中, 在 Controlled(受控制)的文本框中輸入中文 onChange 會觸發多次]( https://github.com/hypers/blog/blob/master/source/_posts/react-issue.md#react-中-在-controlled 受控制的文本框中輸入中文-onchange-會觸發多次)。

我們在設計的時候自然是解決了這些問題,預覽效果: https://rsuitejs.com/form-lib/ ,接下來看一下具體的設計。

設計

針對前面提到 Form 需要解決的兩個問題(數據校驗和數據獲取),在設計的時候,我們把這個兩個問題作為兩個功能,獨立在不同的庫處理。

  • form-lib 處理表單數據初始化,數據獲取。
  • rsuite-schema 定義數據模型,做數據有效性驗證。

這兩個庫可以獨立使用,如果你有自己一套自己的 Form 組件,只是缺少一個數據驗證的工具,那你可以單獨把 rsuite-schema 拿過來使用。

Form 定義一個表單

分別看一下它們是怎麼工作的,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>

Schema 定義一個數據模型

安裝

npm i rsuite-schema --save

rsuite-schema 主要有兩個對象

  • SchemaModel 用於定義數據模型。
  • Type 用於定義數據類型,包括:
    • StringType
    • NumbserType
    • ArrayType
    • DateType
    • ObjectType
    • BooleanType

這裡的 Type 有點像 React 中 PropTypes 的定義。

示例代碼

一個示例:

const userModel = SchemaModel(
    username: StringType().isRequired('用戶名不能為空'),
    email: StringType().isEmail('請輸入正確的郵箱'),
    age: NumberType('年齡應該是一個數字').range(18, 30, '年應該在 1830 歲')
});

這裡定義了一個 userModel, 包含 usernameemailage 3 個欄位, userModel 擁有了一個 check 方法, 當把數據扔進去後會返回驗證結果:

const checkResult = userModel.check({
    username: 'foobar',
    email: '[email protected]',
    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 文檔

Form 與 Schema 的結合

const userModel = SchemaModel({
    username: StringType().isRequired('用戶名不能為空'),
    email: StringType().isEmail('請輸入正確的郵箱'),
    age: NumberType('年齡應該是一個數字').range(18, 30, '年應該在 1830 歲')
});
<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 中的應用

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;

自定義 Field

如果一個組件不是原生表單控制項,也不是 RSuite 庫中提供的基礎組件,要在 form-lib 中使用,應該怎麼處理呢? 只需要在寫組件的時候實現以下對應的 API:

  • value : 受控時候設置的值
  • defalutValue: 預設值,非受控情況先設置的值
  • onChange: 組件數據發生改變的回調函數
  • onBlur: 在失去焦點的回調函數(可選)

接下來我們使用 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 地址:


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Angular Js 的初步認識和使用 一: 1.模塊化 定義模塊和控制器 ng-app="myapp" controller="myctrl" 指定模型 ng-model="" 獲取的屬性值: ng-bind="屬性名"或者{{屬性名}} 2.初始化模塊(在Script中進行) var myapp ...
  • 1.1 DOM操作對頁面的影響 通過js操作DOM的代價很高,影響頁面性能的主要問題有如下幾點: 訪問和修改DOM元素 修改DOM元素的樣式,導致重繪或重排 通過對DOM元素的事件處理,完成與用戶的交互功能 訪問和修改DOM元素 修改DOM元素的樣式,導致重繪或重排 通過對DOM元素的事件處理,完成 ...
  • querySelector 和 querySelectorAll 在傳統的 JavaScript 開發中,查找 DOM 往往是開發人員遇到的第一個頭疼的問題,原生的 JavaScript 所提供的 DOM 選擇方法並不多,僅僅局限於通過 tag, name, id 等方式來查找,這顯然是遠遠不夠的, ...
  • 編程練習 製作一個跳轉提示頁面: 要求: 1. 如果打開該頁面後,如果不做任何操作則5秒後自動跳轉到一個新的地址,如慕課網主頁。 2. 如果點擊“返回”按鈕則返回前一個頁面。 效果: 註意: 在視窗中運行該程式時,該視窗一定要有歷史瀏覽記錄,否則"返回"無效果。 我的解答 <!DOCTYPE htm ...
  • 有時候會需要用到字元的ASCII碼,一時之間調試時可能會忘記字元與ASCII碼對應的數字。 最近喜歡用瀏覽器控制台直接跑JS代碼,將這個代碼直接貼到瀏覽器控制台,即可調試(谷歌瀏覽器快捷鍵 ctrl+shift+j) function GetAsciiCode(){ var str = prompt ...
  • 1 1 /* 2 CSS重置 3 * */ 4 5 body, 6 ul, 7 ol { 8 margin: 0px; 9 padding: 0px; 10 } 11 12 #flash { 13 width: 600px; 14 height: 300px; 15 margin: 100px;..... ...
  • 接著上文,重新在webpack文件夾下麵新建一個項目文件夾demo2,然後用npm init --yes初始化項目的package.json配置文件,然後安裝webpack( npm install [email protected] --save-dev ),然後創建基本的項目文件夾結構,好了,我們的又一 ...
  • webpack,我想大家應該都知道或者聽過,Webpack是前端一個工具,可以讓各個模塊進行載入,預處理,再進行打包。現代的前端開發很多環境都依賴webpack構建,比如vue官方就推薦使用webpack.廢話不多說,我們趕緊開始吧. 第一步、安裝webpack 新建文件夾webpack->再在we ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...