import createAction from 'redux-actions/lib/createAction';
import { change as reduxFormChange } from 'redux-form';
import debugLib from 'debug';
import compositeInputFormatters from '../compositeInputFormatters';
import { COMPOSITE_FORMSECTION_NAME_PREFIX } from '../data/constants';
/**
* All functions in this module are action creators and the return value
* should be passed to the redux store dispatch() function
*
* @module enhanced-redux-form/actions/compositeInputActions
* @category forms
*/
const debug = debugLib('SlimmingWorld:compositeInputActions');
export const REGISTER_COMPOSITE_INPUT = 'compositeInputActions/REGISTER_COMPOSITE_INPUT';
export const UNREGISTER_COMPOSITE_INPUT = 'compositeInputActions/UNREGISTER_COMPOSITE_INPUT';
export const COMPOSITE_VALIDATION_ERRORS = 'compositeInputActions/COMPOSITE_VALIDATION_ERRORS';
export const COMPOSE_COMPOSITE_INPUT = 'compositeInputActions/COMPOSE_COMPOSITE_INPUT';
export const composeCompositeInput = createAction(
COMPOSE_COMPOSITE_INPUT,
(form, field, value) => ({ field, value }),
form => ({ form }),
);
/**
* Register a composite input mount
*
* @function registerCompositeInput
* @param {string} form The id of the redux-form that the input is rendered in
* @param {string} field The name to register the composite input under
* @param {Object} formatter One of the formatters to use as defined in compositeInputFormatters.js
* @param {string} composeOn A string indicating on which event the composite input should run
* its formatter, besides running before each validation round
* @returns {object} The action
*/
export const registerCompositeInput = createAction(
REGISTER_COMPOSITE_INPUT,
(form, field, { formatter, composeOn = null }) => ({
field,
formatter: formatter.name,
composeOn,
}),
form => ({ form }),
);
/**
* Register a composite input unmount
*
* @function unregisterCompositeInput
* @param {string} form The id of the redux-form that the input was rendered in
* @param {string} field The name of the composite input
* @returns {object} The action
*/
export const unregisterCompositeInput = createAction(
UNREGISTER_COMPOSITE_INPUT,
(form, field) => ({ field }),
form => ({ form }),
);
/**
* Uses the decompose function in the compositeInputFormatter configuration to
* decompose a single composite value back to values of the inputs and update
* them accordingly. If no decompose function is configured, will ignore
* this call.
*
* @param {string} form The id of the redux-form that the input was rendered in
* @param {string} input The name of the composite input
*/
export const decomposeCompositeInput = (form, input) => (dispatch, getState) => {
const state = getState();
const formValue = state.form?.[form].values;
const compositeInputState = state.enhancedForm?.compositeInput?.[form]?.[input] || {};
const newValue =
formValue && typeof formValue[input] !== 'undefined' ? formValue[input] : undefined;
// Need to not ignore 0 inputs
const composedValue =
typeof compositeInputState.composedValue !== 'undefined'
? compositeInputState.composedValue
: undefined;
if (composedValue === newValue) {
debug(
`decomposeCompositeInput() ignored change (${form}.${input} => ${newValue}) because it equals the last value composed by composeCompositeInputs()`,
);
return;
}
const { formatter } = compositeInputFormatters[compositeInputState.formatter] || {};
if (!(formatter && formatter.decompose)) {
debug('no decompose() config found on formatter. ignoring change');
return;
}
debug(
`decomposeCompositeInput() detected change (${form}.${input}: ${composedValue} => ${newValue}). `,
);
const formValues = formValue
? formValue[`${COMPOSITE_FORMSECTION_NAME_PREFIX}${input}`]
: undefined;
const currentValues = typeof formValues !== 'undefined' ? formValues : {};
let decomposedValues;
try {
decomposedValues = formatter.decompose(newValue, currentValues);
} catch (e) {
debug(
`Error while running decompose() in formatter "${
compositeInputState.formatter
}" for field "${input}" and value "${newValue}": ${e.message || e}`,
);
debug('skipping update.');
return;
}
if (typeof decomposedValues !== 'object') {
debug(
`Invalid return of decompose() in formatter "${
compositeInputState.formatter
}" for field "${input}" and value "${newValue}". Expected "object", got "${typeof decomposedValues}". skipping update.`,
);
return;
}
const compositeGroupName = `${COMPOSITE_FORMSECTION_NAME_PREFIX}${input}`;
Object.keys(decomposedValues).forEach(fieldName => {
const originalValue = state.form?.[form]?.values?.[compositeGroupName]?.[fieldName];
if (originalValue !== decomposedValues[fieldName]) {
dispatch(
reduxFormChange(form, `${compositeGroupName}.${fieldName}`, decomposedValues[fieldName]),
);
}
});
};
/**
* Uses the formatter passed to CompositeInput components to format the inner values
* of a CompositeInput to a composed value.
*
* @param {string} form The id of the redux-form that the input was rendered in
* @param {Array<string>} [inputs] An array of inputs to compose. If omitted, will compose
* all composite inputs on the form
*/
export const composeCompositeInputs = (form, inputs = null) => (dispatch, getState) => {
const state = getState();
const formState = state.form[form];
if (!formState || !formState.values) {
return;
}
const compositeInputs = state.enhancedForm.compositeInput[form] || {};
const compositionErrors = {};
const inputsToCompose = inputs || Object.keys(compositeInputs);
const fieldHasError = {};
inputsToCompose.forEach(fieldName => {
if (!compositeInputs[fieldName]) {
return;
}
const { formatter } = compositeInputFormatters[compositeInputs[fieldName].formatter];
const formSectionName = `${COMPOSITE_FORMSECTION_NAME_PREFIX}${fieldName}`;
fieldHasError[fieldName] = false;
try {
const childFieldValues = formState.values[formSectionName] || {};
const value = formatter.compose(childFieldValues);
if (formState.values[fieldName] !== value) {
const transformedValue = value ?? '';
dispatch(composeCompositeInput(form, fieldName, transformedValue));
dispatch(reduxFormChange(form, fieldName, transformedValue));
}
} catch (e) {
if (e.compositeError) {
compositionErrors[fieldName] = { message: e.compositeError };
fieldHasError[fieldName] = true;
} else if (e.invalidFields) {
Object.keys(e.invalidFields).forEach(subFieldName => {
compositionErrors[`${formSectionName}.${subFieldName}`] = {
message: e.invalidFields[subFieldName],
};
});
fieldHasError[fieldName] = true;
} else {
throw e;
}
}
});
if (Object.keys(fieldHasError).length) {
dispatch({
type: COMPOSITE_VALIDATION_ERRORS,
payload: {
errors: compositionErrors,
fieldHasError,
},
meta: { form },
});
}
};