Source: app/enhanced-redux-form/enhancedReduxFormMiddleware.js

import { actionTypes as formActionTypes } from 'redux-form';
import { debounce } from 'throttle-debounce';
import * as ValidateOn from './data/ValidateOn';
import {
  formShouldValidate,
  clearValidation,
  destroyEnhancedForm,
} from './actions/enhancedFormActions';
import { composeCompositeInputs } from './actions/compositeInputActions';
import { COMPOSITE_FORMSECTION_NAME_PREFIX } from './data/constants';

/** @module enhanced-redux-form/enhancedReduxFormMiddleware */

/**
 * Debounce delay for redux-form events
 */
const INPUT_ACTION_DEBOUNCE = 150;

/**
 * Delay for grouping blur/focus to detect composite blur/focus
 */
const FOCUS_BLUR_DELAY = 100;

/**
 * Map of ValidateOn event name per actionType
 */
const VALIDATION_EVENTS = Object.keys(ValidateOn).reduce((events, eventName) => {
  // eslint-disable-next-line no-param-reassign
  events[formActionTypes[eventName]] = eventName;
  return events;
}, {});

/**
 * Regular expression to match names of FieldSection instances that have been
 * created by CompositeInput. The name of the CompositeInput is available as
 * the first captured group
 * @type {RegExp}
 */
const COMPOSITE_FIELD_SECTION_NAME_REGEXP = new RegExp(
  `^${COMPOSITE_FORMSECTION_NAME_PREFIX}(.+)$`,
);

/**
 * Extracts all composite field names from the given field name
 * @private
 * @param fieldName
 * @returns {Array<string>} An array of composite input names without prefix
 */
const getCompositeInputNames = fieldName =>
  fieldName
    .split('.')
    .map(inputName => inputName.match(COMPOSITE_FIELD_SECTION_NAME_REGEXP))
    .filter(match => match)
    .map(match => match[1]);

/**
 * Returns the names of all composite inputs which are currently active (with focus)
 * on the given form.
 * @private
 */
const getActiveCompositeInputs = (formState, form) =>
  formState[form] && formState[form].active ? getCompositeInputNames(formState[form].active) : [];

/**
 * Form validation middleware that should be passed to redux when initializing the store. This
 * will intercept redux-form actions and perform enhanced functionality accordingly
 * @function enhancedReduxFormMiddleware
 * @param {object} params Parameters passed by redux middleware API
 * @returns {function} A redux middleware function
 * @category forms
 */
const enhancedReduxFormMiddleware = ({ getState, dispatch }) => {
  const eventBuffer = [];

  let beforeFocusState = null;
  const focusChangeComplete = () => {
    const newState = getState();
    Object.keys(newState.form)
      .filter(
        formName =>
          newState.form[formName].active !== (beforeFocusState.form[formName] || {}).active,
      )
      .forEach(form => {
        const wasActive = getActiveCompositeInputs(beforeFocusState.form, form);
        const isActive = getActiveCompositeInputs(newState.form, form);
        isActive
          .filter(name => !wasActive.includes(name))
          .forEach(name => eventBuffer.push({ form, field: name, event: 'FOCUS' }));
        wasActive
          .filter(name => !isActive.includes(name))
          .forEach(name => eventBuffer.push({ form, field: name, event: 'BLUR' }));
      });

    beforeFocusState = null;
  };
  const focusChangeStart = state => {
    if (!beforeFocusState) {
      beforeFocusState = state;
      setTimeout(focusChangeComplete, FOCUS_BLUR_DELAY);
    }
  };

  const scheduleHandleEvents = debounce(INPUT_ACTION_DEBOUNCE, () => {
    const actionsToExecute = {};
    const state = getState();

    eventBuffer.forEach(event => {
      const { field, form, event: eventName, time: eventTime } = event;
      const formValidationState = state.enhancedForm.validation[form];
      const compositeInputState = state.enhancedForm.compositeInput[form];

      if (!actionsToExecute[form]) {
        actionsToExecute[form] = {
          validate: new Set(),
          clearValidation: new Set(),
          compose: new Set(),
        };
      }

      if (formValidationState) {
        if (
          formValidationState.validateOn &&
          formValidationState.validateOn[eventName] &&
          formValidationState.validateOn[eventName].includes(field)
        ) {
          actionsToExecute[form].validate.add(field);
          actionsToExecute[form].compose.add(field);
        } else if (
          eventName === 'CHANGE' &&
          formValidationState.validated &&
          formValidationState.validated.includes(field) &&
          formValidationState.lastValidationTime[field] &&
          formValidationState.lastValidationTime[field] < eventTime
        ) {
          actionsToExecute[form].clearValidation.add(field);
        }
      }

      if (
        compositeInputState &&
        compositeInputState[field] &&
        compositeInputState[field].composeOn === eventName
      ) {
        actionsToExecute[form].compose.add(field);
      }
    });

    Object.keys(actionsToExecute).forEach(form => {
      if (actionsToExecute[form].compose.size) {
        dispatch(composeCompositeInputs(form, Array.from(actionsToExecute[form].compose)));
      }
      if (actionsToExecute[form].validate.size) {
        dispatch(formShouldValidate(form, Array.from(actionsToExecute[form].validate)));
      }
      if (actionsToExecute[form].clearValidation.size) {
        dispatch(clearValidation(form, Array.from(actionsToExecute[form].clearValidation)));
      }
    });

    eventBuffer.length = 0;
  });

  return next => action => {
    const { type } = action;
    if (type === formActionTypes.DESTROY) {
      dispatch(destroyEnhancedForm(action.meta.form));
      return next(action);
    }

    if (VALIDATION_EVENTS[type]) {
      const { form, field } = action.meta;
      const time = new Date().getTime();

      // push a regular event to the event buffer
      eventBuffer.push({ form, field, event: VALIDATION_EVENTS[type], time });

      const state = getState();

      const compositeInputState = state.enhancedForm.compositeInput[action.meta.form];
      if (compositeInputState) {
        if (type === formActionTypes.CHANGE) {
          getCompositeInputNames(action.meta.field).forEach(name =>
            eventBuffer.push({ form, field: name, event: 'CHANGE', time }),
          );
        }

        if (type === formActionTypes.BLUR || type === formActionTypes.FOCUS) {
          focusChangeStart(state);
        }
      }

      scheduleHandleEvents();
    }

    return next(action);
  };
};

export default enhancedReduxFormMiddleware;