UNPKG

@ngbrown/react-redux-form

Version:

Create Forms Easily with React and Redux

413 lines (341 loc) 11.5 kB
import actionTypes from '../action-types'; import updateField, { getFieldAndForm } from '../utils/update-field'; import updateParentForms from '../utils/update-parent-forms'; import updateSubFields from '../utils/update-sub-fields'; import getFieldForm from '../utils/get-field-form'; import isPristine from '../form/is-pristine'; import map from '../utils/map'; import isPlainObject from '../utils/is-plain-object'; import mapValues from '../utils/map-values'; import inverse from '../utils/inverse'; import mergeValidity from '../utils/merge-validity'; import isValid, { fieldsValid } from '../form/is-valid'; import isValidityValid from '../utils/is-validity-valid'; import isValidityInvalid from '../utils/is-validity-invalid'; import fieldActions from '../actions/field-actions'; import toPath from '../utils/to-path'; import initialFieldState from '../constants/initial-field-state'; import { getMeta, fieldOrForm, updateFieldState } from '../utils/create-field'; import assocIn from '../utils/assoc-in'; import getFormValue from '../utils/get-form-value'; const resetFieldState = (field, customInitialFieldState) => { if (!isPlainObject(field)) return field; const intents = [{ type: 'reset' }]; let resetValue = getMeta(field, 'initialValue'); const loadedValue = getMeta(field, 'loadedValue'); if (loadedValue && (resetValue !== loadedValue)) { intents.push({ type: 'load' }); resetValue = loadedValue; } return fieldOrForm( getMeta(field, 'model'), resetValue, { ...customInitialFieldState, intents } ); }; const setInitialFieldState = (customInitialFieldState) => field => { if (!isPlainObject(field)) return field; if (field.$form) { // eslint-disable-next-line arrow-body-style return mapValues(field, (fieldState, key) => { return key === '$form' ? updateFieldState(customInitialFieldState, { value: field.value, model: field.model, }) : resetFieldState(fieldState, customInitialFieldState); }); } return updateFieldState(customInitialFieldState, { value: field.value, model: field.model, }); }; const addIntent = (intents, newIntent) => { if (!intents) return [newIntent]; if (intents.some(intent => intent.type === newIntent.type)) return intents; return intents.concat(newIntent); }; const clearIntents = (intents, oldIntent) => { if (!intents || typeof oldIntent === 'undefined') return []; return intents.filter(intent => intent.type !== oldIntent.type); }; const defaultOptions = { initialFieldState, }; export function createFormActionsReducer(options) { const formOptions = options ? { ...defaultOptions, ...options, initialFieldState: { ...defaultOptions.initialFieldState, ...options.initialFieldState, }, } : defaultOptions; const customInitialFieldState = formOptions.initialFieldState; return function formActionsReducer(state, action, localPath) { const [field] = getFieldAndForm(state, localPath); const fieldState = field && field.$form ? field.$form : field; const { intents } = fieldState; let fieldUpdates = {}; let subFieldUpdates = {}; let parentFormUpdates; switch (action.type) { case actionTypes.FOCUS: { fieldUpdates = { focus: true, intents: action.silent ? intents : addIntent(intents, action), }; break; } case actionTypes.BLUR: case actionTypes.SET_TOUCHED: { const fieldForm = getFieldForm(state, localPath).$form; fieldUpdates = { focus: action.type === actionTypes.BLUR ? false : field.focus, touched: true, retouched: fieldForm ? !!(fieldForm.submitted || fieldForm.submitFailed) : false, }; parentFormUpdates = { touched: true, retouched: fieldUpdates.retouched, }; break; } case actionTypes.SET_UNTOUCHED: { fieldUpdates = { focus: false, touched: false, }; break; } case actionTypes.SET_PRISTINE: case actionTypes.SET_DIRTY: { const pristine = action.type === actionTypes.SET_PRISTINE; fieldUpdates = { pristine, }; subFieldUpdates = { pristine, }; parentFormUpdates = (form) => ({ pristine: isPristine(form) }); break; } case actionTypes.SET_VALIDATING: { fieldUpdates = { validating: action.validating, validated: !action.validating, }; break; } case actionTypes.SET_VALIDITY: case actionTypes.SET_ERRORS: { const isErrors = action.type === actionTypes.SET_ERRORS; let validity; if (isErrors) { validity = action.merge ? mergeValidity(fieldState.errors, action.errors) : action.errors; } else { validity = action.merge ? mergeValidity(fieldState.validity, action.validity) : action.validity; } const inverseValidity = isPlainObject(validity) ? mapValues(validity, inverse) : !validity; // If the field is a form, its validity is // also based on whether its fields are all valid. const areFieldsValid = (field && field.$form) ? fieldsValid(field, { async: false }) : true; fieldUpdates = { [isErrors ? 'errors' : 'validity']: validity, [isErrors ? 'validity' : 'errors']: inverseValidity, validating: false, validated: true, valid: areFieldsValid && (isErrors ? !isValidityInvalid(validity) : isValidityValid(validity)), }; if (action.async) { const actionValidity = isErrors ? action.errors : action.validity; fieldUpdates.asyncKeys = (isPlainObject(actionValidity) || Array.isArray(actionValidity)) ? Object.keys(actionValidity) : true; } parentFormUpdates = (form) => ({ valid: isValid(form) }); break; } case actionTypes.SET_FIELDS_VALIDITY: { return map(action.fieldsValidity, (fieldValidity, subField) => fieldActions.setValidity(subField, fieldValidity, action.options) ).reduce((accState, subAction) => formActionsReducer( accState, subAction, localPath.concat(toPath(subAction.model))), state); } case actionTypes.RESET_VALIDITY: { let validity = { ...fieldState.validity }; let errors; let valid; if (action.omitKeys && typeof fieldState.errors !== 'string') { errors = { ...fieldState.errors }; action.omitKeys.forEach((key) => { delete validity[key]; delete errors[key]; }); valid = isValidityValid(validity); } else { validity = customInitialFieldState.validity; errors = customInitialFieldState.errors; valid = customInitialFieldState.valid; } fieldUpdates = { valid, validity, errors, }; subFieldUpdates = { valid: customInitialFieldState.valid, validity: customInitialFieldState.validity, errors: customInitialFieldState.errors, }; break; } case actionTypes.SET_PENDING: { fieldUpdates = { pending: action.pending, submitted: false, submitFailed: false, retouched: false, }; subFieldUpdates = { pending: action.pending, submitted: false, submitFailed: false, retouched: false, }; parentFormUpdates = { pending: action.pending }; break; } case actionTypes.SET_SUBMITTED: { const submitted = !!action.submitted; fieldUpdates = { pending: false, submitted, submitFailed: submitted ? false : fieldState && fieldState.submitFailed, touched: true, retouched: false, }; subFieldUpdates = { pending: false, submitted, submitFailed: submitted ? false : fieldUpdates.submitFailed, retouched: false, }; break; } case actionTypes.SET_SUBMIT_FAILED: { fieldUpdates = { pending: false, submitted: fieldState.submitted && !action.submitFailed, submitFailed: !!action.submitFailed, touched: true, retouched: false, }; subFieldUpdates = { pending: false, submitted: !action.submitFailed, submitFailed: !!action.submitFailed, touched: true, retouched: false, }; break; } case actionTypes.RESET: { return localPath.length ? assocIn(state, localPath, resetFieldState(field, customInitialFieldState)) : resetFieldState(field, customInitialFieldState); } case actionTypes.SET_INITIAL: { const setCustomInitialFieldState = setInitialFieldState(customInitialFieldState); return updateField( state, localPath, setCustomInitialFieldState, setCustomInitialFieldState); } case actionTypes.ADD_INTENT: { fieldUpdates = { intents: addIntent(intents, action.intent), }; break; } case actionTypes.CLEAR_INTENTS: { fieldUpdates = { intents: clearIntents(intents, action.intent), }; break; } case actionTypes.CHANGE: { return updateParentForms(state, localPath, (parentForm) => { const formModelValue = getFormValue(parentForm); if (!parentForm.$form) { return { ...customInitialFieldState, value: formModelValue, initialValue: formModelValue, }; } // If the form is invalid (due to async validity) // but its fields are valid and the value has changed, // the form should be "valid" again. if ((!parentForm.$form.validity || typeof parentForm.$form.validity === 'boolean' || !Object.keys(parentForm.$form.validity).length) && !parentForm.$form.valid && isValid(parentForm, { async: false })) { return { value: formModelValue, validity: true, errors: false, valid: true, }; } return { value: formModelValue, }; }); } default: return state; } if (action.clearIntents) { fieldUpdates.intents = clearIntents(intents, action.clearIntents); } const updatedField = updateField(state, localPath, fieldUpdates); const updatedSubFields = Object.keys(subFieldUpdates).length ? updateSubFields(updatedField, localPath, subFieldUpdates) : updatedField; const updatedParentForms = parentFormUpdates ? updateParentForms(updatedSubFields, localPath, parentFormUpdates) : updatedSubFields; return updatedParentForms; }; } export default createFormActionsReducer();