UNPKG

redux-form

Version:

A higher order component decorator for forms using Redux and React

259 lines (249 loc) 7.6 kB
import { ADD_ARRAY_VALUE, BLUR, CHANGE, DESTROY, FOCUS, INITIALIZE, REMOVE_ARRAY_VALUE, RESET, START_ASYNC_VALIDATION, START_SUBMIT, STOP_ASYNC_VALIDATION, STOP_SUBMIT, SUBMIT_FAILED, SWAP_ARRAY_VALUES, TOUCH, UNTOUCH } from './actionTypes'; import mapValues from './mapValues'; import read from './read'; import write from './write'; import getValuesFromState from './getValuesFromState'; import initializeState from './initializeState'; import resetState from './resetState'; import setErrors from './setErrors'; import {makeFieldValue} from './fieldValue'; import normalizeFields from './normalizeFields'; export const globalErrorKey = '_error'; export const initialState = { _active: undefined, _asyncValidating: false, [globalErrorKey]: undefined, _initialized: false, _submitting: false, _submitFailed: false }; const behaviors = { [ADD_ARRAY_VALUE](state, {path, index, value, fields}) { const array = read(path, state); const stateCopy = {...state}; const arrayCopy = array ? [...array] : []; const newValue = value !== null && typeof value === 'object' ? initializeState(value, fields || Object.keys(value)) : makeFieldValue({value}); if (index === undefined) { arrayCopy.push(newValue); } else { arrayCopy.splice(index, 0, newValue); } return write(path, arrayCopy, stateCopy); }, [BLUR](state, {field, value, touch}) { // remove _active from state const {_active, ...stateCopy} = state; // eslint-disable-line prefer-const return write(field, previous => { const result = {...previous}; if (value !== undefined) { result.value = value; } if (touch) { result.touched = true; } return makeFieldValue(result); }, stateCopy); }, [CHANGE](state, {field, value, touch}) { return write(field, previous => { const {asyncError, submitError, ...result} = {...previous, value}; if (touch) { result.touched = true; } return makeFieldValue(result); }, state); }, [DESTROY]() { return undefined; }, [FOCUS](state, {field}) { const stateCopy = write(field, previous => makeFieldValue({...previous, visited: true}), state); stateCopy._active = field; return stateCopy; }, [INITIALIZE](state, {data, fields}) { return { ...initializeState(data, fields, state), _asyncValidating: false, _active: undefined, [globalErrorKey]: undefined, _initialized: true, _submitting: false, _submitFailed: false }; }, [REMOVE_ARRAY_VALUE](state, {path, index}) { const array = read(path, state); const stateCopy = {...state}; const arrayCopy = array ? [...array] : []; if (index === undefined) { arrayCopy.pop(); } else if (isNaN(index)) { delete arrayCopy[index]; } else { arrayCopy.splice(index, 1); } return write(path, arrayCopy, stateCopy); }, [RESET](state) { return { ...resetState(state), _active: undefined, _asyncValidating: false, [globalErrorKey]: undefined, _initialized: state._initialized, _submitting: false, _submitFailed: false }; }, [START_ASYNC_VALIDATION](state, {field}) { return { ...state, _asyncValidating: field || true }; }, [START_SUBMIT](state) { return { ...state, _submitting: true }; }, [STOP_ASYNC_VALIDATION](state, {errors}) { return { ...setErrors(state, errors, 'asyncError'), _asyncValidating: false, [globalErrorKey]: errors && errors[globalErrorKey] }; }, [STOP_SUBMIT](state, {errors}) { return { ...setErrors(state, errors, 'submitError'), [globalErrorKey]: errors && errors[globalErrorKey], _submitting: false, _submitFailed: !!(errors && Object.keys(errors).length) }; }, [SUBMIT_FAILED](state) { return { ...state, _submitFailed: true }; }, [SWAP_ARRAY_VALUES](state, {path, indexA, indexB}) { const array = read(path, state); const arrayLength = array.length; if (indexA === indexB || isNaN(indexA) || isNaN(indexB) || indexA >= arrayLength || indexB >= arrayLength ) { return state; // do nothing } const stateCopy = {...state}; const arrayCopy = [...array]; arrayCopy[indexA] = array[indexB]; arrayCopy[indexB] = array[indexA]; return write(path, arrayCopy, stateCopy); }, [TOUCH](state, {fields}) { return { ...state, ...fields.reduce((accumulator, field) => write(field, value => makeFieldValue({...value, touched: true}), accumulator), state) }; }, [UNTOUCH](state, {fields}) { return { ...state, ...fields.reduce((accumulator, field) => write(field, value => { if (value) { const {touched, ...rest} = value; return makeFieldValue(rest); } return makeFieldValue(value); }, accumulator), state) }; } }; const reducer = (state = initialState, action = {}) => { const behavior = behaviors[action.type]; return behavior ? behavior(state, action) : state; }; function formReducer(state = {}, action = {}) { const {form, key, ...rest} = action; // eslint-disable-line no-redeclare if (!form) { return state; } if (key) { if (action.type === DESTROY) { return { ...state, [form]: state[form] && Object.keys(state[form]).reduce((accumulator, stateKey) => stateKey === key ? accumulator : { ...accumulator, [stateKey]: state[form][stateKey] }, {}) }; } return { ...state, [form]: { ...state[form], [key]: reducer((state[form] || {})[key], rest) } }; } if (action.type === DESTROY) { return Object.keys(state).reduce((accumulator, formName) => formName === form ? accumulator : { ...accumulator, [formName]: state[formName] }, {}); } return { ...state, [form]: reducer(state[form], rest) }; } /** * Adds additional functionality to the reducer */ function decorate(target) { target.plugin = function plugin(reducers) { // use 'function' keyword to enable 'this' return decorate((state = {}, action = {}) => { const result = this(state, action); return { ...result, ...mapValues(reducers, (pluginReducer, key) => pluginReducer(result[key] || initialState, action)) }; }); }; target.normalize = function normalize(normalizers) { // use 'function' keyword to enable 'this' return decorate((state = {}, action = {}) => { const result = this(state, action); return { ...result, ...mapValues(normalizers, (formNormalizers, form) => { const runNormalize = (previous, currentResult) => { const previousValues = getValuesFromState({ ...initialState, ...previous }); const formResult = { ...initialState, ...currentResult }; const values = getValuesFromState(formResult); return normalizeFields(formNormalizers, formResult, previous, values, previousValues); }; if (action.key) { return { ...result[form], [action.key]: runNormalize(state[form][action.key], result[form][action.key]) }; } return runNormalize(state[form], result[form]); }) }; }); }; return target; } export default decorate(formReducer);