React-Redux技术栈——之redux-form详解

React中没有相似Angular那样的双向数据绑定,在作一些表单复杂的后台类页面时,监听、赋值、传递、校验时编码相对复杂,满屏的样板代码伤痛欲绝,故引入能够解决这些问题的 redux-form (v6) 模块。本文大体翻译了官方文档一些比较重要的地方,结合官方Demo加入了一些特性,有些官方跑不起来的地方也进行了优化。javascript

起步

在使用 redux-form 以前,须要具有如下基础:

关于 redux-form 的三个主要模块:

  • formReducer reducer : 表单的各类操做以 Redux action 的方式,经过此 reducer 来促使 Redux store 数据的变化。npm

  • reduxForm() HOC : 此高阶组件用以整合 Redux action 绑定的用户交互与您的组件,并返回一个新的组件供以使用。json

  • <Field/> : 用此代替您本来的 <input/> 组件,能够与redux-form的逻辑相链接。redux

数据流:

在大部分状况下您不须要关心如何建立action,一切都是自动的。下图展现了一个简易的数据流:

Data flow

举个简单的例子,咱们有一个被 reduxForm() 建立的表单组件,里面有一个用 <Field/> 建立的 <input/> 组件,数据流大概是这个样子的:

  1. 用户点击这个 <input/> 组件,

  2. "Focus action" 被触发,

  3. formReducer 更新了对应的状态,

  4. 这个状态被传回 <input/> 组件中。

与此相似的在这个 <input/> 中输入文字、更改状态、提交表单,也是遵循以上这个流程。

redux-form 还能基于此流程处理许多事情,诸如:表单验证与格式化,多参数与action的建立。基于如下的向导,请自助挖掘更深层次的功能。

基本使用向导

步骤 1/4: Form reducer

store须要知道组件如何发送action,所以咱们须要在您的store中注册 formReducer,他能够服务于整个app中你定义的全部表单组件,所以只须要注册一次。

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,请移步 getFormState config查看详情。

步骤 2/4: Form component

为了使您的表单组件能够与store进行交互,咱们须要使用高价函数 reduxForm() 来包裹您的组件。他能够在您执行提交表单等操做的时候,以props的方式提供表单内的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;

如今咱们已经有一个表单组件了,让咱们添加一些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类型组件的数据到store中,基本用法以下:

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

它建立了一个text类型的<input/>组件,还提供了诸如 value onChange onBlur等属性,用于跟踪和维护此组件的各类状态。

注: <Field/> 组件很强大,除了基本的类型,还能够配置类或者无状态组件,欲了解更多,请移步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;

从如今开始,表单上的操做数据已经能够填充至store,并能够执行提交表单操做了。

步骤 4/4: Reacting to submit

提交的数据以JSON对象的形式注入了此表单组件的 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} />
    )
  }
}

表单value的生命周期

本节对理解您的组件value经过 redux-form 的流向很重要

Value 生命周期钩子函数

redux-form 提供了3个 value 生命周期钩子函数,经过props传递给Field组件,而且都是可选的。

format(value:Any) => String

格式化从store里拿出来的数据渲染到组件里,一般会在store保留原来的数据类型,只是在组件中使用的时候进行格式化。

parse(value:String) => Any

把用户输入的string类型的数据进行格式转化,放入store供你使用,也会在store保留转化后类型的数据。

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

容许您对当前字段数据添加某些约束的逻辑,好比能够约束 midDate 的日期在 maxDate 以前等。若是你添加了这些逻辑,经过 normalize()的value将会被解析。

Value 生命周期

value lifecycle

API

限于篇幅问题,在此只列举每一种api经常使用的使用方法,具体请移步官方API文档

reduxForm(config:Object)

经过配置一些参数建立一个可让你配置你的表单的修饰器。诸如配置如何作表单验证、提交成功或失败的回调、获取或失去焦点的action发送、prop命名空间等,具体例子会在以后的demo中介绍。

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

import { reduxForm } from 'redux-form';  // ES6
经常使用参数介绍

必要参数

  • form : String[required] : 用于命名您的表单,在store生成此命名的数据节点。

可选参数

  • onChange : Function [optional] : 表单触发 onChange 事件后的回调。

  • onSubmit : Function [optional[ : 表单提交配置,能够配置须要提交哪些参数,还有提交时触发的 dispatch等。

  • onSubmitSuccess : Function [optional] & onSubmitFail : Function [optional] : 提交表单成功和失败的回调。

  • shouldValidate(params) : boolean [optional] : 同步验证。

  • shouldAsyncValidate(params) : boolean [optional] : 异步验证。

  • touchOnBlur : boolean [optional] & touchOnChange : boolean [optional] : 标识 onBluronChange 的触发。

props

列出所有当前页面由 redux-form 生成用于修饰此表单组件的props。

若是你但愿用严格模式来编写 PropTypes, redux-form 会导出此处全部的 propTypes,你须要引用他们并能够添加本身的propTypes,像这样:

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

class SimpleForm extends Component {
  static propTypes = {
    ...propTypes,
    // other props you might be using
  }
  // ...
}
经常使用属性
  • pristine : true 表示表单数据为原始数据没被修改过,反之为 dirty

  • submitting : 用于表示您的表单提交状态,他只会在您的表单提交后返回一个 promise 对象时起做用。 false 表示 promise 对象为 resolvedrejected 状态。

  • handleSubmit(eventOrSubmit) : Function : 提交表单的函数,若是表单须要验证,验证方法会被执行(包括同步和异步)。调用方法有两种:

    • 组件内部直接调用 <form onSubmit={handleSubmit}>

    • 赋值给prop外部调用 <MyDecoratedForm onSubmit={data => {//do something with data.}}/>

Field

全部您须要与 store 数据链接的表单组件,均可以用 <Field/>。在正确使用它以前,有三条基本概念您须要了解清楚:

  1. 必须包含 name 属性。能够是简单的字符串,如 userNamepassword,也能够是复杂的结构,如 contact.billing.address[2].phones[1].areaCode

  2. 必须包含 component 属性。能够是一个组件、无状态组件或者DOM所支持的默认的标签(input、textarea、select)。

  3. 其余全部属性会经过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.无状态组件

这是一个很是灵活的使用 <Field/> 的方法,使用方法和 redux-form 的前一个版本很类似。但必须在你的 render() 方法外定义它,不然它每次渲染都会被重建,而且因为组件的 prop 会变,就会强制 <Field/> 进行渲染。若是你在 render() 内部定义无状态组件,不但会拖慢你的app,并且组件的input每次都会在组件从新渲染的时候失去焦点。

// 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

好比建立一个文字输入框组件

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

Fields

Field 类似,可是它同时使用多个fields。<Fields/>name 属性中使用一组表单name的数组,而不是用单一一个 name 属性来表示。

重要: 请节制使用 <Fields/>,其内部任何表单组件数据变化时,都会从新渲染整个 <Fields/>。所以会成为您app的性能瓶颈。除非你真的须要这么作,最好仍是用 <Field/> 来一个个自定义您的表单组件

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

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

<Field/> 差很少,有2种使用方式,组件与无状态组件,这里不详细介绍。

FieldArray

这个组件可让你定义一系列的表单,它的工做原理和 <Field/> 同样。经过 <Field/>,给它一个 name,就能够映射到 Redux state中的指定位置。组件也能够经过链接到 Redux stateprops 进行渲染。

经过 <FieldArray/> ,你也须要和 <Field/> 同样给它一个 name。而你注入 <FieldArray/> 的组件会经过字段数组收到一系列的 props,用以查询、更新和迭代。

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

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

后面Demo里会具体介绍

Form

Form 组件对React的form组件进行了简单的封装,用以触发用 redux-form 修饰的组件的 onSubmit 函数。

您能够在如下场景中使用它:

  • 在您表单组件内部,能够经过 onSubmit={this.props.handleSubmit(this.mySubmitFunction)} 执行您的提交。

  • 或者

若是您只是将 onSubmit 函数做为你的配置或属性,那么你不须要用到这个组件。

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

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

只须要将您组件中全部 <form> 替换成 <Form> 便可。

FormSection

FormSection 能够很简单地将现有的表单组件分割成更小的组件,用以在复杂的表单中进行复用。它是经过明确规定好的 FieldFieldsFieldArray字组件 name的前缀来完成此功能的。

使用方法

这个例子所描述的业务是一个购买人与收件人视角的订单用户信息表单结构。购买人与收件人拥有相同的字段结构,所以把这个部分拆分红一个名为 Party 的组件是有意义的。假设如今 Party 包含 givenName middleName surname address 这几个字段,而后将 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 的形式,结果结构以下:

{
    "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,须要设置一个默认的 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"
}
*/

formValues()

做为一个修饰,能够读取当前表单的 value。当表单子组件的 onChange 依赖于当前表单里的值,颇有用。

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)

这些装饰组件如今分别拥有了 withVatshowVatprops

formValueSelector()

formValueSelector 的API能够很方便的 connect() state的值到表单的 value 里。它能够经过表单的 name 为你的表单建立一个 value 拾取器。

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

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

首先须要按照你表单的 name 建立一个 selector

const selector = formValueSelector('myFormName')

而后有几种方法使用 selector:

1.拾取个别的字段

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

2.在分好组的 prop 中按组的方式拾取多个字段

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

3.把 selector 看成 mapStateToProps 来使用

若是你不须要 state 中其余的属性值,selector做为mapStateToProps能够自动完成这个工做。

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

reducer

表单的reducer用来安装您的 Redux state 到您的表单中。

若是您使用 Immutablejs 来管理您的 Redux state,你必须这么从 redux-form/immutable 中导入 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);

reducer.plugin

表单中返回一个经过附加指定功能 reducers 用以接受 actionreducer。 它的参数应该是一个能映射 formName和一个(state, action) => nextState reducer 关系的一个对象。经过每个 reducer的state只能是属于那个表单的一个片断。

说明

flux 体系中最美的一部分应该是全部 reducers(或者 Flux中的标准术语 stores)能够接受全部 actions,他们能够修改基于这些 action来修改数据。举个例子,你有一个登陆的表单,当你提交失败的时候,你想清楚密码输入框内的数据,哪怕你的登陆的提交信息是属于另外一个 reducer/actions体系,你的表单依然能够作出本身的响应。

而不是使用 redux-form 中一个普通的 reducer,你能够经过调用 plugin() 函数来增强你的 reducer

注:这是一个增强功能的操做用来修改你内部的 redux-form state的片断,若是你不当心使用,会把事情搞砸。

例子

下面这个例子的做用是,当 AUTH_LOGIN_FAILaction 被分发时,能够清除登陆表单里的密码输入框:

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)

SubmissionError

这个 throwable error 用于从 onSubmit 返回一个表单验证错误信息。目的是用来区分 promise 失败的缘由到底是验证错误、AJAX I/O错误仍是其余服务器错误。若是它是因为表单里 { field1: 'error', field2: 'error' }产生的错误,那这个错误将会被添加到每个标记过错误属性的字段里,就像异步表单验证错误同样。若是有一个错误没有指定的字段,可是应用到了整个表单,你须要继续传递它,就好像是某个字段调用的 _error同样,而后他会给出一个错误的属性。(就是无论他往外抛)

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
      }
    })
}/>

Action Creators

redux-form 对外开放了全部的内部 action creators,容许你按找你的意愿来完成对分发 action 的控制。进而,官方推荐您在完成您大部分需求的时候,对于那些表单里指定需求的字段的 action来讲,看成这些 action 已经绑定到 dispatch同样,直接将这些 action 经过 props 传递。

具体 action 请参考官方文档。

Selectors

redux-form 提供了一系列有用的 Redux state 拾取器,能够在app的任何地方任何表单内拾取 state 上的数据。

下列全部拾取器拥有统一的使用方法: 他们都(除了getFormNames)使用表单的名字,来建立一个拾取器,不管表单的 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)

Examples

Simple Form

这个例子把表单全部基本的元素都列了出来,和官方Demo有所区别的是,增长了2个 typefileField (直接在 Field 中使用 file 的类型会有点问题),一个是使用了jQuery的 dropify 编写的上传单个文件的组件 MyDropify,一个是使用了 dropzone 编写的上传多个文件的组件 MyDropzone (在这里使用了 react-dropzoneredux-form 的组合)。官方的例子不单独介绍了,主要贴一下两个自定义 Field

注:因为reducer设计之初是纯函数,而提交文件的表单最后取得的值是一个 file 对象,当您使用了 redux-immutable-state-invariant 之类的检测工具,对其中诸如 lastModifiedDate 的值会报错,具体请看。在此,咱们暂时先不考虑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 的具体用法请参考其官方文档。

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版本的有所区别,使用过 dropzone 的应该都知道选择文件能够渲染到框体内,react版本的 dropzone 原声不带这个功能,但它提供了详尽的方法能够本身实现不少功能,好比选择完文件能够渲染到组件中,有时间我再完善此功能。

Sync Validation

同步的表单验证,包括了错误和警告型配置。官方Demo中只演示了输入框的验证,而这里准备了包括 radio select textarea 的验证方式(checkbox 我会在单独的一章讲解),调用方法能够参见本文的源代码。

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;

Field-Level Validation

除了提供一个验证方法一块儿验证表单里的值这种方法以外,还能够对每个 <Field/><FieldArray/> 分别作验证。官方给的Demo已经足够说明问题了,在这里只针对上面的 Sync Validation 做简单的改写。具体请看代码。

Submit Validation

一种服务器表单验证较好的方法是在调用 onSubnit 以后返回一个 rejectedpromise 对象。当您的表单被提交时,有2种方法提供给 redux-form 这个函数。

  1. 把他看成一个 onSubmitprop 传递给您的装饰组件。那样的话,你能够在您的装饰组件中使用 onSubmit={this.props.handleSubmit} 确保当用户点击提交按钮的时候触发这个函数。

  2. 把他看成一个参数传递给您装饰组件内的 this.props.handleSubmit 函数。这种状况下,你须要使用 onClick={this.props.handleSubmit(mySubmit)} 来确保当用户点击提交按钮的时候触发这个函数。

这个错误信息的显示方式和同步验证(Synchronous Validation)后的错误信息同样,但他是经过 onSubmit 函数返回一个封装过的 SubmissionError 对象。这个验证错误就像HTTP的400或500错误同样,和I/O错误是有区别的,而且他还会是这个提交的 promise 对象的状态置为 rejected

DEMO中没什么花头,和官方同样,就是基于 SyncValidation 把表单验证的逻辑放在了提交后的逻辑中,并抛出了一个 SubmissionError

Async Validation

服务器表单验证的方式比较推荐使用Submit Validation,可是可能存在当您填写表单的时候,同时须要服务器端来验证。有一个经典的例子是当一个用户选取一个值,好比用户名,它必须是您系统中惟一的一个值。

为了写一个异步的表单验证,须要给 redux-form 提供一个异步验证的函数(asyncValidation)用来提供一个能够从表单获取数据的一个对象,而后 Redux 分发这个函数,返回一个状态为拥有一个错误对象的 rejects或状态为 reslovepromise 对象。

您须要同时指定某几个字段,经过 asyncBlurFields 的属性配置,来标记是否须要在他们失去焦点的时候触发这个异步验证。

重要
  1. 异步验证会在 onSubmit 以前被调用,因此若是你关心的是 onSubmit 验证,你须要使用 Submit Validation

  2. 当一个字段的同步验证错误时,那它的失去焦点的时候将不会触发异步验证。

Demo中的自定义 <Field/>meta 中有一个 asyncValidating,来标识异步验证的 promise 对象的 Pending 状态。

Initialize From State

经过 initialValues 属性或 reduxForm() 配置的参数所提供的数据,被加载到表单 state 中,而且把这些初始化数据做为原始数据(pristine)。当 reset() 触发的时候,也会返回这些值。除了保存这些 pristine 值,初始化您表单的这个操做也会替换表单里已经存在的值。

在许多应用中,这些值多是来自服务器而且储存在其余 reducer 中的。想要获得这些值,你须要使用 connect() 去本身连接 state 而后映射这些数据到您的 initialValues 属性里。

默认状况下,你只须要经过 initialValues 初始化您的表单组件一次便可。目前有2种方法能够经过新的 pristine 值从新初始化表单。

  1. 传递一个 enableReinitialize 属性或配置 reduxForm() 中的参数为true就可让表单在每次 initialValues 属性变化的时候从新初始化,生成一个新的 pristine 值。若是想要在从新初始化的时候保持已改变过的表单的值,能够设置 keepDirtyOnReinitialize 为true。默认状况下,从新初始化会将 pristine 值替换掉已改变过的表单的值。

  2. 发出一个 INITIALIZE action(用 redux-form action生成器生成)。

此Demo较之官方Demo,增长了 enableReinitializekeepDirtyOnReinitialize 的用法。如下是代码片断。

InitializeFromStateForm = reduxForm({
  form: 'initializeFromState',// a unique identifier for this form
  enableReinitialize:true,
  keepDirtyOnReinitialize:true,// 这个值表示从新初始化表单后,不替换已更改的值,能够用clear来测试
})(InitializeFromStateForm)

Selecting Form Values

有时候您但愿访问表单组件中某些字段的值,你须要在 store 中直接 connect() 表单的值。在通常的使用状况下,redux-form 经过 formValueSelector 提供了一个方便的选择器。

警告: 须要节制使用这个机制,由于这样的话,表单里的某一个值一旦发生改变,就会从新渲染您的组件。

代码片断:

// 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

Field Array

这个例子展现了怎样构建一个字段组,包括拥有一个字段的和拥有一组字段的字段组。在这个表单里,每个俱乐部的成员都有姓和名,还有一个兴趣的列表。如下这些数组的操做 insert, pop, push, remove, shift, swap, unshift 行为是被容许的:(更多详细的内容能够参考FieldArray Docs)

  • 一个 action 的原始构造

  • 经过您表单的 this.props.array 对象绑定的 action

  • 同时绑定表单和经过 FieldArray 组件得到的对象上的数组的 action

Remote Submit

这个例子演示了一个表单如何从一个无关的组件或中间件中发送的一个 SUBMIT 的action来执行提交逻辑。

这个例子里你所看到的的提交按钮,不是直接与表单组件直接连接的,它的做用只是经过 Redux 发送的一个提交的 action

要注意它的工做方式,这个提交函数必须经过 reduxForm() 配置参数的传递或经过 prop 提供给表单组件。如下是发送这个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)

Field Normalizing

当您须要在用户输入和 store 中的数据之间施加某些控制,你可使用 normalizernormalizer 就是一个每当值改变是,能够在保存到 store 以前进行某些转换的一个函数。

一个经常使用的例子:你须要一个某些通过格式化的值,好比电话号码或信用卡号。

Normalizers 传递了4个参数:

  • value - 你设置了 normalizer 字段的值

  • previousValue - 这个值最近一次变化以前的一个值

  • allValues - 表单中,全部字段当前的值

  • previousAllValues - 表单中,全部字段在最近一次变化前的值

这些可使你基于表单中另一个字段而限制某个特定的字段。好比例子中的字段最小最大值:这里你不能设置 min 中的值比 max 中的值大,不能设置 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

下面是对电话号码处理的逻辑

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)}`
}

Wizard

一种常见的UI设计模式是把一个单一的表单分割成几组分开的表单形式,最为熟知的就是 Wizard。使用 redux-form 的话有好多方式能够来作这种设计,但最简单和最推荐的方式是遵循一下几种指示:

  • 把每个页面都用同一个表单名字链接到 reduxForm()

  • 指定 destroyOnUnmountfalse 就能够在表单组件卸载的时候保存表单数据

  • 你能够为整个表单指定一个同步验证函数

  • 使用 onSubmit 来触发进入下一步,由于它强制运行验证函数

须要由你本身来实现的:

  • 在提交成功以后手动调用 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
相关文章
相关标签/搜索