// external dependencies
import isFunction from 'lodash/isFunction';
import React from 'react';
import {connect} from 'react-redux';
// constants
import {
ERROR_TYPES,
keys,
} from './constants';
// utils
import {
getComponentMethods,
isReactClass,
isReactEvent,
testParameter,
} from './utils';
// components
import Component from './Component';
/**
* @module components
*/
/**
* @private
*
* @function addPropertyIfExists
*
* @description
* add to component the property if the value exists
*
* @param {Component} component component to add the property to
* @param {string} property property name
* @param {*} value value of the property to assign
* @returns {Component}
*/
export const addPropertyIfExists = (component, property, value) => {
if (value) {
component[property] = value;
}
return component;
};
/**
* @private
*
* @function assignChildContext
*
* @description
* assign the child context to the component passed
*
* @param {Component} component component to assign child context to
* @param {function} getChildContext method for getting child context
* @param {boolean} [canAccessThis=false] can the method access the instance
* @returns {Component}
*/
export const assignChildContext = (component, getChildContext, canAccessThis = false) => {
testParameter(getChildContext, isFunction, 'getChildContext is not a function', ERROR_TYPES.TYPE);
const boundThis = canAccessThis ? component : undefined;
component.getChildContext = () => getChildContext.call(boundThis, component.props, component.context);
return component;
};
/**
* @private
*
* @function connectIfReduxPropertiesExist
*
* @description
* if there are redux-specific options present, connect the component
*
* @param {Component} component component to connect to redux if applicable
* @param {function|Object} mapDispatchToProps functions wrapped in dispatch to pass as props
* @param {function} mapStateToProps state to pass as props
* @param {function} mergeProps function to merge store state with local props
* @param {Object} reduxOptions additional options to pass to @connect
* @returns {Component}
*/
export const connectIfReduxPropertiesExist = (
component,
{mapDispatchToProps, mapStateToProps, mergeProps, reduxOptions}
) => {
if (mapDispatchToProps || mapStateToProps || mergeProps || reduxOptions) {
return connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
reduxOptions
)(component);
}
return component;
};
/**
* @private
*
* @function getAllPropsToPass
*
* @description
* combine normal props with local methods for all props to pass
*
* @param {Component} component component to get the props and local methods from
* @param {function} [component._getPropsToPass] method to retrieve combined props
* @param {Object} [component._localMethods] local methods assigned to the HOC instance
* @param {Object} [component.props] props passed to the HOC
* @returns {Object}
*/
export const getAllPropsToPass = (component) => component._getPropsToPass(component.props, component._localMethods);
/**
* @private
*
* @function assignLifecycleMethods
*
* @description
* assign the lifecycle methods to the instance
*
* @param {Component|StatefulComponent} component component to assign lifecycle methods to
* @param {function} component._getPropsToPass function to retrieve all props to pass down
* @param {Object} component._localMethods local methods to add to props passed down
* @param {Object} component.props actual props to pass down
* @param {Object} lifecycleMethods map of lifecycle methods
* @param {boolean} [canAccessThis=false] can the method access the instance
* @returns {Component}
*/
export const assignLifecycleMethods = (component, lifecycleMethods, canAccessThis = false) => {
const appliedThis = canAccessThis ? component : undefined;
keys(lifecycleMethods).forEach((key) => {
testParameter(
lifecycleMethods[key],
isFunction,
`${key} is not a function, skipping assignment to instance.`,
ERROR_TYPES.TYPE
);
component[key] = (props) => {
let args = [getAllPropsToPass(component)];
if (props) {
args.push(props);
}
args.push(component.context);
return lifecycleMethods[key].apply(appliedThis, args);
};
});
return component;
};
/**
* @private
*
* @function assignLocalMethods
*
* @description
* assign the local methods to the instance
*
* @param {Component} component component to assign local methods to
* @param {function} component._getPropsToPass function to retrieve all props to pass down
* @param {Object} component._localMethods local methods to add to props passed down
* @param {Object} localMethods map of methods accessible locally through props
* @returns {Component}
*/
export const assignLocalMethods = (component, localMethods) => {
keys(localMethods).forEach((key) => {
component._localMethods[key] = (...args) => {
const [event, ...restOfArgs] = args;
const isFirstArgEvent = isReactEvent(event);
let argsToPass = [getAllPropsToPass(component)];
if (isFirstArgEvent) {
argsToPass.unshift(event);
}
argsToPass.push(component.context);
argsToPass.push(isFirstArgEvent ? restOfArgs : args);
return localMethods[key].apply(undefined, argsToPass);
};
});
return component;
};
/**
* @private
*
* @function getStatefulComponent
*
* @description
* get the stateful component that, if the options are passed, is connected to redux
*
* @param {Component} PassedComponent component wrapped by arco
* @param {Object} options options to apply to the HOC created by arco
* @returns {Component}
*/
export const getStatefulComponent = (PassedComponent, options) => {
const {
childContextTypes,
contextTypes,
getChildContext,
mapDispatchToProps: mapDispatchToPropsIgnored,
mapStateToProps: mapStateToPropsIgnored,
mergeProps: mergePropsIgnored,
propTypes,
reduxOptions: reduxOptionsIgnored,
...restOfOptions
} = options;
const {lifecycleMethods} = getComponentMethods(restOfOptions);
class StatefulComponent extends PassedComponent {
constructor(...args) {
super(...args);
assignLifecycleMethods(this, lifecycleMethods, true);
if (childContextTypes && getChildContext) {
assignChildContext(this, getChildContext, true);
}
}
}
addPropertyIfExists(StatefulComponent, 'childContextTypes', childContextTypes);
addPropertyIfExists(StatefulComponent, 'contextTypes', contextTypes);
addPropertyIfExists(StatefulComponent, 'propTypes', propTypes);
return connectIfReduxPropertiesExist(StatefulComponent, options);
};
/**
* @private
*
* @function getStatelessComponent
*
* @description
* get the stateless component HOC that has local and lifecycle methods based on
* the options, as well as possibly being connected to redux
*
* @param {Component|function} PassedComponent component wrapped by arco
* @param {Object} options options to apply to the HOC created by arco
* @returns {Component}
*/
export const getStatelessComponent = (PassedComponent, options) => {
const {
childContextTypes,
contextTypes,
getChildContext,
mapDispatchToProps: mapDispatchToPropsIgnored,
mapStateToProps: mapStateToPropsIgnored,
mergeProps: mergePropsIgnored,
propTypes,
reduxOptions: reduxOptionsIgnored,
...restOfOptions
} = options;
const {lifecycleMethods, localMethods} = getComponentMethods(restOfOptions);
addPropertyIfExists(PassedComponent, 'contextTypes', contextTypes);
addPropertyIfExists(PassedComponent, 'propTypes', propTypes);
class StatelessComponent extends Component {
constructor(...args) {
super(...args);
assignLifecycleMethods(this, lifecycleMethods);
assignLocalMethods(this, localMethods);
if (childContextTypes && getChildContext) {
assignChildContext(this, getChildContext);
}
}
render() {
const propsToPass = this._getPropsToPass(this.props, this._localMethods);
return <PassedComponent {...propsToPass} />;
}
}
addPropertyIfExists(StatelessComponent, 'childContextTypes', childContextTypes);
return connectIfReduxPropertiesExist(StatelessComponent, options);
};
/**
* @function createComponent
*
* @description
* create a simple component where props are rendered
*
* @example
* import createComponent from 'arco';
*
* const OPTIONS = {
* onButtonClick() {
* alert('hello!');
* }
* };
*
* const Foo = ({onButtonClick}) => {
* return (
* <button
* onClick={onButtonClick}
* type="button"
* >
* Click me!
* </button>
* );
* };
*
* export default createComponent(Foo, OPTIONS);
*
* @param {Component|function} PassedComponent component to wrap
* @param {Object} [options={}] options to apply to the HOC created by arco
* @returns {Component|function(Component): Component}
*/
export const createComponent = (PassedComponent, options = {}) => {
if (isReactClass(PassedComponent)) {
return getStatefulComponent(PassedComponent, options);
}
return getStatelessComponent(PassedComponent, options);
};
export default createComponent;