diff --git a/docs/api.md b/docs/api.md index ebf005f..2af415d 100644 --- a/docs/api.md +++ b/docs/api.md @@ -97,6 +97,11 @@ An object with additional options. the component, no error will be thrown and initialization is automatically deferred to the client. This has no effect on any `clientOnly` init actions. _Defaults to `false`_ +### Returns `function` +{: .no_toc } +Returns a function that can be used to add the init action to a component. The function will +return a new component. See [Higher-Order Components](https://reactjs.org/docs/higher-order-components.html) + ### example {: .no_toc } @@ -136,6 +141,39 @@ export default withInitAction( ``` +## withInitAction `([initProps], initAction, [options])(componentId)(Component)` **advanced**{: .label .label-red} +{: #withInitActionCustom } +You can pass a custom component id to `withInitAction`. Normally the component id is derived from +the `displayName` of the component (or if that does not exist, the `.name` property). This id is +supposed to be unique, because it is used internally to reference which components have been prepared. +If for whatever reason your component does not have a name or does not have a unique name, you can +use this syntax of `withInitAction` to pass a custom name. + +API is otherwise identical to [withInitAction](#withInitAction) + +### example +{: .no_toc } + +``` jsx +class Post extends React.Component { + // ... +} +``` +```jsx +// Regular usage, component id will be 'Post' +export default withInitAction( + (props, dispatch) => dispatch(loadPostData()), + { initSelf: INIT_SELF_BLOCKING } +)(Post); +``` +```jsx +// Custom component id, component id will be 'Custom' +export default withInitAction( + (props, dispatch) => dispatch(loadPostData()), + { initSelf: INIT_SELF_BLOCKING } +)('Custom')(Post); +``` + ## prepareComponent `(Component, props)` {: #prepareComponent } diff --git a/src/utils/optionalParamCurried.js b/src/utils/optionalParamCurried.js new file mode 100644 index 0000000..77602ef --- /dev/null +++ b/src/utils/optionalParamCurried.js @@ -0,0 +1,35 @@ +/** + * Wraps a function to create a curried argument that can be passed optionally. + * + * @param optionalParamType {string} The type of the optional parameter. If the wrapped function + * recieves an argument of this type, it will return a new function that can be called to pass + * the other arguments. If it doesn't match, the optional argument is considered undefined and + * the inner function `fn` is executed immediately with undefined as the first argument. + * @param fn A function that will receive the optional curried argument in the first parameter. + * @returns {Function} + * + * @example + * const test = optionalParamCurried( + * 'string', + * (opt, a, b) => console.log({ opt, a, b }) + * ); + * + * // intended usage: + * test('abc')(1, 2); // logs { opt: 'abc', a: 1, b: 2 } + * test(123, 456); // logs { opt: undefined, a: 123, b: 456 } + * + * // incorrect usage: + * test(1, 2, 3); // redundant 3rd argument. logs { opt: undefined, a: 1, b: 2 } + * test('abc', 1, 2); // does nothing (returns function) + * + */ +const optionalParamCurried = (optionalParamType, fn) => (firstArg, ...restArgs) => { + // eslint-disable-next-line valid-typeof + if (typeof firstArg === optionalParamType) { + return (...args) => fn(firstArg, ...args); + } + + return fn(undefined, firstArg, ...restArgs); +}; + +export default optionalParamCurried; diff --git a/src/withInitAction.js b/src/withInitAction.js index 69eda65..fe6f63f 100644 --- a/src/withInitAction.js +++ b/src/withInitAction.js @@ -8,6 +8,7 @@ import initComponent from './actions/initComponent'; import { MODE_INIT_SELF } from './initMode'; import { INIT_SELF_ASYNC, INIT_SELF_UNMOUNT, INIT_SELF_BLOCKING } from './initSelfMode'; import createPrepareKey from './utils/createPrepareKey'; +import optionalParamCurried from './utils/optionalParamCurried'; const componentIds = []; @@ -108,15 +109,15 @@ export default (p1, p2, p3) => { allowLazy = false, } = options; - return WrappedComponent => { - const componentId = WrappedComponent.displayName || WrappedComponent.name; + return optionalParamCurried('string', (customComponentId, WrappedComponent) => { + const componentId = customComponentId || WrappedComponent.displayName || WrappedComponent.name; if (!componentId) { throw new Error('withInitAction() HoC requires the wrapped component to have a displayName'); } // only warn for unique displayName when we do not have a custom getPrepareKey function if (getPrepareKey === createPrepareKey && componentIds.includes(componentId)) { console.warn( - `Each Component passed to withInitAction() should have a unique displayName. Found duplicate name "${componentId}"`, + `Each Component passed to withInitAction() should have a unique id. Found duplicate name "${componentId}\n Consider passing a custom id, see: https://mediamonks.github.io/react-redux-component-init/api.html#withInitActionCustom"`, ); } componentIds.push(componentId); @@ -252,7 +253,7 @@ export default (p1, p2, p3) => { ConnectedWithInit.initConfig = initConfig; return ConnectedWithInit; - }; + }); }; /** diff --git a/tests/utils/optionalParamCurried.spec.js b/tests/utils/optionalParamCurried.spec.js new file mode 100644 index 0000000..4115a7c --- /dev/null +++ b/tests/utils/optionalParamCurried.spec.js @@ -0,0 +1,33 @@ +import optionalParamCurried from '../../src/utils/optionalParamCurried'; + +describe('optionalParamCurried', () => { + describe('with optionalParamType "string"', () => { + it('passes undefined if first arg is of type "number"', () => { + { + const spy = jest.fn(); + const testFn = optionalParamCurried('string', spy); + + testFn(1, 2, 3) + expect(spy).toHaveBeenCalledWith(undefined, 1, 2, 3); + } + }); + it('passes the curried argument if it is of type "string"', () => { + { + const spy = jest.fn(); + const testFn = optionalParamCurried('string', spy); + + testFn('foo')(1, 2, 3); + expect(spy).toHaveBeenCalledWith('foo', 1, 2, 3); + } + }); + it('does not execute the inner function if only the optional param is passed', () => { + { + const spy = jest.fn(); + const testFn = optionalParamCurried('string', spy); + + testFn('foo', 1, 2, 3); + expect(spy).not.toHaveBeenCalled(); + } + }); + }); +}); diff --git a/tests/withInitAction.spec.js b/tests/withInitAction.spec.js index 17bf716..191b6c7 100644 --- a/tests/withInitAction.spec.js +++ b/tests/withInitAction.spec.js @@ -288,4 +288,55 @@ describe('withInitAction', () => { ); expect(WithInit.initConfig.initProps).toEqual(['a', 'b', 'c']); }); + + + + describe('with two components with the same name', () => { + describe('with a unique custom component id', () => { + it('does not warn and stores the custom id', () => { + clearComponentIds(); + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + const WithInit = withInitAction( + ['a', 'b', 'c'], + () => Promise.resolve() + )('CustomName')(SimpleInitTestComponent); + + // eslint-disable-next-line no-unused-vars + const WithInit2 = withInitAction( + ['a', 'b', 'c'], + () => Promise.resolve() + )(SimpleInitTestComponent); + + expect(consoleWarnSpy).not.toHaveBeenCalled(); + + consoleWarnSpy.mockRestore(); + + expect(WithInit.initConfig.componentId).toBe('CustomName'); + }); + }); + + describe('without a custom component id', () => { + it('warns about duplicate component ids', () => { + clearComponentIds(); + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + // eslint-disable-next-line no-unused-vars + const WithInit = withInitAction( + ['a', 'b', 'c'], + () => Promise.resolve() + )(SimpleInitTestComponent); + + // eslint-disable-next-line no-unused-vars + const WithInit2 = withInitAction( + ['a', 'b', 'c'], + () => Promise.resolve() + )(SimpleInitTestComponent); + + expect(consoleWarnSpy).toHaveBeenCalled(); + + consoleWarnSpy.mockRestore(); + }); + }) + }); });