Source: cmfConnect.js

  1. /**
  2. * This module connect your component in the CMF environment.
  3. * @module react-cmf/lib/cmfConnect
  4. * @example
  5. import { cmfConnect } from '@talend/react-cmf';
  6. function MyComponent(props) {
  7. const onClick = (event) => {
  8. props.dispatchActionCreator('myaction', event, { props: props });
  9. };
  10. return <button onClick={onClick}>Edit {props.foo.name}</button>;
  11. }
  12. function mapStateToProps(state) {
  13. return {
  14. foo: state.cmf.collection.get('foo', { name: 'world' }),
  15. };
  16. }
  17. export default cmfConnect({
  18. mapStateToProps,
  19. });
  20. */
  21. import PropTypes from 'prop-types';
  22. import { useState, useContext, useEffect, forwardRef } from 'react';
  23. import hoistStatics from 'hoist-non-react-statics';
  24. import ImmutablePropTypes from 'react-immutable-proptypes';
  25. import { connect, useStore } from 'react-redux';
  26. import { randomUUID } from '@talend/utils';
  27. import actions from './actions';
  28. import actionCreator from './actionCreator';
  29. import component from './component';
  30. import CONST from './constant';
  31. import expression from './expression';
  32. import onEvent from './onEvent';
  33. import { initState, getStateAccessors, getStateProps } from './componentState';
  34. import { mapStateToViewProps } from './settings';
  35. import omit from './omit';
  36. import { RegistryContext } from './RegistryProvider';
  37. export function getComponentName(WrappedComponent) {
  38. return WrappedComponent.displayName || WrappedComponent.name || 'Component';
  39. }
  40. export function getComponentId(componentId, props) {
  41. if (typeof componentId === 'function') {
  42. return componentId(props) || 'default';
  43. } else if (typeof componentId === 'string') {
  44. return componentId;
  45. } else if (props.componentId) {
  46. return props.componentId;
  47. }
  48. return 'default';
  49. }
  50. export function getStateToProps({
  51. defaultProps,
  52. componentId,
  53. ownProps,
  54. state,
  55. mapStateToProps,
  56. WrappedComponent,
  57. }) {
  58. const props = { ...defaultProps };
  59. const cmfProps = getStateProps(
  60. state,
  61. getComponentName(WrappedComponent),
  62. getComponentId(componentId, ownProps),
  63. );
  64. Object.assign(props, cmfProps);
  65. const viewProps = mapStateToViewProps(
  66. state,
  67. ownProps,
  68. getComponentName(WrappedComponent),
  69. getComponentId(componentId, ownProps),
  70. );
  71. Object.assign(props, viewProps);
  72. let userProps = {};
  73. if (mapStateToProps) {
  74. userProps = mapStateToProps(state, { ...ownProps, ...props }, cmfProps);
  75. }
  76. Object.assign(props, userProps);
  77. Object.assign(props, expression.mapStateToProps(state, { ...ownProps, ...props }));
  78. return props;
  79. }
  80. export function getDispatchToProps({
  81. defaultState,
  82. dispatch,
  83. componentId,
  84. mapDispatchToProps,
  85. ownProps,
  86. WrappedComponent,
  87. }) {
  88. const cmfProps = getStateAccessors(
  89. dispatch,
  90. getComponentName(WrappedComponent),
  91. getComponentId(componentId, ownProps),
  92. defaultState,
  93. );
  94. cmfProps.dispatch = dispatch;
  95. cmfProps.getComponent = component.get;
  96. cmfProps.dispatchActionCreator = (actionId, event, data, context) => {
  97. dispatch(actionCreator.get(context, actionId)(event, data, context));
  98. };
  99. let userProps = {};
  100. if (mapDispatchToProps) {
  101. if (process.env.NODE_ENV === 'development') {
  102. // eslint-disable-next-line no-console
  103. console.warn(`DEPRECATION WARNING: mapDispatchToProps will be removed from cmfConnect.
  104. Please use the injectedProps dispatchActionCreator or dispatch`);
  105. }
  106. userProps = mapDispatchToProps(dispatch, ownProps, cmfProps);
  107. }
  108. return { ...cmfProps, ...userProps };
  109. }
  110. /**
  111. * Internal: you should not have to use this
  112. * return the merged props which cleanup expression props
  113. * call mergeProps if exists after the cleanup
  114. * @param {object} options { mergeProps, stateProps, dispatchProps, ownProps }
  115. */
  116. export function getMergeProps({ mergeProps, stateProps, dispatchProps, ownProps }) {
  117. if (mergeProps) {
  118. return mergeProps(
  119. expression.mergeProps(stateProps),
  120. expression.mergeProps(dispatchProps),
  121. expression.mergeProps(ownProps),
  122. );
  123. }
  124. return {
  125. ...expression.mergeProps(ownProps),
  126. ...expression.mergeProps(dispatchProps),
  127. ...expression.mergeProps(stateProps),
  128. };
  129. }
  130. /**
  131. * this function wrap your component to inject CMF props
  132. * @example
  133. * The following props are injected:
  134. * - props.state
  135. * - props.setState
  136. * - props.initState (you should never have to call it your self)
  137. * - dispatch(action)
  138. * - dispatchActionCreator(id, event, data, [context])
  139. *
  140. * support for the following props
  141. * - initialState (called by props.initState)
  142. * - didMountActionCreator (id or array of id)
  143. * - willUnMountActionCreator (id or array of id)
  144. * - componentId (or will use uuid)
  145. * - keepComponentState (boolean, overrides the keepComponentState defined in container)
  146. * - didMountActionCreator (string called as action creator in didMount)
  147. * - view (string to inject the settings as props with ref support)
  148. * - whateverExpression (will inject `whatever` props and will remove it)
  149. * @example
  150. * options has the following shape:
  151. {
  152. componentId, // string or function(props) to compute the id in the store
  153. defaultState, // the default state when the component is mount
  154. keepComponent, // boolean, when the component is unmount, to keep it's state in redux store
  155. mapStateToProps, // function(state, ownProps) that should return the props (same as redux)
  156. mapDispatchToProps, // same as redux connect arg, you should use dispatchActionCreator instead
  157. mergeProps, // same as redux connect
  158. }
  159. * @param {object} options Option objects to configure the redux connect
  160. * @return {ReactComponent}
  161. */
  162. export default function cmfConnect({
  163. componentId,
  164. defaultState,
  165. defaultProps,
  166. keepComponentState,
  167. mapStateToProps,
  168. mapDispatchToProps,
  169. mergeProps,
  170. omitCMFProps = true,
  171. withComponentRegistry = false,
  172. withDispatch = false,
  173. withDispatchActionCreator = false,
  174. withComponentId = false,
  175. ...rest
  176. } = {}) {
  177. const propsToOmit = [];
  178. if (omitCMFProps) {
  179. if (!defaultState) {
  180. propsToOmit.push(...CONST.INJECTED_STATE_PROPS);
  181. }
  182. if (!withComponentRegistry) {
  183. propsToOmit.push('getComponent');
  184. }
  185. if (!withComponentId) {
  186. propsToOmit.push('componentId');
  187. }
  188. if (!withDispatch) {
  189. propsToOmit.push('dispatch');
  190. }
  191. if (!withDispatchActionCreator) {
  192. propsToOmit.push('dispatchActionCreator');
  193. }
  194. }
  195. let displayNameWarning = true;
  196. return function wrapWithCMF(WrappedComponent) {
  197. if (!WrappedComponent.displayName && displayNameWarning) {
  198. displayNameWarning = false;
  199. // eslint-disable-next-line no-console
  200. console.warn(
  201. `${WrappedComponent.name} has no displayName. Please read https://jira.talendforge.org/browse/TUI-302`,
  202. );
  203. }
  204. function getState(state, id = 'default') {
  205. return state.cmf.components.getIn([getComponentName(WrappedComponent), id], defaultState);
  206. }
  207. function getSetStateAction(state, id, type) {
  208. return {
  209. type: type || `${getComponentName(WrappedComponent)}.setState`,
  210. cmf: {
  211. componentState: actions.components.mergeState(
  212. getComponentName(WrappedComponent),
  213. id,
  214. state,
  215. ),
  216. },
  217. };
  218. }
  219. function CMFContainer(props, ref) {
  220. const [instanceId] = useState(randomUUID());
  221. const registry = useContext(RegistryContext);
  222. const store = useStore();
  223. function dispatchActionCreator(actionCreatorId, event, data, extraContext) {
  224. const extendedContext = { registry, store, ...extraContext };
  225. props.dispatchActionCreator(actionCreatorId, event, data, extendedContext);
  226. }
  227. useEffect(() => {
  228. initState(props);
  229. if (props.saga) {
  230. dispatchActionCreator(
  231. 'cmf.saga.start',
  232. { type: 'DID_MOUNT', componentId: instanceId },
  233. {
  234. ...props, // DEPRECATED
  235. componentId: getComponentId(componentId, props),
  236. },
  237. );
  238. }
  239. if (props.didMountActionCreator) {
  240. dispatchActionCreator(props.didMountActionCreator, null, props);
  241. }
  242. return () => {
  243. if (props.willUnmountActionCreator) {
  244. dispatchActionCreator(props.willUnmountActionCreator, null, props);
  245. }
  246. // if the props.keepComponentState is present we have to stick to it
  247. if (
  248. props.keepComponentState === false ||
  249. (props.keepComponentState === undefined && !keepComponentState)
  250. ) {
  251. props.deleteState(props.initialState);
  252. }
  253. if (props.saga) {
  254. dispatchActionCreator(
  255. 'cmf.saga.stop',
  256. { type: 'WILL_UNMOUNT', componentId: instanceId },
  257. props,
  258. );
  259. }
  260. };
  261. // eslint-disable-next-line react-hooks/exhaustive-deps
  262. }, []);
  263. function getOnEventProps() {
  264. return Object.keys(props).reduce(
  265. (acc, key) => {
  266. // TODO check how to replace the this
  267. onEvent.addOnEventSupport(onEvent.DISPATCH, { props }, acc, key);
  268. onEvent.addOnEventSupport(onEvent.ACTION_CREATOR, { props }, acc, key);
  269. onEvent.addOnEventSupport(onEvent.SETSTATE, { props }, acc, key);
  270. return acc;
  271. },
  272. { toOmit: [], dispatchActionCreator },
  273. );
  274. }
  275. if (props.renderIf === false) {
  276. return null;
  277. }
  278. const { toOmit, spreadCMFState, ...handlers } = getOnEventProps();
  279. // remove all internal props already used by the container
  280. delete handlers.dispatchActionCreator;
  281. toOmit.push(...CONST.CMF_PROPS, ...propsToOmit);
  282. if (props.omitRouterProps) {
  283. toOmit.push('omitRouterProps', ...CONST.INJECTED_ROUTER_PROPS);
  284. }
  285. let spreadedState = {};
  286. if ((spreadCMFState || props.spreadCMFState) && props.state) {
  287. spreadedState = props.state.toJS();
  288. }
  289. const newProps = {
  290. ...omit(props, toOmit),
  291. ...handlers,
  292. ...spreadedState,
  293. };
  294. if (newProps.dispatchActionCreator && toOmit.indexOf('dispatchActionCreator') === -1) {
  295. // override to inject CMFContainer context
  296. newProps.dispatchActionCreator = dispatchActionCreator;
  297. }
  298. if (!newProps.state && defaultState && toOmit.indexOf('state') === -1) {
  299. newProps.state = defaultState;
  300. }
  301. if (rest.forwardRef) {
  302. return <WrappedComponent {...newProps} ref={ref} />;
  303. }
  304. return <WrappedComponent {...newProps} />;
  305. }
  306. let CMFWithRef = hoistStatics(CMFContainer, WrappedComponent);
  307. CMFContainer.displayName = `CMF(${getComponentName(WrappedComponent)})`;
  308. CMFContainer.propTypes = {
  309. ...cmfConnect.propTypes,
  310. };
  311. CMFContainer.WrappedComponent = WrappedComponent;
  312. CMFContainer.getState = getState;
  313. // eslint-disable-next-line @typescript-eslint/default-param-last
  314. CMFContainer.setStateAction = function setStateAction(state, id = 'default', type) {
  315. if (typeof state !== 'function') {
  316. return getSetStateAction(state, id, type);
  317. }
  318. return (_, getReduxState) =>
  319. getSetStateAction(state(getState(getReduxState(), id)), id, type);
  320. };
  321. if (rest.forwardRef) {
  322. CMFWithRef = forwardRef(CMFWithRef);
  323. CMFWithRef.displayName = `ForwardRef(${CMFContainer.displayName})`;
  324. CMFWithRef.WrappedComponent = CMFContainer.WrappedComponent;
  325. CMFWithRef.getState = CMFContainer.getState;
  326. CMFWithRef.setStateAction = CMFContainer.setStateAction;
  327. }
  328. const Connected = connect(
  329. (state, ownProps) =>
  330. getStateToProps({
  331. componentId,
  332. defaultProps,
  333. defaultState,
  334. ownProps,
  335. state,
  336. mapStateToProps,
  337. WrappedComponent,
  338. }),
  339. (dispatch, ownProps) =>
  340. getDispatchToProps({
  341. defaultState,
  342. dispatch,
  343. componentId,
  344. mapDispatchToProps,
  345. ownProps,
  346. WrappedComponent,
  347. }),
  348. (stateProps, dispatchProps, ownProps) =>
  349. getMergeProps({
  350. mergeProps,
  351. stateProps,
  352. dispatchProps,
  353. ownProps,
  354. }),
  355. { ...rest },
  356. )(CMFWithRef);
  357. Connected.CMFContainer = CMFContainer;
  358. return Connected;
  359. };
  360. }
  361. cmfConnect.INJECTED_PROPS = CONST.INJECTED_PROPS;
  362. cmfConnect.INJECTED_STATE_PROPS = CONST.INJECTED_STATE_PROPS;
  363. cmfConnect.INJECTED_ROUTER_PROPS = CONST.INJECTED_ROUTER_PROPS;
  364. cmfConnect.ALL_INJECTED_PROPS = CONST.INJECTED_PROPS.concat(['getComponent', 'componentId']);
  365. cmfConnect.omit = omit;
  366. cmfConnect.omitAllProps = props => cmfConnect.omit(props, cmfConnect.ALL_INJECTED_PROPS);
  367. cmfConnect.propTypes = {
  368. state: ImmutablePropTypes.map,
  369. initialState: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.object]),
  370. getComponent: PropTypes.func,
  371. setState: PropTypes.func,
  372. initState: PropTypes.func,
  373. dispatchActionCreator: PropTypes.func,
  374. dispatch: PropTypes.func,
  375. };