UNPKG

redux-form

Version:

A higher order component decorator for forms using Redux and React

233 lines (217 loc) 9.41 kB
import * as importedActions from './actions'; import getDisplayName from './getDisplayName'; import {initialState} from './reducer'; import deepEqual from 'deep-equal'; import bindActionData from './bindActionData'; import getValues from './getValues'; import isValid from './isValid'; import readFields from './readFields'; import handleSubmit from './handleSubmit'; import asyncValidation from './asyncValidation'; import silenceEvents from './events/silenceEvents'; import silenceEvent from './events/silenceEvent'; import wrapMapDispatchToProps from './wrapMapDispatchToProps'; import wrapMapStateToProps from './wrapMapStateToProps'; /** * Creates a HOC that knows how to create redux-connected sub-components. */ const createHigherOrderComponent = (config, isReactNative, React, connect, WrappedComponent, mapStateToProps, mapDispatchToProps, mergeProps, options) => { const {Component, PropTypes} = React; return (reduxMountPoint, formName, formKey, getFormState) => { class ReduxForm extends Component { constructor(props) { super(props); // bind functions this.asyncValidate = this.asyncValidate.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.fields = readFields(props, {}, {}, this.asyncValidate, isReactNative); const {submitPassback} = this.props; submitPassback(() => this.handleSubmit()); // wrapped in function to disallow params } componentWillMount() { const {fields, form, initialize, initialValues} = this.props; if (initialValues && !form._initialized) { initialize(initialValues, fields); } } componentWillReceiveProps(nextProps) { if (!deepEqual(this.props.fields, nextProps.fields) || !deepEqual(this.props.form, nextProps.form, {strict: true})) { this.fields = readFields(nextProps, this.props, this.fields, this.asyncValidate, isReactNative); } if (!deepEqual(this.props.initialValues, nextProps.initialValues)) { this.props.initialize(nextProps.initialValues, nextProps.fields); } } componentWillUnmount() { if (config.destroyOnUnmount) { this.props.destroy(); } } asyncValidate(name, value) { const {asyncValidate, dispatch, fields, form, startAsyncValidation, stopAsyncValidation, validate} = this.props; const isSubmitting = !name; if (asyncValidate) { const values = getValues(fields, form); if (name) { values[name] = value; } const syncErrors = validate(values, this.props); const {allPristine} = this.fields._meta; const initialized = form._initialized; // if blur validating, only run async validate if sync validation passes // and submitting (not blur validation) or form is dirty or form was never initialized const syncValidationPasses = isSubmitting || isValid(syncErrors[name]); if (syncValidationPasses && (isSubmitting || !allPristine || !initialized)) { return asyncValidation(() => asyncValidate(values, dispatch, this.props), startAsyncValidation, stopAsyncValidation, name); } } } handleSubmit(submitOrEvent) { const {onSubmit, fields, form} = this.props; const check = submit => { if (!submit || typeof submit !== 'function') { throw new Error('You must either pass handleSubmit() an onSubmit function or pass onSubmit as a prop'); } return submit; }; return !submitOrEvent || silenceEvent(submitOrEvent) ? // submitOrEvent is an event: fire submit handleSubmit(check(onSubmit), getValues(fields, form), this.props, this.asyncValidate) : // submitOrEvent is the submit function: return deferred submit thunk silenceEvents(() => handleSubmit(check(submitOrEvent), getValues(fields, form), this.props, this.asyncValidate)); } render() { const allFields = this.fields; const {addArrayValue, asyncBlurFields, blur, change, destroy, focus, fields, form, initialValues, initialize, onSubmit, propNamespace, reset, removeArrayValue, returnRejectedSubmitPromise, startAsyncValidation, startSubmit, stopAsyncValidation, stopSubmit, submitFailed, swapArrayValues, touch, untouch, validate, ...passableProps} = this.props; // eslint-disable-line no-redeclare const {allPristine, allValid, errors, formError, values} = allFields._meta; const props = { // State: active: form._active, asyncValidating: form._asyncValidating, dirty: !allPristine, error: formError, errors, fields: allFields, formKey, invalid: !allValid, pristine: allPristine, submitting: form._submitting, submitFailed: form._submitFailed, valid: allValid, values, // Actions: asyncValidate: silenceEvents(() => this.asyncValidate()), // ^ doesn't just pass this.asyncValidate to disallow values passing destroyForm: silenceEvents(destroy), handleSubmit: this.handleSubmit, initializeForm: silenceEvents(initValues => initialize(initValues, fields)), resetForm: silenceEvents(reset), touch: silenceEvents((...touchFields) => touch(...touchFields)), touchAll: silenceEvents(() => touch(...fields)), untouch: silenceEvents((...untouchFields) => untouch(...untouchFields)), untouchAll: silenceEvents(() => untouch(...fields)) }; const passedProps = propNamespace ? {[propNamespace]: props} : props; return (<WrappedComponent {...{ ...passableProps, // contains dispatch ...passedProps }}/>); } } ReduxForm.displayName = `ReduxForm(${getDisplayName(WrappedComponent)})`; ReduxForm.WrappedComponent = WrappedComponent; ReduxForm.propTypes = { // props: asyncBlurFields: PropTypes.arrayOf(PropTypes.string), asyncValidate: PropTypes.func, dispatch: PropTypes.func.isRequired, fields: PropTypes.arrayOf(PropTypes.string).isRequired, form: PropTypes.object, initialValues: PropTypes.any, onSubmit: PropTypes.func, propNamespace: PropTypes.string, readonly: PropTypes.bool, returnRejectedSubmitPromise: PropTypes.bool, submitPassback: PropTypes.func.isRequired, validate: PropTypes.func, // actions: addArrayValue: PropTypes.func.isRequired, blur: PropTypes.func.isRequired, change: PropTypes.func.isRequired, destroy: PropTypes.func.isRequired, focus: PropTypes.func.isRequired, initialize: PropTypes.func.isRequired, removeArrayValue: PropTypes.func.isRequired, reset: PropTypes.func.isRequired, startAsyncValidation: PropTypes.func.isRequired, startSubmit: PropTypes.func.isRequired, stopAsyncValidation: PropTypes.func.isRequired, stopSubmit: PropTypes.func.isRequired, submitFailed: PropTypes.func.isRequired, swapArrayValues: PropTypes.func.isRequired, touch: PropTypes.func.isRequired, untouch: PropTypes.func.isRequired }; ReduxForm.defaultProps = { asyncBlurFields: [], form: initialState, readonly: false, returnRejectedSubmitPromise: false, validate: () => ({}) }; // bind touch flags to blur and change const unboundActions = { ...importedActions, blur: bindActionData(importedActions.blur, { touch: !!config.touchOnBlur }), change: bindActionData(importedActions.change, { touch: !!config.touchOnChange }) }; // make redux connector with or without form key const decorate = formKey !== undefined && formKey !== null ? connect( wrapMapStateToProps(mapStateToProps, state => { const formState = getFormState(state, reduxMountPoint); if (!formState) { throw new Error(`You need to mount the redux-form reducer at "${reduxMountPoint}"`); } return formState && formState[formName] && formState[formName][formKey]; }), wrapMapDispatchToProps(mapDispatchToProps, bindActionData(unboundActions, { form: formName, key: formKey })), mergeProps, options ) : connect( wrapMapStateToProps(mapStateToProps, state => { const formState = getFormState(state, reduxMountPoint); if (!formState) { throw new Error(`You need to mount the redux-form reducer at "${reduxMountPoint}"`); } return formState && formState[formName]; }), wrapMapDispatchToProps(mapDispatchToProps, bindActionData(unboundActions, {form: formName})), mergeProps, options ); return decorate(ReduxForm); }; }; export default createHigherOrderComponent;