src/createFields.js
// @flow
import { Component, createElement, createRef } from 'react'
import PropTypes from 'prop-types'
import invariant from 'invariant'
import get from 'lodash/get'
import createConnectedFields from './ConnectedFields'
import shallowCompare from './util/shallowCompare'
import plain from './structure/plain'
import prefixName from './util/prefixName'
import { withReduxForm } from './ReduxFormContext'
import type { Structure, ReactContext } from './types'
import type { Props as PropsWithoutContext, WarnAndValidateProp } from './FieldsProps.types'
import validateComponentProp from './util/validateComponentProp'
type Props = ReactContext & PropsWithoutContext
const validateNameProp = prop => {
if (!prop) {
return new Error('No "names" prop was specified <Fields/>')
}
if (!Array.isArray(prop) && !prop._isFieldArray) {
return new Error(
'Invalid prop "names" supplied to <Fields/>. Must be either an array of strings or the fields array generated by FieldArray.'
)
}
}
const warnAndValidatePropType = PropTypes.oneOfType([
PropTypes.func,
PropTypes.arrayOf(PropTypes.func),
PropTypes.objectOf(PropTypes.oneOfType([PropTypes.func, PropTypes.arrayOf(PropTypes.func)]))
])
const fieldsPropTypes = {
component: validateComponentProp,
format: PropTypes.func,
parse: PropTypes.func,
props: PropTypes.object,
forwardRef: PropTypes.bool,
validate: warnAndValidatePropType,
warn: warnAndValidatePropType
}
const getFieldWarnAndValidate = (prop?: WarnAndValidateProp, name) =>
Array.isArray(prop) || typeof prop === 'function' ? prop : get(prop, name, undefined)
export default function createFields(structure: Structure<any, any>) {
const ConnectedFields = createConnectedFields(structure)
class Fields extends Component<Props> {
connected = createRef<ConnectedFields>()
constructor(props: Props) {
super((props: Props))
if (!props._reduxForm) {
throw new Error('Fields must be inside a component decorated with reduxForm()')
}
const error = validateNameProp(props.names)
if (error) {
throw error
}
}
shouldComponentUpdate(nextProps: Props) {
return shallowCompare(this, nextProps)
}
componentDidMount() {
this.registerFields(this.props.names)
}
UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (!plain.deepEqual(this.props.names, nextProps.names)) {
const { props } = this
const { unregister } = props._reduxForm
// unregister old name
this.props.names.forEach(name => unregister(prefixName(props, name)))
// register new name
this.registerFields(nextProps.names)
}
}
componentWillUnmount() {
const { props } = this
const { unregister } = props._reduxForm
this.props.names.forEach(name => unregister(prefixName(props, name)))
}
registerFields(names: string[]) {
const { props } = this
const {
_reduxForm: { register }
} = props
names.forEach(name =>
register(
prefixName(props, name),
'Field',
() => getFieldWarnAndValidate(this.props.validate, name),
() => getFieldWarnAndValidate(this.props.warn, name)
)
)
}
getRenderedComponent() {
invariant(
this.props.forwardRef,
'If you want to access getRenderedComponent(), ' +
'you must specify a forwardRef prop to Fields'
)
return this.connected.current ? this.connected.current.getRenderedComponent() : null
}
get names(): string[] {
const { props } = this
return this.props.names.map(name => prefixName(props, name))
}
get dirty(): boolean {
return this.connected.current ? this.connected.current.isDirty() : false
}
get pristine(): boolean {
return !this.dirty
}
get values(): Object {
return this.connected.current ? this.connected.current.getValues() : {}
}
render() {
const { props } = this
return createElement(ConnectedFields, {
...this.props,
names: this.props.names.map(name => prefixName(props, name)),
ref: this.connected
})
}
}
Fields.propTypes = {
names: (props, propName) => validateNameProp(props[propName]),
...fieldsPropTypes
}
return withReduxForm(Fields)
}