/* global WP_DEFINE_DEVELOPMENT, WP_DEFINE_IS_NODE */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { stopSubmit as reduxFormStopSubmit } from 'redux-form';
import { handleSubmitErrors } from '../../enhanced-redux-form/actions/enhancedFormActions';
import {
registerWizardForm,
submitWizardForm,
destroyWizardForm,
} from '../actions/wizardFormActions';
import isSubmissionError from '../utils/isSubmissionError';
import {
createWizardStepsSelector,
createWizardStepPathsSelector,
} from '../reducers/wizardReducer';
/** @module enhanced-redux-form/enhancedFormWizard */
/**
* Adds multi-step form functionality to a component. For general usage instructions, please refer
* to {@tutorial enhanced-redux-form-wizard}.
* @function enhancedFormWizard
* @tutorial enhanced-redux-form-wizard
* @param {object} wizardFormConfig Configuration object for the wizard
* @param {string} wizardFormConfig.name A unique name for this wizard
* @param {function} wizardFormConfig.onSubmit A submit handler that will be called after the last
* form step has been submitted. It will receive the following parameters:
* * __dispatch__ The redux dispatch function
* * __values__ An object containing the submitted form values of all form steps
* @param {boolean} [wizardFormConfig.destroyOnUnmount] If false, will not destroy the wizard form
* state when this component is unmounted. Defaults to true
* @param {string} [wizardFormConfig.generalErrorMessage] When the API call comes back with an
* error that doesn't have the default error response shape, this message will be shown instead.
* @param {string} [wizardFormConfig.transformApiFieldNames] A function that maps each field
* name in an API validation response to a field name in this form. Can be used to map flattened
* field names in the API back to nested names in redux-form.
* @returns {function} A function that wraps a component in the wizard functionality
* @example export default enhancedFormWizard({
* name: 'onlineRegistration',
* onSubmit: (dispatch, values) => { ... },
* })(OnlineRegistration);
* @category forms
*/
const enhancedFormWizard = wizardFormConfig => WrappedComponent => {
if (WP_DEFINE_DEVELOPMENT) {
// validation of the wizardFormConfig object
if (typeof wizardFormConfig !== 'object') {
throw new TypeError(
`Unexpected argument type of "${typeof wizardFormConfig} passed to enhancedFormWizard"`,
);
}
if (typeof wizardFormConfig.name === 'undefined') {
throw new ReferenceError(
'Expected enhancedFormWizard configuration object to contain a "name" property',
);
}
}
const { routingAdapter } = wizardFormConfig;
if (!routingAdapter) {
throw new ReferenceError('A routingAdapter should be passed to enhancedFormWizard config');
}
class EnhancedFormWizard extends Component {
constructor(props) {
super(props);
const steps = props.dispatch(routingAdapter.initialize(props));
props.registerWizardForm(steps);
}
getChildContext() {
return {
enhancedFormWizard: {
name: wizardFormConfig.name,
routingAdapter,
},
};
}
componentDidMount() {
this.redirectToFirstStep(this.props);
}
componentDidUpdate(prevProps) {
// changing the 'submitting' state to true indicates to this component it should submit
if (!prevProps.submitting && this.props.submitting) {
const onSubmit = prevProps.onSubmit || wizardFormConfig.onSubmit || (() => {});
prevProps.submitWizardForm(onSubmit).catch(error => {
// swallow submission errors. they are pushed to the redux state
if (!isSubmissionError(error)) {
prevProps.stopSubmit(wizardFormConfig.name);
prevProps.registerError(wizardFormConfig.name, error.response.parsed.error);
throw error;
}
});
}
this.redirectToFirstStep(this.props);
}
componentWillUnmount() {
if (
typeof wizardFormConfig.destroyOnUnmount === 'undefined' ||
wizardFormConfig.destroyOnUnmount
) {
this.props.destroyWizardForm();
}
}
redirectToFirstStepRAF = null;
redirectToFirstStep(props) {
if (this.redirectToFirstStepRAF !== null) {
cancelAnimationFrame(this.redirectToFirstStepRAF);
}
this.redirectToFirstStepRAF = requestAnimationFrame(() => {
this.redirectToFirstStepRAF = null;
props.dispatch(routingAdapter.redirectToFirstStep(props));
});
}
render() {
return !WP_DEFINE_IS_NODE ? <WrappedComponent {...this.props} /> : null;
}
}
EnhancedFormWizard.displayName = `enhancedFormWizard(${WrappedComponent.displayName ||
WrappedComponent.name ||
'Component'})`;
EnhancedFormWizard.propTypes = {
/* eslint-disable react/no-unused-prop-types */
/*
* Note: normally we don't use dispatch directly inside our component. This is an exception
* because we need to dispatch dynamic actions from the routing adapter.
*/
dispatch: PropTypes.func.isRequired,
registerWizardForm: PropTypes.func.isRequired,
/* eslint-enable react/no-unused-prop-types */
destroyWizardForm: PropTypes.func.isRequired,
// eslint-disable-next-line react/no-unused-prop-types
submitWizardForm: PropTypes.func.isRequired,
/* eslint-disable react/no-unused-prop-types */
onSubmit: PropTypes.func,
/* eslint-disable react/no-unused-prop-types */
stepPaths: PropTypes.arrayOf(PropTypes.string).isRequired,
lastMountedStepIndex: PropTypes.number.isRequired,
/* eslint-enable react/no-unused-prop-types */
submitting: PropTypes.bool.isRequired,
/* eslint-disable react/no-unused-prop-types */
stopSubmit: PropTypes.func,
/* eslint-disable react/no-unused-prop-types */
registerError: PropTypes.func,
wizardForm: PropTypes.object,
};
EnhancedFormWizard.childContextTypes = {
enhancedFormWizard: PropTypes.shape({
name: PropTypes.string.isRequired,
routingAdapter: PropTypes.object.isRequired,
}).isRequired,
};
const stepsSelector = createWizardStepsSelector(wizardFormConfig.name, true);
const stepPathsSelector = createWizardStepPathsSelector(wizardFormConfig.name, true);
return connect(
state => {
if (state.enhancedForm.wizard[wizardFormConfig.name]) {
const wizard = state.enhancedForm.wizard[wizardFormConfig.name];
const steps = stepsSelector(state.enhancedForm.wizard);
// look up the visible step index that corresponds with the last mounted one
let visibleStepIndex = -1;
let findStepIndex = wizard.lastMountedStepIndex;
const findStepIndexFunc = step => step.stepIndex === findStepIndex;
// try to find the step, else try the previous one
while (visibleStepIndex === -1 && findStepIndex !== -1) {
visibleStepIndex = steps.findIndex(findStepIndexFunc);
--findStepIndex; // eslint-disable-line no-plusplus
}
return {
submitting: wizard.submitting,
lastMountedStepIndex: wizard.lastMountedStepIndex,
// when hiding some steps for progress, this will find the correct index for non-hidden
// steps, so we can correctly use this in the stepIndicator
lastMountedVisibleStepIndex: visibleStepIndex,
stepPaths: stepPathsSelector(state.enhancedForm.wizard),
wizardForm: wizard,
};
}
return {
submitting: false,
lastMountedStepIndex: -1,
stepPaths: [],
};
},
dispatch => ({
registerWizardForm: steps => dispatch(registerWizardForm(wizardFormConfig.name, steps)),
submitWizardForm: onSubmit =>
dispatch(
submitWizardForm(
wizardFormConfig.name,
onSubmit,
wizardFormConfig.transformApiFieldNames,
wizardFormConfig.generalErrorMessage,
routingAdapter,
),
),
destroyWizardForm: () => dispatch(destroyWizardForm(wizardFormConfig.name)),
registerError: (formName, error) => dispatch(handleSubmitErrors(formName, error)),
stopSubmit: formName => dispatch(reduxFormStopSubmit(formName, { _error: 'failed' })),
dispatch,
}),
)(EnhancedFormWizard);
};
export default enhancedFormWizard;