Skip to content

Commit

Permalink
feat(theme): consumer component props forward
Browse files Browse the repository at this point in the history
  • Loading branch information
Artur Yorsh committed Nov 6, 2018
1 parent f1cccd6 commit c14a5f1
Show file tree
Hide file tree
Showing 10 changed files with 296 additions and 73 deletions.
5 changes: 5 additions & 0 deletions src/framework/theme/component/index.ts
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 src/framework/theme/component/theme-consumer.component.tsx
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);
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import React from 'react';
import { Provider } from './createContext';
import { Provider } from '../service';

interface PropsType {
interface ProviderProps {
children: JSX.Element;
theme: Object;
}

export default class ThemeProvider extends React.PureComponent<PropsType> {

export class ThemeProvider extends React.PureComponent<ProviderProps> {
static defaultProps = {
theme: {},
};
Expand Down
12 changes: 1 addition & 11 deletions src/framework/theme/index.ts
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';
23 changes: 0 additions & 23 deletions src/framework/theme/primitives/themeConsumer.tsx

This file was deleted.

2 changes: 2 additions & 0 deletions src/framework/theme/service/index.ts
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';
62 changes: 62 additions & 0 deletions src/framework/theme/service/react-props-mapping.service.ts
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;
}
97 changes: 72 additions & 25 deletions src/framework/theme/theme.spec.tsx
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();
});

Loading

0 comments on commit c14a5f1

Please sign in to comment.