-
Notifications
You must be signed in to change notification settings - Fork 960
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(theme): consumer component props forward
- Loading branch information
Artur Yorsh
committed
Nov 6, 2018
1 parent
f1cccd6
commit c14a5f1
Showing
10 changed files
with
296 additions
and
73 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export { ThemeProvider } from './theme-provider.component'; | ||
export { | ||
withTheme, | ||
ThemedComponentProps, | ||
} from './theme-consumer.component'; |
56 changes: 56 additions & 0 deletions
56
src/framework/theme/component/theme-consumer.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import React from 'react'; | ||
import { | ||
Consumer, | ||
forwardProps, | ||
} from '../service'; | ||
|
||
export interface ThemedComponentProps<T> extends React.ClassAttributes<T> { | ||
theme: Object; | ||
} | ||
|
||
export function withTheme<T extends ThemedComponentProps<T>>(Component: React.ComponentType<T>) { | ||
type TExcept = Exclude<keyof T, keyof ThemedComponentProps<T>>; | ||
type ForwardedProps = Pick<T, TExcept>; | ||
|
||
class ConsumingComponent extends React.Component<ForwardedProps, {}> { | ||
rootComponentRef = undefined; | ||
|
||
getConsumingComponent = undefined; | ||
|
||
constructor(props) { | ||
super(props); | ||
this.setRootComponentRef = this.setRootComponentRef.bind(this); | ||
} | ||
|
||
setRootComponentRef(ref) { | ||
this.rootComponentRef = ref; | ||
} | ||
|
||
renderRootComponent(component: React.ComponentType<T>, theme: Object) { | ||
const RootComponent = component; | ||
return ( | ||
<RootComponent | ||
ref={this.setRootComponentRef} | ||
theme={theme} | ||
{...this.props} | ||
/> | ||
); | ||
} | ||
|
||
render() { | ||
return ( | ||
<Consumer> | ||
{theme => this.renderRootComponent(Component, theme)} | ||
</Consumer> | ||
); | ||
} | ||
} | ||
|
||
const ResultComponent = ConsumingComponent; | ||
ResultComponent.prototype.getConsumingComponent = function getWrappedInstance() { | ||
const hasWrappedInstance = this.rootComponentRef.getConsumingComponent; | ||
return hasWrappedInstance ? this.rootComponentRef.getConsumingComponent() : this.rootComponentRef; | ||
}; | ||
|
||
return forwardProps(ResultComponent, Component); | ||
} |
7 changes: 3 additions & 4 deletions
7
...mework/theme/primitives/themeProvider.tsx → ...me/component/theme-provider.component.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1 @@ | ||
import ThemeProvider from './primitives/themeProvider'; | ||
import { | ||
withTheme, | ||
WithThemeProps, | ||
} from './primitives/themeConsumer'; | ||
|
||
export { | ||
ThemeProvider, | ||
withTheme, | ||
WithThemeProps, | ||
}; | ||
export * from './component'; |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { Consumer, Provider } from './react-context.service'; | ||
export { forwardProps } from './react-props-mapping.service'; |
File renamed without changes.
62 changes: 62 additions & 0 deletions
62
src/framework/theme/service/react-props-mapping.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import React, { ComponentType } from 'react'; | ||
|
||
const REACT_METHODS = [ | ||
'autobind', | ||
'childContextTypes', | ||
'componentDidMount', | ||
'componentDidUpdate', | ||
'componentWillMount', | ||
'componentWillReceiveProps', | ||
'componentWillUnmount', | ||
'componentWillUpdate', | ||
'contextTypes', | ||
'displayName', | ||
'getChildContext', | ||
'getDefaultProps', | ||
'getDOMNode', | ||
'getInitialState', | ||
'mixins', | ||
'propTypes', | ||
'render', | ||
'replaceProps', | ||
'setProps', | ||
'shouldComponentUpdate', | ||
'statics', | ||
'updateComponent', | ||
]; | ||
|
||
export function forwardProps<P>(Source: ComponentType<P>, Target: ComponentType<P>): ComponentType<P> { | ||
function filterProps(prop) { | ||
// React specific methods and properties or properties from React's prototype | ||
const isReactProp = REACT_METHODS.includes(prop) || prop in React.Component.prototype; | ||
// Properties from enhanced component's prototype | ||
const isTargetProp = prop in Target.prototype; | ||
// Private methods | ||
const isPrivateProp = prop.startsWith('_'); | ||
|
||
return !(isReactProp || isTargetProp || isPrivateProp); | ||
} | ||
|
||
function mapProps(prop) { | ||
if (typeof Source.prototype[prop] === 'function') { | ||
// Make sure the function is called with correct context | ||
Target.prototype[prop] = function (...args) { | ||
return Source.prototype[prop].apply(this.getConsumingComponent(), args); | ||
}; | ||
} else { | ||
// Copy properties as getters and setters | ||
Object.defineProperty(Target.prototype, prop, { | ||
get() { | ||
return this.getConsumingComponent()[prop]; | ||
}, | ||
set(value) { | ||
this.getConsumingComponent()[prop] = value; | ||
}, | ||
}); | ||
} | ||
} | ||
|
||
Object.getOwnPropertyNames(Source.prototype).filter(filterProps).forEach(mapProps); | ||
|
||
return Target; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,48 +1,95 @@ | ||
import React from 'react'; | ||
import { View } from 'react-native'; | ||
import { render } from 'react-native-testing-library'; | ||
import { | ||
View, | ||
TouchableOpacity, | ||
ViewProps, | ||
ViewStyle, | ||
} from 'react-native'; | ||
import { | ||
fireEvent, | ||
render, | ||
} from 'react-native-testing-library'; | ||
import { | ||
ThemeProvider, | ||
withTheme, | ||
WithThemeProps, | ||
} from './index'; | ||
} from './component'; | ||
|
||
interface TestProps extends WithThemeProps { | ||
testId: string; | ||
const touchableTestId = '@theme/touchable'; | ||
const consumerTestId = '@theme/consumer'; | ||
|
||
interface ProviderComponentProps extends ViewProps { | ||
onPress?: () => void; | ||
} | ||
|
||
class TestComponent extends React.Component<TestProps> { | ||
class ProviderComponent extends React.Component<ProviderComponentProps> { | ||
themedComponentRef = undefined; | ||
|
||
constructor(props) { | ||
super(props); | ||
this.onThemedComponentPress = this.onThemedComponentPress.bind(this); | ||
} | ||
|
||
onThemedComponentPress() { | ||
this.themedComponentRef.setStyle({}); | ||
this.props.onPress(); | ||
} | ||
|
||
setThemedComponentRef(ref) { | ||
this.themedComponentRef = ref; | ||
} | ||
|
||
render() { | ||
const ThemedComponent = withTheme(ConsumerComponent); | ||
return ( | ||
<View testID={this.props.testId}/> | ||
<ThemeProvider theme={{}}> | ||
<TouchableOpacity | ||
testID={touchableTestId} | ||
onPress={this.onThemedComponentPress}> | ||
<ThemedComponent | ||
testID={consumerTestId} | ||
ref={(ref) => this.setThemedComponentRef(ref)} | ||
/> | ||
</TouchableOpacity> | ||
</ThemeProvider> | ||
); | ||
} | ||
} | ||
|
||
it('Checks theme consumer renders properly', async () => { | ||
const ThemedComponent = withTheme(TestComponent); | ||
const themedComponentTestId = '@theme/root'; | ||
class ConsumerComponent extends React.Component<any> { | ||
setStyle(style: ViewStyle) { | ||
this.setState({ style }); | ||
} | ||
|
||
render() { | ||
return ( | ||
<View {...this.props}/> | ||
); | ||
} | ||
} | ||
|
||
it('Checks theme consumer renders properly', async () => { | ||
const component = render( | ||
<ThemeProvider theme={{}}> | ||
<ThemedComponent testId={themedComponentTestId}/> | ||
</ThemeProvider>, | ||
<ProviderComponent/>, | ||
); | ||
|
||
const themedComponent = component.getByTestId(themedComponentTestId); | ||
expect(themedComponent).not.toBeNull(); | ||
const consumerComponent = component.getByTestId(consumerTestId); | ||
expect(consumerComponent).not.toBeNull(); | ||
}); | ||
|
||
it('Checks theme consumer receives theme prop', async () => { | ||
const ThemedComponent = withTheme(TestComponent); | ||
const themedComponentTestId = '@theme/root'; | ||
|
||
const component = render( | ||
<ThemeProvider theme={{}}> | ||
<ThemedComponent testId={themedComponentTestId}/> | ||
</ThemeProvider>, | ||
<ProviderComponent/>, | ||
); | ||
const consumerComponent = component.getByTestId(consumerTestId); | ||
expect(consumerComponent.props.theme).not.toBeNull(); | ||
}); | ||
|
||
const themedComponent = component.getByTestId(themedComponentTestId); | ||
expect(themedComponent.props.theme).not.toBeNull(); | ||
it(`Checks root component has consumers's ref`, async () => { | ||
const onPress = jest.fn(); | ||
const component = render( | ||
<ProviderComponent onPress={onPress}/>, | ||
); | ||
const touchableComponent = component.getByTestId(touchableTestId); | ||
fireEvent.press(touchableComponent); | ||
expect(onPress).toHaveBeenCalled(); | ||
}); | ||
|
Oops, something went wrong.