優(yōu)雅地提高 React 的表單頁面的開發(fā)效率

Den Form

為什么叫 Den Form ? 可能是因為 丹鳳眼 非常迷人吧...

一個非常輕巧的 Form 實現(xiàn), gzip 體積只有 3kb, 可以很方便跨越組件層級獲取表單對象, 或者管理聯(lián)動更新

安裝

$ yarn add react-den-form 

基礎(chǔ)使用

Form 組件會在有 field 屬性的子組件上注入 onChange 事件, 并獲取其中的值

field 屬性是 Form 組件用于標(biāo)記哪一類子組件需要被監(jiān)管的字段, 并且也是用于校驗更新的 key

field 可以重復(fù), 但是如果兩個子組件擁有相同的 field 值, 那么當(dāng)此類 field 需要更新時, 兩個子組件都會進行更新

import React from "react";
import Form from "react-den-form";

export default () => {
  return (
    <div>
      <Form onSubmit={({ data }) => console.log(data)}>
        <input field="userName" />
      </Form>
    </div>
  );
};

當(dāng)我們輸入數(shù)據(jù)后, 按回車鍵, onSubmit 方法打印如下:

{userName: "333"}

主動傳入的 onChange 事件不會受到影響

Form 組件會在有 field 屬性的子組件上注入 onChange 事件, 并獲取其中的值

import React from "react";
import Form from "react-den-form";

export default () => {
  return (
    <div>
      <Form onChange={({ data }) => console.log(data)} onError>
        <input onChange={() => console.log("self-onChange")} field="userName" />
      </Form>
    </div>
  );
};

當(dāng)我們輸入數(shù)據(jù)時, onChange 方法打印如下:

self-onChange
{userName: "333"}

多層級獲取數(shù)據(jù)不需要進行額外處理

import React from "react";
import Form from "react-den-form";

export default () => {
  return (
    <div>
      <Form onChange={({ data }) => console.log(data)}>
        <div>
          <div>
            <div>
              <input field="userName" />
            </div>
            <input field="password" />
          </div>
        </div>
      </Form>
    </div>
  );
};

當(dāng)我們輸入數(shù)據(jù)時, onChange 方法打印如下:

{userName: "333", password: "555"}

子函數(shù)組件也不需額外處理

import React from "react";
import Form from "react-den-form";

function SubInput() {
  return (
    <div>
      <div>
        <input field="subPassword" />
      </div>
    </div>
  );
}

export default () => {
  return (
    <div>
      <Form onChange={({ data }) => console.log(data)}>
        <div>
          <div>
            <div>
              <input field="userName" />
            </div>
            <input field="password" />
            <SubInput />
          </div>
        </div>
      </Form>
    </div>
  );
};

當(dāng)我們輸入數(shù)據(jù)時, onChange 方法打印如下:

{userName: "333", password: "555", subPassword: "666"}

Form 組件嵌套不需要處理

由于 Form 內(nèi)部有一個 form 標(biāo)簽, 外層 onSubmit 會捕獲所有子組件的 onSubmit 事件, 但是 data 數(shù)據(jù)只會捕獲 當(dāng)前層級內(nèi)的 field 對象

export default () => {
  return (
    <div>
      {/* 此 Form 只會捕獲 userName及password, age及vipLevel被子Form攔截了 */}
      <Form onSubmit={({ data }) => console.log("1", data)}>
        <input field="userName" />
        <input field="password" />
        {/* 此 Form 只會捕獲 age及vipLevel */}
        <Form onSubmit={({ data }) => console.log("2", data)}>
          <input field="age" />
          <button type="submit">此Submit會被最近的父級捕獲</button>
        </Form>
      </Form>
    </div>
  );
};

自定義 Input 組件

如果我們自己定義的特殊組件, 需要滿足兩個條件:

  1. 組件外部的 props 需要設(shè)置 field 屬性
  2. 組件內(nèi)部需要使用 this.props.onChange 返回數(shù)據(jù)
import React from "react";
import Form from "react-den-form";

class SubInput extends React.Component {
  handleOnChange = e => {
    // 需要使用 this.props.onChange 返回數(shù)據(jù)
    this.props.onChange(e.target.value);
  };

  render() {
    return <input onChange={this.handleOnChange} />;
  }
}

export default () => {
  return (
    <div>
      <Form onChange={({ data }) => console.log(data)}>
        {/* 需要設(shè)置 field 屬性 */}
        <SubInput field="userName" />
      </Form>
    </div>
  );
};

類組件的嵌套需要使用 toForm 進行處理

被 Form 組件包裹過的類組件的 props 都會有一個 toForm 的函數(shù)

類組件不同于函數(shù)組件, 需要使用 toForm 對其內(nèi)部的 render 返回值進行處理, 才可以識別其內(nèi)部有 field 屬性的對象, 將其劃分在 Form 組件的監(jiān)管范圍內(nèi):

import React from "react";
import Form from "react-den-form";

// 例子, 這是一個有表單的頁面, 使用類組件進行編寫
class HomePage extends React.Component {
  render() {
    // 需要使用 toForm 處理返回值
    return this.props.toForm(
      <div>
        <nav>導(dǎo)航欄</nav>
        <div>
          <div>輸入?yún)^(qū)域</div>
          <input field="userName" />
        </div>
      </div>
    );
  }
}

export default () => {
  return (
    <div>
      <Form onChange={({ data }) => console.log(data)}>
        <HomePage />
      </Form>
    </div>
  );
};

使用 onChangeGetter 獲取自定義組件的值

以下標(biāo)簽, From 會自動識別 onChange 的返回值, 進行解析獲取

import React from "react";
import Form from "react-den-form";

export default () => {
  return (
    <div>
      <Form onChange={(...args) => console.log(args)}>
        <input field="userName" />
        <textarea field="password" />
        <select field="loginType">
          <option value="signUp">Sign up</option>
          <option value="signIn">Sign in</option>
        </select>
      </Form>
    </div>
  );
};

我們自己定義的特殊組件, 如果它們的 onChange 的返回值結(jié)構(gòu)不確定, 我們可以編寫 onChangeGetter 屬性:

import React from "react";
import Form from "react-den-form";

class SubInput extends React.Component {
  // 假定數(shù)據(jù)有一定的層級
  inputData = {
    value: ""
  };

  handleOnChange = e => {
    this.inputData.value = e.target.value;
    this.props.onChange(this.inputData);
  };

  render() {
    return <input onChange={this.handleOnChange} />;
  }
}

export default () => {
  return (
    <div>
      <Form onChange={({ data }) => console.log(data)}>
        <SubInput field="userName" onChangeGetter={e => e.value} />
      </Form>
    </div>
  );
};

onChangeGetter 的默認(rèn)值相當(dāng)于 onChangeGetter={e => e}

表單提交

以下三個情形為都會觸發(fā) Form 的 onSubmit 函數(shù):

  • 包含 field 屬性的對象中, 使用鍵盤的回車鍵
  • 包含 submit 屬性, 點擊(onClick)
  • 包含 type="submit" 屬性的對象中, 點擊(onClick)
import React from "react";
import Form from "react-den-form";

export default () => {
  return (
    <div>
      <Form onSubmit={({ data }) => console.log(data)}>
        <input field="userName" />
        <input submit field="password" />
        <button type="submit" />
      </Form>
    </div>
  );
};

fetch 提交

Form 表單內(nèi)部并無封裝請求行為, 請在 onSubmit 事件中自行處理, 如:

import React from "react";
import Form from "react-den-form";

function fetchLogin({ data }) {
  fetch("/api/login", { method: "post", body: JSON.stringify(data) })
    .then(res => {
      return res.json();
    })
    .then(data => {
      console.log(data);
    });
}

export default () => {
  return (
    <div>
      <Form onSubmit={fetchLogin}>
        <input field="userName" />
        <input field="password" />
      </Form>
    </div>
  );
};

上下文獲取數(shù)據(jù)

我們?yōu)?Form 顯式注入一個 data, 當(dāng)數(shù)據(jù)變化時, data 的值也會變化, 這樣可以在上下文獲取 Form 的數(shù)據(jù)

// React.Component 版本
import React from "react";
import Form from "react-den-form";

export default class extends React.Component {
  data = {};

  render() {
    return (
      <div>
        <Form data={this.data}>
          <input field="userName" />
          <button onClick={() => console.log(this.data)}>show-data</button>
        </Form>
      </div>
    );
  }
}
// useHooks 版本
import React, { useState } from "react";
import Form from "react-den-form";

export default () => {
  const [data] = useState({});

  return (
    <div>
      <Form data={data}>
        <input field="userName" />
        <button onClick={() => console.log(data)}>show-data</button>
      </Form>
    </div>
  );
};

輸入數(shù)據(jù), 點擊 button, data 數(shù)據(jù)打印如下:

{ userName: "dog" }

表單校驗

表單校驗是無痛的, 并且是高效的

我們給 input 組件添加 errorcheck 屬性, 該屬性可以是一個正則對象, 也可以是一個 函數(shù), 如果 errorcheck 校驗的結(jié)果為 false, 就會將其他 error 相關(guān)的屬性賦予至組件中

如下代碼, 如果 input 內(nèi)容不包含 123, 字體顏色為紅色:

import "./App.css";
import React from "react";
import Form from "react-den-form";

export default () => {
  return (
    <div>
      <Form>
        <input
          field="userName"
          errorcheck={/123/}
          errorstyle={{ color: "#f00" }}
        />
      </Form>
    </div>
  );
};

表單校驗相關(guān)的 api:

prop 類型 用途
errorcheck 正則或函數(shù) 若返回值為 false, 將其他 error Api 應(yīng)用至組件中
errorstyle style 對象 若校驗為錯誤, 將 errorstyle 合并至 style 中
errorclass className 字符串 若校驗為錯誤, 將 errorstyle 合并至 className 中
errorprops props 對象 若校驗為錯誤, 將 errorprops 合并至整個 props 中

為什么是 errorcheck 而不是 errorCheck ? 這是因為 React 對 DOM 元素屬性的定義為 lowercass:

Warning: React does not recognize the `errorCheck` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `errorcheck` instead. If you accidentally passed it from a parent component, remove it from the DOM element.

表單校驗時進行特殊處理

如果我們有一個需求, 當(dāng)表單校驗錯誤時, 顯示一個提示信息, 當(dāng)表單校驗通過時, 取消提示信息, 我們就需要對每次校驗有差異時, 進行處理

使用 onChange 方法每次都會被執(zhí)行, 可是我們只希望在表單校驗結(jié)果有變化時進行提示

Form 提供了一個 onErrorCheck 的屬性, 滿足以上需求

import React from "react";
import Form from "react-den-form";

export default () => {
  return (
    <div>
      {/* 只有當(dāng) input內(nèi)容校驗結(jié)果發(fā)生變化時, onErrorCheck 才會執(zhí)行 */}
      <Form onErrorCheck={({ isError, data }) => console.log(isError, data)}>
        <input
          field="userName"
          errorcheck={/123/}
          errorstyle={{ color: "#f00" }}
        />
      </Form>
    </div>
  );
};

聯(lián)動

當(dāng)我們修改一個對象時, 根據(jù)某些條件, 希望修改另一個對象的行為我們稱之為聯(lián)動

我們可以在任何 Form 的回調(diào)函數(shù)中使用 update 進行更新某個被 field 捆綁的組件的 value 或者 props

下面這個例子: 1. 當(dāng)在 password 輸入時, 會將 userName 的 input 框內(nèi)容改為 'new value'; 2. 當(dāng) userName 的 input 的內(nèi)容包含 'aa' 時, 會將 password 的 value 和 style 進行修改;

import React from "react";
import Form from "react-den-form";

export default () => {
  return (
    <div>
      <Form
        onChange={({ data, field, update }) => {
          if (field === "password") {
            update({ userName: "new value" });
          }
          if (/aa/.test(data.userName)) {
            update({
              password: {
                value: "new value and style",
                style: { fontSize: 30 }
              }
            });
          }
        }}
      >
        <input field="userName" />
        <input field="password" />
      </Form>
    </div>
  );
};

性能開銷

Form 存在的意義在于簡化開發(fā), 用計算機的時間換取開發(fā)者的時間, 所以會有一些性能開銷.

但是 Form 的開銷絕對不大, 因為 Form 內(nèi)部更新時只會針對指定的子組件進行更新.

  1. 每個包含 field 屬性的子組件都相當(dāng)于一個受控組件, 當(dāng)子組件 onChange 時, 此子組件會進行更新
  2. Form 組件聲明或被外部更新時會去查詢當(dāng)前 JSX 對象中的所有子組件是否包含 field 或者 submit 屬性, 如果包含, 則注入 onChange 或 onClick; 如果不希望 Form 被外部更新, 請聲明 <Form shouldUpdate={false} >{...}</Form>

如果因為使用 Form 遇到了性能問題, 請檢查以下情況:

  • 請減少 Form 內(nèi)部子組件的個數(shù), 最好不要超過 100 個
  • 在一個無限長的滾動列表外包裹 Form 時, 請盡量使用 react-virtualized 或 react-window 類型的虛擬 List 組件, 以減少 Form 包裹的內(nèi)容個數(shù)
  • 如果 Form 子組件的個數(shù)過多時, 請確保 Form 組件不會由外部頻繁更新, 或者添加 shouldUpdate={false} 至 Form 中: <Form shouldUpdate={false} >{...}</Form>

我們有理由相信, 在一個設(shè)計合理的應(yīng)用中, 每個 Form 包裹的組件個數(shù)應(yīng)該是有限的

支持哪些 React 渲染層 ?

此庫支持所有 React 的渲染層, 如 ReactDOM, ReactNative, ReactVR, 但是非 ReactDOM 中, 需要初始化事件類型

如 ReactNative 中, 在項目之初設(shè)定:

import { immitProps } from "react-den-form";

// 設(shè)定 ReactNative 中的讀取值和更新值的監(jiān)聽屬性:
immitProps.value = "value";
immitProps.change = "onChange";
immitProps.click = "onPress";

API

Form API

屬性 描述 類型 默認(rèn)值 必填
data 用于捆綁上下文的 data 對象, 也會當(dāng)成默認(rèn)值設(shè)置到相應(yīng)的 field 監(jiān)管的組件中 Object: {<field>:<value>} {} --
onChange 當(dāng)有 field 監(jiān)管的子組件執(zhí)行 onChange 時, 進行回調(diào) (IEvents) => void undefined --
onSubmit 當(dāng)有表單進行提交時, 進行回調(diào) (IEvents) => void undefined --
onErrorCheck 當(dāng)有 field 監(jiān)管的子組件的錯誤校驗結(jié)果發(fā)生變化時, 進行回調(diào) (IEvents) => void undefined --

IEvents API (回調(diào)函數(shù)的參數(shù))

Form 組件回調(diào)函數(shù)的參數(shù)如下: ({isError, event, data, field, value, element, update }) => void

具體的 API 描述如下:

屬性 描述 類型 默認(rèn)值 必傳
isError 最后輸入的對象校驗是否正確 Boolean false true
event onChange 的原始返回對象 Boolean undefined false
data form.data 對象, 包含所有 field 監(jiān)管的值 Object: {<field>:<value>} {} true
field 當(dāng)前修改的組件的 field 屬性 String undefined true
value 當(dāng)前修改的值 Boolean '' String or Number
element 當(dāng)前修改的 ReactElement Boolean React.Element true
update 更新某個 field 監(jiān)管的對象 (IEvents)=> {<field>:<value>} (IEvents)=> {<field>:<value>} true

子組件關(guān)聯(lián)參數(shù)

一個子組件可以被識別關(guān)聯(lián)的參數(shù)如下

<input
  field="userName"
  submit
  type="submit"
  errorcheck={/123/}
  errorstyle={{ color: "#f00" }}
  errorclass="input-error-style"
  errorprops={{ disable: true }}
/>

具體的 API 描述如下:

屬性 描述 類型 默認(rèn)值 必傳
field 用于標(biāo)記組件是否被 Form 組件監(jiān)聽, 并且也是 data 用于存放值的 key String undefined --
submit 用于確定當(dāng)前對象是否可以響應(yīng) onClick 事件進行提交 Boolean undefined --
type 當(dāng) type = "submit" 時, 可以響應(yīng) onClick 事件進行提交 Object: {<field>:<value>} undefined --
errorcheck 用于校驗當(dāng)前對象 value 是否錯誤 正則對象或函數(shù) undefined --
errorstyle 當(dāng)前對象 value 錯誤時, 合并 errorstyle 至 style Object undefined --
errorclass 當(dāng)前對象 value 錯誤時, 合并 errorclass 至 className String undefined --
errorprops 當(dāng)前對象 value 錯誤時, 合并 errorprops 至 props Object undefined --
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子厚掷,更是在濱河造成了極大的恐慌屋吨,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機蠢莺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來零如,“玉大人躏将,你說我怎么就攤上這事】祭伲” “怎么了祸憋?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長肖卧。 經(jīng)常有香客問我蚯窥,道長,這世上最難降的妖魔是什么塞帐? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任拦赠,我火速辦了婚禮,結(jié)果婚禮上葵姥,老公的妹妹穿的比我還像新娘荷鼠。我一直安慰自己,他們只是感情好榔幸,可當(dāng)我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布允乐。 她就那樣靜靜地躺著,像睡著了一般削咆。 火紅的嫁衣襯著肌膚如雪牍疏。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天拨齐,我揣著相機與錄音鳞陨,去河邊找鬼。 笑死奏黑,一個胖子當(dāng)著我的面吹牛炊邦,可吹牛的內(nèi)容都是我干的编矾。 我是一名探鬼主播熟史,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼馁害,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蹂匹?” 一聲冷哼從身側(cè)響起碘菜,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎限寞,沒想到半個月后忍啸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡履植,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年计雌,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片玫霎。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡凿滤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出庶近,到底是詐尸還是另有隱情翁脆,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布鼻种,位于F島的核電站反番,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏叉钥。R本人自食惡果不足惜罢缸,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望投队。 院中可真熱鬧祖能,春花似錦、人聲如沸蛾洛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽轧膘。三九已至钞螟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間谎碍,已是汗流浹背鳞滨。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蟆淀,地道東北人拯啦。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓澡匪,卻偏偏與公主長得像,于是被迫代替她去往敵國和親褒链。 傳聞我的和親對象是個殘疾皇子唁情,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,055評論 2 355