From 8b7dab8fe15d6cbeff19745ec5e38bfcfde5713e Mon Sep 17 00:00:00 2001 From: Javi Velasco Date: Fri, 12 May 2017 12:46:24 +0200 Subject: [PATCH] Add mapThemrProps option --- README.md | 3 ++- src/components/themr.js | 49 +++++++++++++++++++++-------------- test/components/themr.spec.js | 44 +++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index afb5a09..232b5af 100644 --- a/README.md +++ b/README.md @@ -158,12 +158,13 @@ Makes available a `theme` context to use in styled components. The shape of the Returns a `function` to wrap a component and make it themeable. -The returned component accepts a `theme`, `composeTheme` and `innerRef` props apart from the props of the original component. They former two are used to provide a `theme` to the component and to configure the style composition, which can be configured via options too, while the latter is used to pass a ref callback to the decorated component. The function arguments are: +The returned component accepts a `theme`, `composeTheme`, `innerRef` and `mapThemrProps` props apart from the props of the original component. They former two are used to provide a `theme` to the component and to configure the style composition, which can be configured via options too. `innerRef` is used to pass a ref callback to the decorated component and `mapThemrProps` is a function that can be used to map properties to the decorated component. The function arguments are: - `Identifier` *(String)* used to provide a unique identifier to the component that will be used to get a theme from context. - `[defaultTheme]` (*Object*) is classname object resolved from CSS modules. It will be used as the default theme to calculate a new theme that will be passed to the component. - `[options]` (*Object*) If specified it allows to customize the behavior: - [`composeTheme = 'deeply'`] *(String)* allows to customize the way themes are merged or to disable merging completely. The accepted values are `deeply` to deeply merge themes, `softly` to softly merge themes and `false` to disable theme merging. + - [`mapThemrProps = (props, theme) => ({ ref, theme })`] *(Function)* allows to customize how properties are passed down to the decorated component. By default, themr extracts all own properties passing down just `innerRef` as `ref` and the generated theme as `theme`. If you are decorating a component that needs to map the reference or any other custom property, this function is called with *all* properties given to the component plus the generated `theme` in the second parameter. It should return the properties you want to pass. ## About diff --git a/src/components/themr.js b/src/components/themr.js index 3c7c309..3bde057 100644 --- a/src/components/themr.js +++ b/src/components/themr.js @@ -17,7 +17,8 @@ const COMPOSE_SOFTLY = 'softly' const DONT_COMPOSE = false const DEFAULT_OPTIONS = { - composeTheme: COMPOSE_DEEPLY + composeTheme: COMPOSE_DEEPLY, + mapThemrProps: defaultMapThemrProps } const THEMR_CONFIG = typeof Symbol !== 'undefined' ? @@ -32,7 +33,10 @@ const THEMR_CONFIG = typeof Symbol !== 'undefined' ? * @returns {function(ThemedComponent:Function):Function} - ThemedComponent */ export default (componentName, localTheme, options = {}) => (ThemedComponent) => { - const { composeTheme: optionComposeTheme } = { ...DEFAULT_OPTIONS, ...options } + const { + composeTheme: optionComposeTheme, + mapThemrProps: optionMapThemrProps + } = { ...DEFAULT_OPTIONS, ...options } validateComposeOption(optionComposeTheme) let config = ThemedComponent[THEMR_CONFIG] @@ -61,12 +65,14 @@ export default (componentName, localTheme, options = {}) => (ThemedComponent) => composeTheme: PropTypes.oneOf([ COMPOSE_DEEPLY, COMPOSE_SOFTLY, DONT_COMPOSE ]), innerRef: PropTypes.func, theme: PropTypes.object, - themeNamespace: PropTypes.string + themeNamespace: PropTypes.string, + mapThemrProps: PropTypes.func } static defaultProps = { ...ThemedComponent.defaultProps, - composeTheme: optionComposeTheme + composeTheme: optionComposeTheme, + mapThemrProps: optionMapThemrProps } constructor(...args) { @@ -106,14 +112,6 @@ export default (componentName, localTheme, options = {}) => (ThemedComponent) => : {} } - getPropsForComponent() { - //exclude themr-only props - //noinspection JSUnusedLocalSymbols - const { composeTheme, innerRef, themeNamespace, ...props } = this.props //eslint-disable-line no-unused-vars - - return props - } - getTheme(props) { return props.composeTheme === COMPOSE_SOFTLY ? { @@ -145,14 +143,10 @@ export default (componentName, localTheme, options = {}) => (ThemedComponent) => } render() { - const { innerRef } = this.props - const props = this.getPropsForComponent() - - return React.createElement(ThemedComponent, { - ...props, - ref: innerRef, - theme: this.theme_ - }) + return React.createElement( + ThemedComponent, + this.props.mapThemrProps(this.props, this.theme_) + ) } } @@ -283,3 +277,18 @@ function removeNamespace(key, themeNamespace) { const capitalized = key.substr(themeNamespace.length) return capitalized.slice(0, 1).toLowerCase() + capitalized.slice(1) } + +function defaultMapThemrProps(ownProps, theme) { + const { + composeTheme, //eslint-disable-line no-unused-vars + innerRef, + themeNamespace, //eslint-disable-line no-unused-vars + ...rest + } = ownProps + + return { + ...rest, + ref: innerRef, + theme + } +} diff --git a/test/components/themr.spec.js b/test/components/themr.spec.js index 049b607..430bf01 100644 --- a/test/components/themr.spec.js +++ b/test/components/themr.spec.js @@ -293,6 +293,50 @@ describe('Themr decorator function', () => { expect(spy.withArgs(stub).calledOnce).toBe(true) }) + it('allows to customize props passing using mapThemrProps from props', () => { + class Container extends Component { + render() { + return + } + } + + const spy = sinon.stub() + const hoc = C => ({ withRef, ...rest }) => () + const customMapper = (props, theme) => { + const { composeTheme, innerRef, mapThemrProps, themeNamespace, ...rest } = props //eslint-disable-line no-unused-vars + return { withRef: innerRef, theme, className: 'fooClass', ...rest } + } + const theme = {} + const DecoratedContainer = hoc(Container) + const ThemedDecoratedContainer = themr('Container', theme)(DecoratedContainer) + const tree = TestUtils.renderIntoDocument() + const stub = TestUtils.findRenderedComponentWithType(tree, Container) + expect(spy.withArgs(stub).calledOnce).toBe(true) + expect(stub.props).toMatch({ theme, className: 'fooClass' }) + }) + + it('allows to customize props passing using mapThemrProps from options', () => { + class Container extends Component { + render() { + return + } + } + + const spy = sinon.stub() + const hoc = C => ({ withRef, ...rest }) => () + const customMapper = (props, theme) => { + const { composeTheme, innerRef, mapThemrProps, themeNamespace, ...rest } = props //eslint-disable-line no-unused-vars + return { withRef: innerRef, theme, className: 'fooClass', ...rest } + } + const theme = {} + const DecoratedContainer = hoc(Container) + const ThemedDecoratedContainer = themr('Container', {}, { mapThemrProps: customMapper })(DecoratedContainer) + const tree = TestUtils.renderIntoDocument() + const stub = TestUtils.findRenderedComponentWithType(tree, Container) + expect(spy.withArgs(stub).calledOnce).toBe(true) + expect(stub.props).toMatch({ theme, className: 'fooClass' }) + }) + it('should throw if themeNamespace passed without theme', () => { const theme = { Container: { foo: 'foo_1234' } }