Skip to content

Commit

Permalink
Add mapThemrProps option (#59)
Browse files Browse the repository at this point in the history
  • Loading branch information
javivelasco authored May 12, 2017
1 parent b7fec0d commit 4dbb6b6
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 21 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 29 additions & 20 deletions src/components/themr.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' ?
Expand All @@ -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]
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
? {
Expand Down Expand Up @@ -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_)
)
}
}

Expand Down Expand Up @@ -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
}
}
44 changes: 44 additions & 0 deletions test/components/themr.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Passthrough {...this.props} />
}
}

const spy = sinon.stub()
const hoc = C => ({ withRef, ...rest }) => (<C ref={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(<ThemedDecoratedContainer innerRef={spy} mapThemrProps={customMapper} />)
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 <Passthrough {...this.props} />
}
}

const spy = sinon.stub()
const hoc = C => ({ withRef, ...rest }) => (<C ref={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(<ThemedDecoratedContainer innerRef={spy} />)
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' } }

Expand Down

0 comments on commit 4dbb6b6

Please sign in to comment.