React-Redux技術(shù)?!畆edux-form詳解

React中沒有類似Angular那樣的雙向數(shù)據(jù)綁定,在做一些表單復(fù)雜的后臺(tái)類頁面時(shí),監(jiān)聽、賦值、傳遞、校驗(yàn)時(shí)編碼相對(duì)復(fù)雜,滿屏的樣板代碼傷痛欲絕,故引入可以解決這些問題的 redux-form (v6) 模塊。本文大致翻譯了官方文檔一些比較重要的地方,結(jié)合官方Demo加入了一些特性,有些官方跑不起來的地方也進(jìn)行了優(yōu)化。

目錄

<h2 id="getting-started">起步</h2>

在使用 redux-form 之前,需要具備以下基礎(chǔ):

關(guān)于 redux-form 的三個(gè)主要模塊:

  • formReducer reducer : 表單的各種操作以 Redux action 的方式,通過此 reducer 來促使 Redux store 數(shù)據(jù)的變化。
  • reduxForm() HOC : 此高階組件用以整合 Redux action 綁定的用戶交互與您的組件,并返回一個(gè)新的組件供以使用。
  • <Field/> : 用此代替您原本的 <input/> 組件,可以與redux-form的邏輯相連接。

數(shù)據(jù)流:

在大部分情況下您不需要關(guān)心如何創(chuàng)建action,一切都是自動(dòng)的。下圖展示了一個(gè)簡易的數(shù)據(jù)流:

Data flow
Data flow

舉個(gè)簡單的例子,我們有一個(gè)被 reduxForm() 創(chuàng)建的表單組件,里面有一個(gè)用 <Field/> 創(chuàng)建的 <input/> 組件,數(shù)據(jù)流大概是這個(gè)樣子的:

  1. 用戶點(diǎn)擊這個(gè) <input/> 組件,
  2. "Focus action" 被觸發(fā),
  3. formReducer 更新了對(duì)應(yīng)的狀態(tài),
  4. 這個(gè)狀態(tài)被傳回 <input/> 組件中。

與此類似的在這個(gè) <input/> 中輸入文字、更改狀態(tài)、提交表單,也是遵循以上這個(gè)流程。

redux-form 還能基于此流程處理許多事情,諸如:表單驗(yàn)證與格式化,多參數(shù)與action的創(chuàng)建?;谝韵碌南?qū)В?qǐng)自助挖掘更深層次的功能。

基本使用向?qū)?/h4>

步驟 1/4: Form reducer

store需要知道組件如何發(fā)送action,因此我們需要在您的store中注冊(cè) formReducer,他可以服務(wù)于整個(gè)app中你定義的所有表單組件,因此只需要注冊(cè)一次。

import { createStore, combineReducers } from 'redux'
import { reducer as formReducer } from 'redux-form'

const rootReducer = combineReducers({
  // ...your other reducers here
  // you have to pass formReducer under 'form' key,
  // for custom keys look up the docs for 'getFormState'
  form: formReducer
})

const store = createStore(rootReducer)

注: 在reducer中合并的formReducer的key必須命名為"form"。如果您因某些原因需要自定義key,請(qǐng)移步 getFormState config查看詳情。

步驟 2/4: Form component

為了使您的表單組件可以與store進(jìn)行交互,我們需要使用高價(jià)函數(shù) reduxForm() 來包裹您的組件。他可以在您執(zhí)行提交表單等操作的時(shí)候,以props的方式提供表單內(nèi)的state。

import React from 'react'
import { Field, reduxForm } from 'redux-form'

let ContactForm = props => {
  const { handleSubmit } = props
  return (
    <form onSubmit={ handleSubmit }>
      { /* form body*/ }
    </form>
  )
}

ContactForm = reduxForm({
  // a unique name for the form
  form: 'contact'
})(ContactForm)

export default ContactForm;

現(xiàn)在我們已經(jīng)有一個(gè)表單組件了,讓我們添加一些input組件。

注: 如果您覺得 ()() 這類的語法很迷惑,您可以把它分兩步來看:

// ...

// create new, "configured" function
createReduxForm = reduxForm({ form: 'contact' })

// evaluate it for ContactForm component
ContactForm = createReduxForm( ContactForm )

export default ContactForm;
步驟 3/4: Form <Field/> Components

<Field/> 組件可以連接所有input類型組件的數(shù)據(jù)到store中,基本用法如下:

<Field name="inputName" component="input" type="text" />

它創(chuàng)建了一個(gè)text類型的<input/>組件,還提供了諸如 value onChange onBlur等屬性,用于跟蹤和維護(hù)此組件的各種狀態(tài)。

注: <Field/> 組件很強(qiáng)大,除了基本的類型,還可以配置類或者無狀態(tài)組件,欲了解更多,請(qǐng)移步Field usage

import React from 'react'
import { Field, reduxForm } from 'redux-form'

const ContactForm = props => {
  const { handleSubmit } = props
  return (
    <form onSubmit={ handleSubmit }>
      <div>
        <label htmlFor="firstName">First Name</label>
        <Field name="firstName" component="input" type="text" />
      </div>
      <div>
        <label htmlFor="lastName">Last Name</label>
        <Field name="lastName" component="input" type="text" />
      </div>
      <div>
        <label htmlFor="email">Email</label>
        <Field name="email" component="input" type="email" />
      </div>
      <button type="submit">Submit</button>
    </form>
  )
}

ContactForm = reduxForm({
  // a unique name for the form
  form: 'contact'
})(ContactForm)

export default ContactForm;

從現(xiàn)在開始,表單上的操作數(shù)據(jù)已經(jīng)可以填充至store,并可以執(zhí)行提交表單操作了。

步驟 4/4: Reacting to submit

提交的數(shù)據(jù)以JSON對(duì)象的形式注入了此表單組件的 onSubmit 方法里了,可以打印出來看:

import React from 'react'
import ContactForm from './ContactForm'

class ContactPage extends React.Component {
  submit = (values) => {
    // print the form values to the console
    console.log(values)
  }
  render() {
    return (
      <ContactForm onSubmit={this.submit} />
    )
  }
}

<h2 id="field-value-lifecycle">表單value的生命周期</h2>

本節(jié)對(duì)理解您的組件value通過 redux-form 的流向很重要

Value 生命周期鉤子函數(shù)

redux-form 提供了3個(gè) value 生命周期鉤子函數(shù),通過props傳遞給Field組件,并且都是可選的。

format(value:Any) => String

格式化從store里拿出來的數(shù)據(jù)渲染到組件里,通常會(huì)在store保留原來的數(shù)據(jù)類型,只是在組件中使用的時(shí)候進(jìn)行格式化。

parse(value:String) => Any

把用戶輸入的string類型的數(shù)據(jù)進(jìn)行格式轉(zhuǎn)化,放入store供你使用,也會(huì)在store保留轉(zhuǎn)化后類型的數(shù)據(jù)。

normalize(value:Any, previousValue:Any, allValues:Object, previousAllValues:Object) => Any

允許您對(duì)當(dāng)前字段數(shù)據(jù)添加某些約束的邏輯,比如可以約束 midDate 的日期在 maxDate 之前等。如果你添加了這些邏輯,通過 normalize()的value將會(huì)被解析。

Value 生命周期

value lifecycle
value lifecycle

<h2 id="api">API</h2>

限于篇幅問題,在此只列舉每一種api常用的使用方法,具體請(qǐng)移步官方API文檔

<h4 id="redux-form">API:reduxForm(config:Object)</h4>

通過配置一些參數(shù)創(chuàng)建一個(gè)可以讓你配置你的表單的修飾器。諸如配置如何做表單驗(yàn)證、提交成功或失敗的回調(diào)、獲取或失去焦點(diǎn)的action發(fā)送、prop命名空間等,具體例子會(huì)在之后的demo中介紹。

Importing
var reduxForm = require('redux-form').reduxForm;  // ES5

import { reduxForm } from 'redux-form';  // ES6
常用參數(shù)介紹

必要參數(shù)

  • form : String[required] : 用于命名您的表單,在store生成此命名的數(shù)據(jù)節(jié)點(diǎn)。

可選參數(shù)

  • onChange : Function [optional] : 表單觸發(fā) onChange 事件后的回調(diào)。
  • onSubmit : Function [optional[ : 表單提交配置,可以配置需要提交哪些參數(shù),還有提交時(shí)觸發(fā)的 dispatch等。
  • onSubmitSuccess : Function [optional] & onSubmitFail : Function [optional] : 提交表單成功和失敗的回調(diào)。
  • shouldValidate(params) : boolean [optional] : 同步驗(yàn)證。
  • shouldAsyncValidate(params) : boolean [optional] : 異步驗(yàn)證。
  • touchOnBlur : boolean [optional] & touchOnChange : boolean [optional] : 標(biāo)識(shí) onBluronChange 的觸發(fā)。

<h4 id="props">API:props</h4>

列出全部當(dāng)前頁面由 redux-form 生成用于修飾此表單組件的props。

如果你希望用嚴(yán)格模式來編寫 PropTypes, redux-form 會(huì)導(dǎo)出此處所有的 propTypes,你需要引用他們并可以添加自己的propTypes,像這樣:

import {reduxForm, propTypes} from 'redux-form';

class SimpleForm extends Component {
  static propTypes = {
    ...propTypes,
    // other props you might be using
  }
  // ...
}
常用屬性
  • pristine : true 表示表單數(shù)據(jù)為原始數(shù)據(jù)沒被修改過,反之為 dirty
  • submitting : 用于表示您的表單提交狀態(tài),他只會(huì)在您的表單提交后返回一個(gè) promise 對(duì)象時(shí)起作用。 false 表示 promise 對(duì)象為 resolvedrejected 狀態(tài)。
  • handleSubmit(eventOrSubmit) : Function : 提交表單的函數(shù),如果表單需要驗(yàn)證,驗(yàn)證方法會(huì)被執(zhí)行(包括同步和異步)。調(diào)用方法有兩種:
    • 組件內(nèi)部直接調(diào)用 <form onSubmit={handleSubmit}>
    • 賦值給prop外部調(diào)用 <MyDecoratedForm onSubmit={data => {//do something with data.}}/>

<h4 id="Field">API:Field</h4>

所有您需要與 store 數(shù)據(jù)連接的表單組件,都可以用 <Field/>。在正確使用它之前,有三條基本概念您需要了解清楚:

  1. 必須包含 name 屬性??梢允呛唵蔚淖址?userName、password,也可以是復(fù)雜的結(jié)構(gòu),如 contact.billing.address[2].phones[1].areaCode
  2. 必須包含 component 屬性。可以是一個(gè)組件、無狀態(tài)組件或者DOM所支持的默認(rèn)的標(biāo)簽(input、textarea、select)。
  3. 其他所有屬性會(huì)通過prop傳遞到元素生成器中。如 className
Importing
var Field = require('redux-form').Field;  // ES5

import { Field } from 'redux-form';  // ES6
使用方法

1.組件

可以是任何自定義的 class 組件活著其他第三方庫。

// MyCustomInput.js
import React, { Component } from 'react'

class MyCustomInput extends Component {
  render() {
    const { input: { value, onChange } } = this.props
    return (
      <div>
        <span>The current value is {value}.</span>
        <button type="button" onClick={() => onChange(value + 1)}>Inc</button>
        <button type="button" onClick={() => onChange(value - 1)}>Dec</button>
      </div>
    )
  }
}

然后這樣使用:

import MyCustomInput from './MyCustomInput'

...

<Field name="myField" component={MyCustomInput}/>

2.無狀態(tài)組件

這是一個(gè)非常靈活的使用 <Field/> 的方法,使用方法和 redux-form 的前一個(gè)版本很相似。但必須在你的 render() 方法外定義它,否則它每次渲染都會(huì)被重建,并且由于組件的 prop 會(huì)變,就會(huì)強(qiáng)制 <Field/> 進(jìn)行渲染。如果你在 render() 內(nèi)部定義無狀態(tài)組件,不但會(huì)拖慢你的app,而且組件的input每次都會(huì)在組件重新渲染的時(shí)候失去焦點(diǎn)。

// outside your render() method
const renderField = (field) => (
    <div className="input-row">
      <input {...field.input} type="text"/>
      {field.meta.touched && field.meta.error &&
       <span className="error">{field.meta.error}</span>}
    </div>
  )

// inside your render() method
<Field name="myField" component={renderField}/>

3.string: input, select, or textarea

比如創(chuàng)建一個(gè)文字輸入框組件

<Field component="input" type="text"/>

<h4 id="Fields">API:Fields</h4>

Field 相似,但是它同時(shí)使用多個(gè)fields。<Fields/>name 屬性中使用一組表單name的數(shù)組,而不是用單一一個(gè) name 屬性來表示。

重要: 請(qǐng)節(jié)制使用 <Fields/>,其內(nèi)部任何表單組件數(shù)據(jù)變化時(shí),都會(huì)重新渲染整個(gè) <Fields/>。因此會(huì)成為您app的性能瓶頸。除非你真的需要這么做,最好還是用 <Field/> 來一個(gè)個(gè)自定義您的表單組件

Importing
var Fields = require('redux-form').Fields;  // ES5

import { Fields } from 'redux-form';  // ES6
使用方法

<Field/> 差不多,有2種使用方式,組件與無狀態(tài)組件,這里不詳細(xì)介紹。

<h4 id="FieldArray"> API:FieldArray </h4>

這個(gè)組件可以讓你定義一系列的表單,它的工作原理和 <Field/> 一樣。通過 <Field/>,給它一個(gè) name,就可以映射到 Redux state中的指定位置。組件也可以通過連接到 Redux stateprops 進(jìn)行渲染。

通過 <FieldArray/> ,你也需要和 <Field/> 一樣給它一個(gè) name。而你注入 <FieldArray/> 的組件會(huì)通過字段數(shù)組收到一系列的 props,用以查詢、更新和迭代。

Importing
var FieldArray = require('redux-form').FieldArray;  // ES5

import { FieldArray } from 'redux-form';  // ES6
使用方法

后面Demo里會(huì)具體介紹

<h4 id="Form"> API:Form </h4>

Form 組件對(duì)React的form組件進(jìn)行了簡單的封裝,用以觸發(fā)用 redux-form 修飾的組件的 onSubmit 函數(shù)。

您可以在以下場景中使用它:

  • 在您表單組件內(nèi)部,可以通過 onSubmit={this.props.handleSubmit(this.mySubmitFunction)} 執(zhí)行您的提交。
  • 或者
    • 通過 submit() Instance API來啟動(dòng)您的提交內(nèi)容。(即,在引用您的表單組件的地方直接調(diào)用)
    • 通過 dispatch 一個(gè) action 的方式啟動(dòng)調(diào)用。請(qǐng)參考 Remote Submit Example

如果您只是將 onSubmit 函數(shù)作為你的配置或?qū)傩?,那么你不需要用到這個(gè)組件。

Importing
var Form = require('redux-form').Form;  // ES5

import { Form } from 'redux-form';  // ES6
使用方法

只需要將您組件中所有 <form> 替換成 <Form> 即可。

<h4 id="FormSection"> API:FormSection </h4>

FormSection 可以很簡單地將現(xiàn)有的表單組件分割成更小的組件,用以在復(fù)雜的表單中進(jìn)行復(fù)用。它是通過明確規(guī)定好的 Field、FieldsFieldArray字組件 name的前綴來完成此功能的。

使用方法

這個(gè)例子所描述的業(yè)務(wù)是一個(gè)購買人與收件人視角的訂單用戶信息表單結(jié)構(gòu)。購買人與收件人擁有相同的字段結(jié)構(gòu),因此把這個(gè)部分拆分成一個(gè)名為 Party 的組件是有意義的。假設(shè)現(xiàn)在 Party 包含 givenName middleName surname address 這幾個(gè)字段,然后將 address 部分再度拆分成可重用的組件 Address。代碼如下:

//Address.js
class Address extends Component {
    render() {
        return <div>
            <Field name="streetName" component="input" type="text"/>
            <Field name="number" component="input" type="text"/>
            <Field name="zipCode" component="input" type="text"/>
        </div>
    }
}

//Party.js
class Party extends Component {
    render() {
        return <div>
            <Field name="givenName" component="input" type="text"/>
            <Field name="middleName" component="input" type="text"/>
            <Field name="surname" component="input" type="text"/>
            <FormSection name="address">
                <Address/>
            </FormSection>
        </div>
    }
}

//OrderForm.js
class OrderForm extends Component {
    render() {
        return <form onsubmit={...}>
            <FormSection name="buyer">
                <Party/>
            </FormSection>
            <FormSection name="recipient">
                <Party/>
            </FormSection>
        </form>
    }
}
//don't forget to connect OrderForm with reduxForm()

字段完整的名字最后將變成如 buyer.address.streetName 的形式,結(jié)果結(jié)構(gòu)如下:

{
    "buyer": {
        "givenName": "xxx",
        "middleName": "yyy",
        "surname": "zzz",
        "address": {
            "streetName": undefined,
            "number": "123",
            "zipCode": "9090"
        }
    },
    "recipient": {
        "givenName": "aaa",
        "middleName": "bbb",
        "surname": "ccc",
        "address": {
            "streetName": "foo",
            "number": "4123",
            "zipCode": "78320"
        }
    }
}

類似 Address 的組件很少更改它的 name,為了使組件繼承 FormSection 而不是 Component,需要設(shè)置一個(gè)默認(rèn)的 name 如下:

class Address extends FormSection {
    //ES2015 syntax with babel transform-class-properties
    static defaultProps = {
        name: "address"
    }
    render() {
        return <div>
            <Field name="streetName" component="input" type="text"/>
            <Field name="number" component="input" type="text"/>
            <Field name="zipCode" component="input" type="text"/>
        </div>
    }
}
//Regular syntax:
/*
Address.defaultProps = {
    name: "address"
}
*/

<h4 id="formValues"> API:formValues() </h4>

作為一個(gè)修飾,可以讀取當(dāng)前表單的 value。當(dāng)表單子組件的 onChange 依賴于當(dāng)前表單里的值,很有用。

Importing
var formValues = require('redux-form').formValues;  // ES5

import { formValues } from 'redux-form';  // ES6
使用方法
const ItemList = formValues('withVat')(MyItemizedList)

const ItemList = formValues({showVat: 'withVat'})(MyItemizedList)

這些裝飾組件現(xiàn)在分別擁有了 withVatshowVatprops。

<h4 id="formValueSelector"> API:formValueSelector() </h4>

formValueSelector 的API可以很方便的 connect() state的值到表單的 value 里。它可以通過表單的 name 為你的表單創(chuàng)建一個(gè) value 拾取器。

Importing
var formValueSelector = require('redux-form').formValueSelector;  // ES5

import { formValueSelector } from 'redux-form';  // ES6
使用方法

首先需要按照你表單的 name 創(chuàng)建一個(gè) selector。

const selector = formValueSelector('myFormName')

然后有幾種方法使用 selector:

1.拾取個(gè)別的字段

connect(
  state => ({
    firstValue: selector(state, 'first'),
    secondValue: selector(state, 'second')
  })
)(MyFormComponent)

2.在分好組的 prop 中按組的方式拾取多個(gè)字段

connect(
  state => ({
    myValues: selector(state, 'first', 'second')
  })
)(MyFormComponent)

3.把 selector 當(dāng)作 mapStateToProps 來使用

如果你不需要 state 中其他的屬性值,selector作為mapStateToProps可以自動(dòng)完成這個(gè)工作。

connect(
  state => selector(state, 'first', 'second')
)(MyFormComponent)

<h4 id="reducer"> API:reducer </h4>

表單的reducer用來安裝您的 Redux state 到您的表單中。

如果您使用 Immutablejs 來管理您的 Redux state,你必須這么從 redux-form/immutable 中導(dǎo)入 reducer 模塊。

ES5例子
var redux = require('redux');
var formReducer = require('redux-form').reducer;
// Or with Immutablejs:
// var formReducer = require('redux-form/immutable').reducer;

var reducers = {
  // ... your other reducers here ...
  form: formReducer
};
var reducer = redux.combineReducers(reducers);
var store = redux.createStore(reducer);
ES6例子
import { createStore, combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';
// Or with Immutablejs:
// import { reducer as formReducer } from 'redux-form/immutable';

const reducers = {
  // ... your other reducers here ...
  form: formReducer
};
const reducer = combineReducers(reducers);
const store = createStore(reducer);

<h4 id="reducer.plugin"> API:reducer.plugin </h4>

表單中返回一個(gè)通過附加指定功能 reducers 用以接受 actionreducer。 它的參數(shù)應(yīng)該是一個(gè)能映射 formName和一個(gè)(state, action) => nextState reducer 關(guān)系的一個(gè)對(duì)象。通過每一個(gè) reducer的state只能是屬于那個(gè)表單的一個(gè)片段。

說明

flux 體系中最美的一部分應(yīng)該是所有 reducers(或者 Flux中的標(biāo)準(zhǔn)術(shù)語 stores)可以接受所有 actions,他們可以修改基于這些 action來修改數(shù)據(jù)。舉個(gè)例子,你有一個(gè)登錄的表單,當(dāng)你提交失敗的時(shí)候,你想清楚密碼輸入框內(nèi)的數(shù)據(jù),哪怕你的登錄的提交信息是屬于另一個(gè) reducer/actions體系,你的表單依然可以做出自己的響應(yīng)。

而不是使用 redux-form 中一個(gè)普通的 reducer,你可以通過調(diào)用 plugin() 函數(shù)來加強(qiáng)你的 reducer。

注:這是一個(gè)加強(qiáng)功能的操作用來修改你內(nèi)部的 redux-form state的片段,如果你不小心使用,會(huì)把事情搞砸。

例子

下面這個(gè)例子的作用是,當(dāng) AUTH_LOGIN_FAILaction 被分發(fā)時(shí),可以清除登錄表單里的密碼輸入框:

import { createStore, combineReducers } from 'redux'
import { reducer as formReducer } from 'redux-form'
import { AUTH_LOGIN_FAIL } from '../actions/actionTypes'

const reducers = {
  // ... your other reducers here ...
  form: formReducer.plugin({
    login: (state, action) => {   // <----- 'login' is name of form given to reduxForm()
      switch(action.type) {
        case AUTH_LOGIN_FAIL:
          return {
            ...state,
            values: {
              ...state.values,
              password: undefined // <----- clear password value
            },
            fields: {
              ...state.fields,
              password: undefined // <----- clear field state, too (touched, etc.)
            }
          }
        default:
          return state
      }
    }
  })
}
const reducer = combineReducers(reducers)
const store = createStore(reducer)

<h4 id="SubmissionError"> API: SubmissionError </h4>

這個(gè) throwable error 用于從 onSubmit 返回一個(gè)表單驗(yàn)證錯(cuò)誤信息。目的是用來區(qū)分 promise 失敗的原因究竟是驗(yàn)證錯(cuò)誤、AJAX I/O錯(cuò)誤還是其他服務(wù)器錯(cuò)誤。如果它是由于表單里 { field1: 'error', field2: 'error' }產(chǎn)生的錯(cuò)誤,那這個(gè)錯(cuò)誤將會(huì)被添加到每一個(gè)標(biāo)記過錯(cuò)誤屬性的字段里,就像異步表單驗(yàn)證錯(cuò)誤一樣。如果有一個(gè)錯(cuò)誤沒有指定的字段,但是應(yīng)用到了整個(gè)表單,你需要繼續(xù)傳遞它,就好像是某個(gè)字段調(diào)用的 _error一樣,然后他會(huì)給出一個(gè)錯(cuò)誤的屬性。(就是不管他往外拋)

Importing
var SubmissionError = require('redux-form').SubmissionError;  // ES5

import { SubmissionError } from 'redux-form';  // ES6
使用方法
<MyForm onSubmit={values =>
  ajax.send(values) // however you send data to your server...
    .catch(error => {
      // how you pass server-side validation errors back is up to you
      if(error.validationErrors) {
        throw new SubmissionError(error.validationErrors)
      } else {
        // what you do about other communication errors is up to you
      }
    })
}/>

<h4 id="Action-Creators"> API: Action Creators </h4>

redux-form 對(duì)外開放了所有的內(nèi)部 action creators,允許你按找你的意愿來完成對(duì)分發(fā) action 的控制。進(jìn)而,官方推薦您在完成您大部分需求的時(shí)候,對(duì)于那些表單里指定需求的字段的 action來說,當(dāng)作這些 action 已經(jīng)綁定到 dispatch一樣,直接將這些 action 通過 props 傳遞。

具體 action 請(qǐng)參考官方文檔。

<h4 id="Selectors"> API: Selectors </h4>

redux-form 提供了一系列有用的 Redux state 拾取器,可以在app的任何地方任何表單內(nèi)拾取 state 上的數(shù)據(jù)。

下列所有拾取器擁有統(tǒng)一的使用方法: 他們都(除了getFormNames)使用表單的名字,來創(chuàng)建一個(gè)拾取器,無論表單的 state是什么。

import {
  getFormValues,
  getFormInitialValues,
  getFormSyncErrors,
  getFormMeta,
  getFormAsyncErrors,
  getFormSyncWarnings,
  getFormSubmitErrors,
  getFormNames,
  isDirty,
  isPristine,
  isValid,
  isInvalid,
  isSubmitting,
  hasSubmitSucceeded,
  hasSubmitFailed
} from 'redux-form'

MyComponent = connect(
  state => ({
    values: getFormValues('myForm')(state),
    initialValues: getFormInitialValues('myForm')(state),
    syncErrors: getFormSyncErrors('myForm')(state),
    fields: getFormMeta('myForm')(state),
    asyncErrors: getFormAsyncErrors('myForm')(state),
    syncWarnings: getFormSyncWarnings('myForm')(state),
    submitErrors: getFormSubmitErrors('myForm')(state),
    names: getFormNames('myForm')(state),
    dirty: isDirty('myForm')(state),
    pristine: isPristine('myForm')(state),
    valid: isValid('myForm')(state),
    invalid: isInvalid('myForm')(state),
    submitting: isSubmitting('myForm')(state),
    submitSucceeded: hasSubmitSucceeded('myForm')(state),
    submitFailed: hasSubmitFailed('myForm')(state)
  })
)(MyComponent)

<h2 id="Examples">Examples</h2>

<h3 id="Simple"> Demo: Simple Form </h3>

這個(gè)例子把表單所有基本的元素都列了出來,和官方Demo有所區(qū)別的是,增加了2個(gè) typefileField (直接在 Field 中使用 file 的類型會(huì)有點(diǎn)問題),一個(gè)是使用了jQuery的 dropify 編寫的上傳單個(gè)文件的組件 MyDropify,一個(gè)是使用了 dropzone 編寫的上傳多個(gè)文件的組件 MyDropzone (在這里使用了 react-dropzoneredux-form 的組合)。官方的例子不單獨(dú)介紹了,主要貼一下兩個(gè)自定義 Field。

注:由于reducer設(shè)計(jì)之初是純函數(shù),而提交文件的表單最后取得的值是一個(gè) file 對(duì)象,當(dāng)您使用了 redux-immutable-state-invariant 之類的檢測工具,對(duì)其中諸如 lastModifiedDate 的值會(huì)報(bào)錯(cuò),具體請(qǐng)看。在此,我們暫時(shí)先不考慮immutable的問題。

Simple路徑

src/components/demo/simple/

MyDropify

src/components/utils/MyDropify.js

代碼:

import React, { Component } from 'react';
const $ = window.$;
require('dropify');

class MyDropify extends Component {
  componentDidMount(){
    $('.dropify').dropify();
  }
  render() {
    const { input,dataAllowedFileExtensions } = this.props
    const onAttachmentChange = (e) => {
        e.preventDefault();
        const files = [...e.target.files];
        input.onChange(files);
    };
    return (
      <div>
        <input type="file"
               onChange={onAttachmentChange}
               className="dropify"
               data-allowed-file-extensions={dataAllowedFileExtensions} />
      </div>
    )
  }
}

export default MyDropify;

使用方法:

  <div className="form-group">
    <div className="input-group">
      <label>Dropify</label>
      <Field component={MyDropify}
             name="inputfile1"
             dataAllowedFileExtensions="doc docx txt pdf xls xlsx jpg png bmp"></Field>
    </div>
  </div>

dropify 的具體用法請(qǐng)參考其官方文檔。

MyDropzone

src/components/utils/MyDropify.js

代碼:

import React, { Component } from 'react';
import Dropzone from 'react-dropzone';
class MyDropzone extends Component {
  render() {
    const { input,desc,accept } = this.props
    const onDrop = (files) => {
        input.onChange(files);
    };
    return (
      <Dropzone onDrop={onDrop} accept={accept}>
        {({ isDragActive, isDragReject, acceptedFiles, rejectedFiles }) => {
           if (isDragActive) {
             return "This file is authorized";
          }
           if (isDragReject) {
             return "This file is not authorized";
          }
           return acceptedFiles.length || rejectedFiles.length
             ? `Accepted ${acceptedFiles.length}, rejected ${rejectedFiles.length} files`
            : desc;
        }}
      </Dropzone>
    )
  }
}

export default MyDropzone;

使用方法:

  <div className="form-group">
    <div className="input-group">
      <label>Dropzone</label>
      <Field component={MyDropzone}
             name="inputfile2"
             desc="My Dropzone"
             accept="image/png,image/jpeg"></Field>
    </div>
  </div>

react-dropzone 和jQuery版本的有所區(qū)別,使用過 dropzone 的應(yīng)該都知道選擇文件可以渲染到框體內(nèi),react版本的 dropzone 原聲不帶這個(gè)功能,但它提供了詳盡的方法可以自己實(shí)現(xiàn)很多功能,比如選擇完文件可以渲染到組件中,有時(shí)間我再完善此功能。

<h3 id="snycValidation"> Demo: Sync Validation </h3>

同步的表單驗(yàn)證,包括了錯(cuò)誤和警告型配置。官方Demo中只演示了輸入框的驗(yàn)證,而這里準(zhǔn)備了包括 radio select textarea 的驗(yàn)證方式(checkbox 我會(huì)在單獨(dú)的一章講解),調(diào)用方法可以參見本文的源代碼。

Sync Validation路徑

src/components/demo/syncValidation/

radioField

src/components/utils/validation/radioField.js

import React from 'react';

const inputField = ({
  input,
  label,
  type,
  meta: { touched, error, warning }
}) => (
  <div className={touched && error ? 'has-error form-group':'form-group'}>
    <div className="input-group">
      <span className="input-group-addon">{label}</span>
      <input {...input} placeholder={label} type={type} className="form-control"/>
    </div>
    {touched &&
      ((error && <div className="help-block with-errors">{error}</div>) ||
        (warning && <div className="help-block with-errors">{warning}</div>))}
  </div>
)

export default inputField;
selectField

src/components/utils/validation/selectField.js

import React from 'react';
const selectField = ({
  input,
  label,
  selects,
  meta: { touched, error, warning }
}) => (
  <div className={touched && error ? 'has-error form-group':'form-group'}>
    <div className="input-group">
      <span className="input-group-addon">{label}</span>
      <select {...input} className="form-control">
        {
          selects.map((item, i) => (
            <option key={i} value={item.value}>{item.text}</option>
          ))
        }
      </select>
    </div>
    {touched &&
      ((error && <div className="help-block with-errors">{error}</div>) ||
        (warning && <div className="help-block with-errors">{warning}</div>))}
  </div>
)

export default selectField;
textareaField

src/components/utils/validation/textareaField.js

import React from 'react';

const textareaField = ({
  input,
  label,
  type,
  cols,
  rows,
  meta: { touched, error, warning }
}) => (
  <div className={touched && error ? 'has-error form-group':'form-group'}>
    <label>{label}</label>
    <textarea {...input} cols={cols} rows={rows} className="form-control"></textarea>
    {touched &&
      ((error && <div className="help-block with-errors">{error}</div>) ||
        (warning && <div className="help-block with-errors">{warning}</div>))}
  </div>
)

export default textareaField;

<h3 id="FieldLevelValidation"> Demo: Field-Level Validation </h3>

除了提供一個(gè)驗(yàn)證方法一起驗(yàn)證表單里的值這種方法之外,還可以對(duì)每一個(gè) <Field/><FieldArray/> 分別做驗(yàn)證。官方給的Demo已經(jīng)足夠說明問題了,在這里只針對(duì)上面的 Sync Validation 作簡單的改寫。具體請(qǐng)看代碼。

<h3 id="SubmitValidation"> Demo: Submit Validation </h3>

一種服務(wù)器表單驗(yàn)證較好的方法是在調(diào)用 onSubnit 之后返回一個(gè) rejectedpromise 對(duì)象。當(dāng)您的表單被提交時(shí),有2種方法提供給 redux-form 這個(gè)函數(shù)。

  1. 把他當(dāng)作一個(gè) onSubmitprop 傳遞給您的裝飾組件。那樣的話,你可以在您的裝飾組件中使用 onSubmit={this.props.handleSubmit} 確保當(dāng)用戶點(diǎn)擊提交按鈕的時(shí)候觸發(fā)這個(gè)函數(shù)。
  2. 把他當(dāng)作一個(gè)參數(shù)傳遞給您裝飾組件內(nèi)的 this.props.handleSubmit 函數(shù)。這種情況下,你需要使用 onClick={this.props.handleSubmit(mySubmit)} 來確保當(dāng)用戶點(diǎn)擊提交按鈕的時(shí)候觸發(fā)這個(gè)函數(shù)。

這個(gè)錯(cuò)誤信息的顯示方式和同步驗(yàn)證(Synchronous Validation)后的錯(cuò)誤信息一樣,但他是通過 onSubmit 函數(shù)返回一個(gè)封裝過的 SubmissionError 對(duì)象。這個(gè)驗(yàn)證錯(cuò)誤就像HTTP的400或500錯(cuò)誤一樣,和I/O錯(cuò)誤是有區(qū)別的,并且他還會(huì)是這個(gè)提交的 promise 對(duì)象的狀態(tài)置為 rejected。

DEMO中沒什么花頭,和官方一樣,就是基于 SyncValidation 把表單驗(yàn)證的邏輯放在了提交后的邏輯中,并拋出了一個(gè) SubmissionError。

<h3 id="AsyncValidation"> Demo: Async Validation </h3>

服務(wù)器表單驗(yàn)證的方式比較推薦使用Submit Validation,但是可能存在當(dāng)您填寫表單的時(shí)候,同時(shí)需要服務(wù)器端來驗(yàn)證。有一個(gè)經(jīng)典的例子是當(dāng)一個(gè)用戶選取一個(gè)值,比如用戶名,它必須是您系統(tǒng)中唯一的一個(gè)值。

為了寫一個(gè)異步的表單驗(yàn)證,需要給 redux-form 提供一個(gè)異步驗(yàn)證的函數(shù)(asyncValidation)用來提供一個(gè)可以從表單獲取數(shù)據(jù)的一個(gè)對(duì)象,然后 Redux 分發(fā)這個(gè)函數(shù),返回一個(gè)狀態(tài)為擁有一個(gè)錯(cuò)誤對(duì)象的 rejects或狀態(tài)為 reslovepromise 對(duì)象。

您需要同時(shí)指定某幾個(gè)字段,通過 asyncBlurFields 的屬性配置,來標(biāo)記是否需要在他們失去焦點(diǎn)的時(shí)候觸發(fā)這個(gè)異步驗(yàn)證。

重要
  1. 異步驗(yàn)證會(huì)在 onSubmit 之前被調(diào)用,所以如果你關(guān)心的是 onSubmit 驗(yàn)證,你需要使用 Submit Validation
  2. 當(dāng)一個(gè)字段的同步驗(yàn)證錯(cuò)誤時(shí),那它的失去焦點(diǎn)的時(shí)候?qū)⒉粫?huì)觸發(fā)異步驗(yàn)證。

Demo中的自定義 <Field/>meta 中有一個(gè) asyncValidating,來標(biāo)識(shí)異步驗(yàn)證的 promise 對(duì)象的 Pending 狀態(tài)。

<h3 id="initializeFromState"> Demo: Initialize From State </h3>

通過 initialValues 屬性或 reduxForm() 配置的參數(shù)所提供的數(shù)據(jù),被加載到表單 state 中,并且把這些初始化數(shù)據(jù)作為原始數(shù)據(jù)(pristine)。當(dāng) reset() 觸發(fā)的時(shí)候,也會(huì)返回這些值。除了保存這些 pristine 值,初始化您表單的這個(gè)操作也會(huì)替換表單里已經(jīng)存在的值。

在許多應(yīng)用中,這些值可能是來自服務(wù)器并且儲(chǔ)存在其他 reducer 中的。想要得到這些值,你需要使用 connect() 去自己鏈接 state 然后映射這些數(shù)據(jù)到您的 initialValues 屬性里。

默認(rèn)情況下,你只需要通過 initialValues 初始化您的表單組件一次即可。目前有2種方法可以通過新的 pristine 值重新初始化表單。

  1. 傳遞一個(gè) enableReinitialize 屬性或配置 reduxForm() 中的參數(shù)為true就可以讓表單在每次 initialValues 屬性變化的時(shí)候重新初始化,生成一個(gè)新的 pristine 值。如果想要在重新初始化的時(shí)候保持已改變過的表單的值,可以設(shè)置 keepDirtyOnReinitialize 為true。默認(rèn)情況下,重新初始化會(huì)將 pristine 值替換掉已改變過的表單的值。
  2. 發(fā)出一個(gè) INITIALIZE action(用 redux-form action生成器生成)。

此Demo較之官方Demo,增加了 enableReinitializekeepDirtyOnReinitialize 的用法。以下是代碼片段。

InitializeFromStateForm = reduxForm({
  form: 'initializeFromState',// a unique identifier for this form
  enableReinitialize:true,
  keepDirtyOnReinitialize:true,// 這個(gè)值表示重新初始化表單后,不替換已更改的值,可以用clear來測試
})(InitializeFromStateForm)

<h3 id="selectingFormValues"> Demo: Selecting Form Values </h3>

有時(shí)候您希望訪問表單組件中某些字段的值,你需要在 store 中直接 connect() 表單的值。在一般的使用情況下,redux-form 通過 formValueSelector 提供了一個(gè)方便的選擇器。

警告: 需要節(jié)制使用這個(gè)機(jī)制,因?yàn)檫@樣的話,表單里的某一個(gè)值一旦發(fā)生改變,就會(huì)重新渲染您的組件。

代碼片段:

// Decorate with reduxForm(). It will read the initialValues prop provided by connect()
SelectingFormValuesForm = reduxForm({
  form: 'selectingFormValues',// a unique identifier for this form
})(SelectingFormValuesForm)

// Decorate with connect to read form values
const selector = formValueSelector('selectingFormValues') // <-- same as form name
SelectingFormValuesForm = connect(state => {
  // can select values individually
  const hasEmailValue = selector(state, 'hasEmail')
  const favoriteColorValue = selector(state, 'favoriteColor')
  // or together as a group
  const { firstName, lastName } = selector(state, 'firstName', 'lastName')
  return {
    hasEmailValue,
    favoriteColorValue,
    fullName: `${firstName || ''} ${lastName || ''}`
  }
})(SelectingFormValuesForm)

export default SelectingFormValuesForm

<h3 id="demofieldArray"> Demo: Field Array </h3>

這個(gè)例子展示了怎樣構(gòu)建一個(gè)字段組,包括擁有一個(gè)字段的和擁有一組字段的字段組。在這個(gè)表單里,每一個(gè)俱樂部的成員都有姓和名,還有一個(gè)興趣的列表。以下這些數(shù)組的操作 insert, pop, push, remove, shift, swap, unshift 行為是被允許的:(更多詳細(xì)的內(nèi)容可以參考FieldArray Docs)

  • 一個(gè) action 的原始構(gòu)造
  • 通過您表單的 this.props.array 對(duì)象綁定的 action
  • 同時(shí)綁定表單和通過 FieldArray 組件獲得的對(duì)象上的數(shù)組的 action

<h3 id="remoteSubmit"> Demo: Remote Submit </h3>

這個(gè)例子演示了一個(gè)表單如何從一個(gè)無關(guān)的組件或中間件中發(fā)送的一個(gè) SUBMIT 的action來執(zhí)行提交邏輯。

這個(gè)例子里你所看到的的提交按鈕,不是直接與表單組件直接鏈接的,它的作用只是通過 Redux 發(fā)送的一個(gè)提交的 action。

要注意它的工作方式,這個(gè)提交函數(shù)必須通過 reduxForm() 配置參數(shù)的傳遞或通過 prop 提供給表單組件。以下是發(fā)送這個(gè)action的方式:

import React from 'react'
import { connect } from 'react-redux'
import { submit } from 'redux-form'

const style = {
  padding: '10px 20px',
  width: 140,
  display: 'block',
  margin: '20px auto',
  fontSize: '16px'
}

const RemoteSubmitButton = ({ dispatch }) => (
  <button
    type="button"
    style={style}
    onClick={() => dispatch(submit('remoteSubmit'))}
  >
    Submit
  </button>
)
//   remoteSubmit 為表單的名字
export default connect()(RemoteSubmitButton)

<h3 id="normalizing"> Demo: Field Normalizing </h3>

當(dāng)您需要在用戶輸入和 store 中的數(shù)據(jù)之間施加某些控制,你可以使用 normalizer。normalizer 就是一個(gè)每當(dāng)值改變是,可以在保存到 store 之前進(jìn)行某些轉(zhuǎn)換的一個(gè)函數(shù)。

一個(gè)常用的例子:你需要一個(gè)某些經(jīng)過格式化的值,比如電話號(hào)碼或信用卡號(hào)。

Normalizers 傳遞了4個(gè)參數(shù):

  • value - 你設(shè)置了 normalizer 字段的值
  • previousValue - 這個(gè)值最近一次變化之前的一個(gè)值
  • allValues - 表單中,所有字段當(dāng)前的值
  • previousAllValues - 表單中,所有字段在最近一次變化前的值

這些可以使你基于表單中另外一個(gè)字段而限制某個(gè)特定的字段。比如例子中的字段最小最大值:這里你不能設(shè)置 min 中的值比 max 中的值大,不能設(shè)置 max 中的值比 min 的值更小(下面有代碼)

const upper = value => value && value.toUpperCase()
const lower = value => value && value.toLowerCase()
const lessThan = otherField => (value, previousValue, allValues) =>
  parseFloat(value) < parseFloat(allValues[otherField]) ? value : previousValue
const greaterThan = otherField => (value, previousValue, allValues) =>
  parseFloat(value) > parseFloat(allValues[otherField]) ? value : previousValue

下面是對(duì)電話號(hào)碼處理的邏輯

const normalizePhone = value => {
  if (!value) {
    return value
  }

  const onlyNums = value.replace(/[^\d]/g, '')
  if (onlyNums.length <= 3) {
    return onlyNums
  }
  if (onlyNums.length <= 7) {
    return `${onlyNums.slice(0, 3)}-${onlyNums.slice(3)}`
  }
  return `${onlyNums.slice(0, 3)}-${onlyNums.slice(3, 6)}-${onlyNums.slice(6, 10)}`
}

<h3 id="Wizard"> Demo: Wizard </h3>

一種常見的UI設(shè)計(jì)模式是把一個(gè)單一的表單分割成幾組分開的表單形式,最為熟知的就是 Wizard。使用 redux-form 的話有好多方式可以來做這種設(shè)計(jì),但最簡單和最推薦的方式是遵循一下幾種指示:

  • 把每一個(gè)頁面都用同一個(gè)表單名字連接到 reduxForm()
  • 指定 destroyOnUnmountfalse 就可以在表單組件卸載的時(shí)候保存表單數(shù)據(jù)
  • 你可以為整個(gè)表單指定一個(gè)同步驗(yàn)證函數(shù)
  • 使用 onSubmit 來觸發(fā)進(jìn)入下一步,因?yàn)樗鼜?qiáng)制運(yùn)行驗(yàn)證函數(shù)

需要由你自己來實(shí)現(xiàn)的:

  • 在提交成功之后手動(dòng)調(diào)用 props.destory()

例子里的代碼主要列出控制 Wizard 的組件,其他組件的用法已被我們熟知。

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import WizardFormFirstPage from './WizardFormFirstPage'
import WizardFormSecondPage from './WizardFormSecondPage'
import WizardFormThirdPage from './WizardFormThirdPage'

class WizardForm extends Component {
  constructor(props) {
    super(props)
    this.nextPage = this.nextPage.bind(this)
    this.previousPage = this.previousPage.bind(this)
    this.state = {
      page: 1
    }
  }
  nextPage() {
    this.setState({ page: this.state.page + 1 })
  }

  previousPage() {
    this.setState({ page: this.state.page - 1 })
  }

  render() {
    const { onSubmit } = this.props
    const { page } = this.state
    return (
      <div>
        {page === 1 && <WizardFormFirstPage onSubmit={this.nextPage} />}
        {page === 2 &&
          <WizardFormSecondPage
            previousPage={this.previousPage}
            onSubmit={this.nextPage}
          />}
        {page === 3 &&
          <WizardFormThirdPage
            previousPage={this.previousPage}
            onSubmit={onSubmit}
          />}
      </div>
    )
  }
}

WizardForm.propTypes = {
  onSubmit: PropTypes.func.isRequired
}

export default WizardForm
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容