From c641acdc9e04da5dc9476e3db9245c87f6e6608e Mon Sep 17 00:00:00 2001 From: Javi Velasco Date: Tue, 2 Aug 2016 11:29:12 +0200 Subject: [PATCH 1/2] Allow to retrieve the wrapped instance as react-redux does --- package.json | 3 +++ src/components/themr.js | 43 ++++++++++++++++++++++------- test/components/themr.spec.js | 51 +++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index e9e6e24..ec4bf03 100644 --- a/package.json +++ b/package.json @@ -56,5 +56,8 @@ "license": "MIT", "peerDependencies": { "react": "^0.14.0 || ^15.0.0-0" + }, + "dependencies": { + "invariant": "^2.2.1" } } diff --git a/src/components/themr.js b/src/components/themr.js index 0673342..1ea0ca7 100644 --- a/src/components/themr.js +++ b/src/components/themr.js @@ -1,15 +1,17 @@ import React, { Component, PropTypes } from 'react' +import invariant from 'invariant' const COMPOSE_DEEPLY = 'deeply' const COMPOSE_SOFTLY = 'softly' const DONT_COMPOSE = false const DEFAULT_OPTIONS = { - composeTheme: COMPOSE_DEEPLY + composeTheme: COMPOSE_DEEPLY, + withRef: false } -export default (componentName, localTheme, options = DEFAULT_OPTIONS) => (ThemedComponent) => { - const { composeTheme: optionComposeTheme } = options +export default (componentName, localTheme, options = {}) => (ThemedComponent) => { + const { composeTheme: optionComposeTheme, withRef: optionWithRef } = { ...DEFAULT_OPTIONS, ...options } validateComposeOption(optionComposeTheme) return class Themed extends Component { static displayName = `Themed ${ThemedComponent.name}`; @@ -27,6 +29,15 @@ export default (componentName, localTheme, options = DEFAULT_OPTIONS) => (Themed composeTheme: optionComposeTheme } + getWrappedInstance() { + invariant(optionWithRef, + 'To access the wrapped instance, you need to specify ' + + '{ withRef: true } as the third argument of the themr() call.' + ) + + return this.refs.wrappedInstance + } + getThemeNotComposed() { if (this.props.theme) return this.props.theme if (localTheme) return localTheme @@ -47,12 +58,26 @@ export default (componentName, localTheme, options = DEFAULT_OPTIONS) => (Themed render() { const { composeTheme, ...rest } = this.props - return React.createElement(ThemedComponent, { - ...rest, - theme: composeTheme - ? this.getTheme() - : this.getThemeNotComposed() - }) + let renderedElement + + if (optionWithRef) { + renderedElement = React.createElement(ThemedComponent, { + ...rest, + ref: 'wrappedInstance', + theme: composeTheme + ? this.getTheme() + : this.getThemeNotComposed() + }) + } else { + renderedElement = React.createElement(ThemedComponent, { + ...rest, + theme: composeTheme + ? this.getTheme() + : this.getThemeNotComposed() + }) + } + + return renderedElement } } } diff --git a/test/components/themr.spec.js b/test/components/themr.spec.js index 0990fc7..1114292 100644 --- a/test/components/themr.spec.js +++ b/test/components/themr.spec.js @@ -273,4 +273,55 @@ describe('Themr decorator function', () => { const stub = TestUtils.findRenderedComponentWithType(tree, Passthrough) expect(stub.props.theme).toEqual({}) }) + + it('should throw when trying to access the wrapped instance if withRef is not specified', () => { + const theme = { Container: { foo: 'foo_1234' } } + + @themr('Container') + class Container extends Component { + render() { + return + } + } + + const tree = TestUtils.renderIntoDocument( + + + + ) + + const container = TestUtils.findRenderedComponentWithType(tree, Container) + expect(() => container.getWrappedInstance()).toThrow( + /To access the wrapped instance, you need to specify \{ withRef: true \} as the third argument of the themr\(\) call\./ + ) + }) + + it('should return the instance of the wrapped component for use in calling child methods', () => { + const someData = { + some: 'data' + } + + class Container extends Component { + someInstanceMethod() { + return someData + } + + render() { + return + } + } + + const decorator = themr('Component', null, { withRef: true }) + const Decorated = decorator(Container) + + const tree = TestUtils.renderIntoDocument( + + ) + + const decorated = TestUtils.findRenderedComponentWithType(tree, Decorated) + + expect(() => decorated.someInstanceMethod()).toThrow() + expect(decorated.getWrappedInstance().someInstanceMethod()).toBe(someData) + expect(decorated.refs.wrappedInstance.someInstanceMethod()).toBe(someData) + }) }) From e4c087e010e1d5bf0cdd73bbe845573ce44e759d Mon Sep 17 00:00:00 2001 From: Javi Velasco Date: Tue, 2 Aug 2016 11:40:16 +0200 Subject: [PATCH 2/2] Update docs --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7635d32..356f1f0 100644 --- a/README.md +++ b/README.md @@ -162,10 +162,9 @@ The returned component accepts a `theme` and `composeTheme` apart from the prop - `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*) is an option object that for now only accepts one value: `composeTheme` which accepts: - - `deeply` to deeply merge themes. - - `softly` to softly merge themes. - - `false` to disable theme merging. +- `[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. + - [`withRef = false`] *(Boolean)* if true, stores a ref to the wrapped component instance and makes it available via `getWrappedInstance()` method. Defaults to false. ## About