erikras/redux-form

View on GitHub
src/ConnectedField.js

Summary

Maintainability
B
6 hrs
Test Coverage
// @flow
import React, { Component, createElement } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import createFieldProps from './createFieldProps'
import onChangeValue from './events/onChangeValue'
import { dataKey } from './util/eventConsts'
import plain from './structure/plain'
import isReactNative from './isReactNative'
import type { ElementRef } from 'react'
import type { Structure } from './types.js.flow'
import type { Props } from './ConnectedField.types'
import validateComponentProp from './util/validateComponentProp'
import isEvent from './events/isEvent'

const propsToNotUpdateFor = ['_reduxForm']

const isObject = entity => entity && typeof entity === 'object'

const isFunction = entity => entity && typeof entity === 'function'

const eventPreventDefault = event => {
  if (isObject(event) && isFunction(event.preventDefault)) {
    event.preventDefault()
  }
}

const eventDataTransferGetData = (event, key) => {
  if (isObject(event) && isObject(event.dataTransfer) && isFunction(event.dataTransfer.getData)) {
    return event.dataTransfer.getData(key)
  }
}

const eventDataTransferSetData = (event, key, value) => {
  if (isObject(event) && isObject(event.dataTransfer) && isFunction(event.dataTransfer.setData)) {
    event.dataTransfer.setData(key, value)
  }
}

function createConnectedField<L, M>(structure: Structure<L, M>) {
  const { deepEqual, getIn } = structure
  const getSyncError = (syncErrors: Object, name: string) => {
    const error = plain.getIn(syncErrors, name)
    // Because the error for this field might not be at a level in the error structure where
    // it can be set directly, it might need to be unwrapped from the _error property
    return error && error._error ? error._error : error
  }

  const getSyncWarning = (syncWarnings: Object, name: string) => {
    const warning = getIn(syncWarnings, name)
    // Because the warning for this field might not be at a level in the warning structure where
    // it can be set directly, it might need to be unwrapped from the _warning property
    return warning && warning._warning ? warning._warning : warning
  }

  class ConnectedField extends Component<Props> {
    ref: ElementRef<any> = React.createRef()

    shouldComponentUpdate(nextProps: Props) {
      const nextPropsKeys = Object.keys(nextProps)
      const thisPropsKeys = Object.keys(this.props)
      // if we have children, we MUST update in React 16
      // https://twitter.com/erikras/status/915866544558788608
      return !!(
        this.props.children ||
        nextProps.children ||
        nextPropsKeys.length !== thisPropsKeys.length ||
        nextPropsKeys.some(prop => {
          if (~(nextProps.immutableProps || []).indexOf(prop)) {
            return this.props[prop] !== nextProps[prop]
          }
          return (
            !~propsToNotUpdateFor.indexOf(prop) && !deepEqual(this.props[prop], nextProps[prop])
          )
        })
      )
    }

    isPristine = (): boolean => this.props.pristine

    getValue = (): any => this.props.value

    getRenderedComponent(): React.Component<any, any> {
      return this.ref.current
    }

    handleChange = (event: any) => {
      const {
        name,
        dispatch,
        parse,
        normalize,
        onChange,
        _reduxForm,
        value: previousValue
      } = this.props
      const newValue = onChangeValue(event, { name, parse, normalize })

      let defaultPrevented = false
      if (onChange) {
        // Can't seem to find a way to extend Event in React Native,
        // thus I simply avoid adding preventDefault() in a RN environment
        // to prevent the following error:
        // `One of the sources for assign has an enumerable key on the prototype chain`
        // Reference: https://github.com/facebook/react-native/issues/5507
        if (!isReactNative && isEvent(event)) {
          onChange(
            {
              ...event,
              preventDefault: () => {
                defaultPrevented = true
                return eventPreventDefault(event)
              }
            },
            newValue,
            previousValue,
            name
          )
        } else {
          const onChangeResult = onChange(event, newValue, previousValue, name)
          // Return value of change handler affecting preventDefault is RN
          // specific behavior.
          if (isReactNative) {
            defaultPrevented = onChangeResult
          }
        }
      }
      if (!defaultPrevented) {
        // dispatch change action
        dispatch(_reduxForm.change(name, newValue))

        // call post-change callback
        if (_reduxForm.asyncValidate) {
          _reduxForm.asyncValidate(name, newValue, 'change')
        }
      }
    }

    handleFocus = (event: any) => {
      const { name, dispatch, onFocus, _reduxForm } = this.props

      let defaultPrevented = false
      if (onFocus) {
        if (!isReactNative) {
          onFocus(
            {
              ...event,
              preventDefault: () => {
                defaultPrevented = true
                return eventPreventDefault(event)
              }
            },
            name
          )
        } else {
          defaultPrevented = onFocus(event, name)
        }
      }

      if (!defaultPrevented) {
        dispatch(_reduxForm.focus(name))
      }
    }

    handleBlur = (event: any) => {
      const {
        name,
        dispatch,
        parse,
        normalize,
        onBlur,
        _reduxForm,
        _value,
        value: previousValue
      } = this.props
      let newValue = onChangeValue(event, { name, parse, normalize })

      // for checkbox and radio, if the value property of checkbox or radio equals
      // the value passed by blur event, then fire blur action with previousValue.
      if (newValue === _value && _value !== undefined) {
        newValue = previousValue
      }

      let defaultPrevented = false
      if (onBlur) {
        if (!isReactNative) {
          onBlur(
            {
              ...event,
              preventDefault: () => {
                defaultPrevented = true
                return eventPreventDefault(event)
              }
            },
            newValue,
            previousValue,
            name
          )
        } else {
          defaultPrevented = onBlur(event, newValue, previousValue, name)
        }
      }

      if (!defaultPrevented) {
        // dispatch blur action
        dispatch(_reduxForm.blur(name, newValue))

        // call post-blur callback
        if (_reduxForm.asyncValidate) {
          _reduxForm.asyncValidate(name, newValue, 'blur')
        }
      }
    }

    handleDragStart = (event: any) => {
      const { name, onDragStart, value } = this.props
      eventDataTransferSetData(event, dataKey, value == null ? '' : value)

      if (onDragStart) {
        onDragStart(event, name)
      }
    }

    handleDrop = (event: any) => {
      const { name, dispatch, onDrop, _reduxForm, value: previousValue } = this.props
      const newValue = eventDataTransferGetData(event, dataKey)

      let defaultPrevented = false
      if (onDrop) {
        onDrop(
          {
            ...event,
            preventDefault: () => {
              defaultPrevented = true
              return eventPreventDefault(event)
            }
          },
          newValue,
          previousValue,
          name
        )
      }

      if (!defaultPrevented) {
        // dispatch change action
        dispatch(_reduxForm.change(name, newValue))
        eventPreventDefault(event)
      }
    }

    render() {
      const {
        component,
        forwardRef,
        name,
        // remove props that are part of redux internals:
        _reduxForm, // eslint-disable-line no-unused-vars
        normalize, // eslint-disable-line no-unused-vars
        onBlur, // eslint-disable-line no-unused-vars
        onChange, // eslint-disable-line no-unused-vars
        onFocus, // eslint-disable-line no-unused-vars
        onDragStart, // eslint-disable-line no-unused-vars
        onDrop, // eslint-disable-line no-unused-vars
        immutableProps, // eslint-disable-line no-unused-vars
        ...rest
      } = this.props
      const { custom, ...props } = createFieldProps(structure, name, {
        ...rest,
        form: _reduxForm.form,
        onBlur: this.handleBlur,
        onChange: this.handleChange,
        onDrop: this.handleDrop,
        onDragStart: this.handleDragStart,
        onFocus: this.handleFocus
      })
      if (forwardRef) {
        custom.ref = this.ref
      }
      if (typeof component === 'string') {
        const { input, meta } = props // eslint-disable-line no-unused-vars
        // flatten input into other props
        return createElement(component, { ...input, ...custom })
      } else {
        return createElement(component, { ...props, ...custom })
      }
    }
  }

  ConnectedField.propTypes = {
    component: validateComponentProp,
    props: PropTypes.object
  }

  const connector = connect(
    (state, ownProps) => {
      const {
        name,
        _reduxForm: { initialValues, getFormState }
      } = ownProps
      const formState = getFormState(state)
      const initialState = getIn(formState, `initial.${name}`)
      const initial =
        initialState !== undefined ? initialState : initialValues && getIn(initialValues, name)
      const value = getIn(formState, `values.${name}`)
      const submitting = getIn(formState, 'submitting')
      const syncError = getSyncError(getIn(formState, 'syncErrors'), name)
      const syncWarning = getSyncWarning(getIn(formState, 'syncWarnings'), name)
      const pristine = deepEqual(value, initial)
      return {
        asyncError: getIn(formState, `asyncErrors.${name}`),
        asyncValidating: getIn(formState, 'asyncValidating') === name,
        dirty: !pristine,
        pristine,
        state: getIn(formState, `fields.${name}`),
        submitError: getIn(formState, `submitErrors.${name}`),
        submitFailed: getIn(formState, 'submitFailed'),
        submitting,
        syncError,
        syncWarning,
        initial,
        value,
        _value: ownProps.value // save value passed in (for radios)
      }
    },
    undefined,
    undefined,
    { forwardRef: true }
  )
  return connector(ConnectedField)
}

export default createConnectedField