@kabbi/react-redux-form
Version:
Create Forms Easily with React and Redux
257 lines (217 loc) • 6.67 kB
JavaScript
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import _get from '../utils/get';
import identity from '../utils/identity';
import omit from '../utils/omit';
import isPlainObject from '../utils/is-plain-object';
import pick from '../utils/pick';
import { connect } from 'react-redux';
import invariant from 'invariant';
import actions from '../actions';
import Control from './control-component';
import controlPropsMap from '../constants/control-props-map';
import deepCompareChildren from '../utils/deep-compare-children';
import shallowCompareWithoutChildren from '../utils/shallow-compare-without-children';
import getModel from '../utils/get-model';
import getFieldFromState from '../utils/get-field-from-state';
import resolveModel from '../utils/resolve-model';
import { getCheckboxValue } from '../utils/get-value';
import initialFieldState from '../constants/initial-field-state';
const fieldPropTypes = {
model: PropTypes.oneOfType([
PropTypes.func,
PropTypes.string,
]).isRequired,
component: PropTypes.oneOfType([
PropTypes.func,
PropTypes.string,
]),
parser: PropTypes.func,
updateOn: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.string),
PropTypes.string,
]),
changeAction: PropTypes.func,
validators: PropTypes.oneOfType([
PropTypes.func,
PropTypes.object,
]),
asyncValidators: PropTypes.object,
validateOn: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.string),
PropTypes.string,
]),
asyncValidateOn: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.string),
PropTypes.string,
]),
errors: PropTypes.oneOfType([
PropTypes.func,
PropTypes.object,
]),
mapProps: PropTypes.oneOfType([
PropTypes.func,
PropTypes.object,
]),
componentMap: PropTypes.object,
dynamic: PropTypes.bool,
dispatch: PropTypes.func,
getRef: PropTypes.func,
// Calculated props
fieldValue: PropTypes.object,
store: PropTypes.shape({
subscribe: PropTypes.func,
dispatch: PropTypes.func,
getState: PropTypes.func,
}),
};
function getControlType(control, props, options) {
const { controlPropsMap: _controlPropsMap } = options;
const controlDisplayNames = Object.keys(_controlPropsMap)
.filter((controlKey) => {
const propsMap = _controlPropsMap[controlKey];
if (isPlainObject(propsMap) && propsMap.component) {
return control.type === propsMap.component;
}
return false;
});
if (controlDisplayNames.length) return controlDisplayNames[0];
try {
let controlDisplayName = control.constructor.displayName
|| control.type.displayName
|| control.type.name
|| control.type;
if (controlDisplayName === 'input') {
controlDisplayName = _controlPropsMap[control.props.type] ? control.props.type : 'text';
}
return _controlPropsMap[controlDisplayName] ? controlDisplayName : null;
} catch (error) {
return undefined;
}
}
const defaultStrategy = {
Control,
controlPropTypes: fieldPropTypes,
getFieldFromState,
actions,
};
function createFieldClass(customControlPropsMap = {}, s = defaultStrategy) {
// Use the fieldPropTypes if no controlProptypes have been defined to
// maintain backwards compatibiltiy.
const controlPropTypes = s.controlPropTypes || fieldPropTypes;
function mapStateToProps(state, props) {
const {
model,
} = props;
const modelString = getModel(model, state);
const fieldValue = s.getFieldFromState(state, modelString)
|| initialFieldState;
return {
model: modelString,
fieldValue,
};
}
const options = {
controlPropsMap: {
...controlPropsMap,
...customControlPropsMap,
},
};
// TODO: refactor
const defaultControlPropsMap = {
checkbox: {
changeAction: s.actions.checkWithValue,
getValue: getCheckboxValue,
isToggle: true,
},
radio: {
isToggle: true,
},
};
class Field extends Component {
shouldComponentUpdate(nextProps, nextState) {
const { dynamic } = this.props;
if (dynamic) {
return deepCompareChildren(this, nextProps, nextState);
}
return shallowCompareWithoutChildren(this, nextProps);
}
createControlComponent(control) {
const { props } = this;
if (!control
|| !control.props
|| control instanceof Control) {
return control;
}
const controlType = getControlType(control, props, options);
const {
mapProps = options.controlPropsMap[controlType],
} = props;
const controlProps = pick(props, Object.keys(controlPropTypes));
if (!mapProps) {
return React.cloneElement(
control,
null,
this.mapChildrenToControl(control.props.children)
);
}
return React.createElement(
s.Control,
{
...controlProps,
control,
controlProps: control.props,
component: control.type,
mapProps,
...(defaultControlPropsMap[controlType] || {}),
});
}
mapChildrenToControl(children) {
if (React.Children.count(children) > 1) {
return React.Children.map(
children,
(child) => this.createControlComponent(child));
}
return this.createControlComponent(children);
}
render() {
const {
component,
children, // eslint-disable-line react/prop-types
fieldValue,
} = this.props;
const allowedProps = omit(this.props, Object.keys(controlPropTypes));
const renderableChildren = typeof children === 'function'
? children(fieldValue)
: children;
if (!component) {
invariant(React.Children.count(renderableChildren) === 1,
'Empty wrapper components for <Field> are only possible'
+ 'when there is a single child. Please check the children'
+ `passed into <Field model="${this.props.model}">.`);
return this.createControlComponent(renderableChildren);
}
return React.createElement(
component,
allowedProps,
this.mapChildrenToControl(renderableChildren));
}
}
if (process.env.NODE_ENV !== 'production') {
Field.propTypes = fieldPropTypes;
}
Field.defaultProps = {
updateOn: 'change',
asyncValidateOn: 'blur',
parser: identity,
changeAction: actions.change,
dynamic: true,
component: 'div',
};
return resolveModel(connect(mapStateToProps)(Field));
}
export {
controlPropsMap,
createFieldClass,
};
export default createFieldClass(controlPropsMap);