Skip to content

Commit

Permalink
Merge pull request #30 from mediamonks/feature/component-id-arg
Browse files Browse the repository at this point in the history
Allow passing a custom component id
  • Loading branch information
flut1 authored Dec 19, 2018
2 parents 254f3a1 + 70bf1b5 commit 7df79e7
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 4 deletions.
38 changes: 38 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down Expand Up @@ -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 }

Expand Down
35 changes: 35 additions & 0 deletions src/utils/optionalParamCurried.js
Original file line number Diff line number Diff line change
@@ -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;
9 changes: 5 additions & 4 deletions src/withInitAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -252,7 +253,7 @@ export default (p1, p2, p3) => {
ConnectedWithInit.initConfig = initConfig;

return ConnectedWithInit;
};
});
};

/**
Expand Down
33 changes: 33 additions & 0 deletions tests/utils/optionalParamCurried.spec.js
Original file line number Diff line number Diff line change
@@ -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();
}
});
});
});
51 changes: 51 additions & 0 deletions tests/withInitAction.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
})
});
});

0 comments on commit 7df79e7

Please sign in to comment.