Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add tests #9

Merged
merged 6 commits into from Feb 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Expand Up @@ -136,6 +136,30 @@ export { ThemeProvider, withTheme };
import { ThemeProvider, withTheme } from './theming';
```

## Applying a custom theme to a component
If you want to change the theme for a certain component, you can directly pass the theme prop to the component. The theme passed as the prop is merged with the theme from the Provider.

```js
import * as React from 'react';
import MyButton from './MyButton';

export default function ButtonExample() {
return (
<MyButton theme={{ roundness: 3 }}>
Press me
</MyButton>
);
}
```

## Gotchas
The `ThemeProvider` exposes the theme to the components via [React's context API](https://reactjs.org/docs/context.html),
which means that the component must be in the same tree as the `ThemeProvider`. Some React Native components will render a
different tree such as a `Modal`, in which case the components inside the `Modal` won't be able to access the theme. The work
around is to get the theme using the `withTheme` HOC and pass it down to the components as props, or expose it again with the
exported `ThemeProvider` component.


[build-badge]: https://img.shields.io/circleci/project/github/callstack/react-theme-provider/master.svg?style=flat-square
[build]: https://circleci.com/gh/callstack/react-theme-provider
[version-badge]: https://img.shields.io/npm/v/@callstack/react-theme-provider.svg?style=flat-square
Expand Down
1 change: 1 addition & 0 deletions __mocks__/styleMock.js
@@ -0,0 +1 @@
module.exports = {};
9 changes: 0 additions & 9 deletions examples/web/src/App.test.js

This file was deleted.

6 changes: 5 additions & 1 deletion examples/web/src/ThemeChanger.js
Expand Up @@ -6,7 +6,11 @@ const Header = ({ theme, themes, onChangeTheme }) => (
<Container textColor={theme.primaryColor} background={theme.secondaryColor}>
CHANGE THEME:{' '}
<select onChange={e => onChangeTheme(e.target.value)}>
{themes.map(themeName => <option value={themeName}>{themeName}</option>)}
{themes.map(themeName => (
<option key={themeName} value={themeName}>
{themeName}
</option>
))}
</select>
</Container>
);
Expand Down
4 changes: 2 additions & 2 deletions examples/web/src/theming.js
@@ -1,7 +1,7 @@
/* @flow */
import { createTheming } from 'react-theme-provider';
import { createTheming } from '@callstack/react-theme-provider';

import type { ThemingType } from 'react-theme-provider';
import type { ThemingType } from '@callstack/react-theme-provider';

export type Theme = {
primaryColor: string,
Expand Down
15 changes: 12 additions & 3 deletions package.json
Expand Up @@ -13,8 +13,8 @@
"build:standalonedev": "cross-env NODE_ENV=development BABEL_TARGET=rollup rollup -c",
"build": "rm -rf dist && rm -rf lib && npm run build:standalone && npm run build:babel",
"prepublish": "npm run build",
"test": "echo \"Error: no test specified\" && exit 1",
"example": "yarn link && cd examples/web && yarn link react-theme-provider && yarn start"
"test": "jest",
"example": "yarn link && cd examples/web && yarn link @callstack/react-theme-provider && yarn start"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -44,8 +44,10 @@
"eslint": "^4.16.0",
"eslint-config-callstack-io": "^1.1.1",
"flow-bin": "^0.63.0",
"jest": "^22.4.0",
"prettier": "^1.7.4",
"react": "^16.0.0",
"react-dom": "^16.2.0",
"rollup": "^0.55.2",
"rollup-plugin-babel": "^3.0.3",
"rollup-plugin-commonjs": "^8.3.0",
Expand All @@ -58,7 +60,14 @@
"react": "^15.3.0 || ^16.0.0"
},
"dependencies": {
"deepmerge": "^2.0.1",
"hoist-non-react-statics": "^2.5.0",
"lodash.isequal": "^4.5.0",
"lodash.merge": "^4.6.1",
"prop-types": "^15.6.0"
},
"jest": {
"moduleNameMapper": {
"\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js"
}
}
}
5 changes: 5 additions & 0 deletions src/__tests__/.eslintrc
@@ -0,0 +1,5 @@
{
"env": {
"jest": true
}
}
97 changes: 97 additions & 0 deletions src/__tests__/createTheming.test.js
@@ -0,0 +1,97 @@
import React from 'react';
import ReactDOM from 'react-dom';
import createTheming from '../createTheming';

describe('createTheming', () => {
const node = document.createElement('div');

afterEach(() => {
ReactDOM.unmountComponentAtNode(node);
});

const darkTheme = {
primaryColor: '#FFA72A',
accentColor: '#458622',
backgroundColor: '#504f4d',
textColor: '#FFC777',
secondaryColor: '#252525',
};

const lightTheme = {
primaryColor: '#ffcaaa',
accentColor: '#45ffaa',
backgroundColor: '#aaffcf',
textColor: '#FFa7af',
secondaryColor: '#ffffff',
};

const { withTheme, ThemeProvider } = createTheming(darkTheme);

it('provides { theme } props', () => {
const PropsChecker = withTheme(({ theme }) => {
expect(typeof theme).toBe('object');
expect(theme).toEqual(darkTheme);
return null;
});

ReactDOM.render(
<ThemeProvider>
<PropsChecker />
</ThemeProvider>,
node
);
});

it('hoists non-react statics from the wrapped component', () => {
class Component extends React.Component {
static foo() {
return 'bar';
}

render() {
return null;
}
}
Component.hello = 'world';

const decorated = withTheme(Component);

expect(decorated.hello).toBe('world');
expect(typeof decorated.foo).toBe('function');
expect(decorated.foo()).toBe('bar');
});

it('render ThemeProvider multiple times', () => {
const {
ThemeProvider: DarkThemeProvider,
withTheme: withDarkTheme,
} = createTheming(darkTheme);
const {
ThemeProvider: LightThemeProvider,
withTheme: withLightTheme,
} = createTheming({});

const DarkPropsChecker = withDarkTheme(({ theme }) => {
expect(typeof theme).toBe('object');
expect(theme).toEqual(darkTheme);
return null;
});

const LightPropsChecker = withLightTheme(({ theme }) => {
expect(typeof theme).toBe('object');
expect(theme).toEqual(lightTheme);
return null;
});

ReactDOM.render(
<DarkThemeProvider>
<LightThemeProvider theme={lightTheme}>
<LightPropsChecker />
</LightThemeProvider>

<DarkPropsChecker />
</DarkThemeProvider>,
node
);
});
});
5 changes: 3 additions & 2 deletions src/createThemeProvider.js
Expand Up @@ -2,6 +2,7 @@

import * as React from 'react';
import PropTypes from 'prop-types';
import isEqual from 'lodash.isequal';

type ThemeProviderProps<T> = {
children?: any,
Expand Down Expand Up @@ -35,7 +36,7 @@ function createThemeProvider<T>(
}

componentWillReceiveProps(nextProps: *) {
if (this.props.theme !== nextProps.theme) {
if (!isEqual(this.props.theme, nextProps.theme)) {
this._subscriptions.forEach(cb => cb(nextProps.theme));
}
}
Expand All @@ -58,7 +59,7 @@ function createThemeProvider<T>(
_get = () => this.props.theme;

render() {
return React.Children.only(this.props.children);
return this.props.children;
}
};
}
Expand Down
35 changes: 14 additions & 21 deletions src/createWithTheme.js
Expand Up @@ -2,13 +2,20 @@

import * as React from 'react';
import PropTypes from 'prop-types';
import merge from 'deepmerge';
import merge from 'lodash.merge';
import isEqual from 'lodash.isequal';
import hoistNonReactStatics from 'hoist-non-react-statics';

type withThemeRetunType<Theme, Props: {}> = React.ComponentType<
React.ElementConfig<React.ComponentType<$Diff<Props, { theme: Theme }>>>
>;

const isClassComponent = (Component: Function) => !!Component.prototype.render;
const isClassComponent = (Component: Function) =>
Boolean(
Component &&
Component.prototype &&
typeof Component.prototype.render === 'function'
);

export type WithThemeType<T> = <Props: {}>(
Comp: React.ComponentType<Props>
Expand Down Expand Up @@ -58,7 +65,7 @@ const createWithTheme = <T>(
}

componentWillReceiveProps(nextProps: *) {
if (this.props.theme !== nextProps.theme) {
if (!isEqual(this.props.theme, nextProps.theme)) {
this.setState({
theme: this._merge(
this.context[channel] && this.context[channel].get(),
Expand All @@ -75,7 +82,9 @@ const createWithTheme = <T>(
_merge = (theme: T, props: *) =>
// Only merge if both theme from context and props are present
// Avoiding unnecessary merge allows us to check equality by reference
theme && props.theme ? merge(theme, props.theme) : theme || props.theme;
theme && props.theme
? merge({}, theme, props.theme)
: theme || props.theme;

_subscription: { remove: Function };
_root: any;
Expand Down Expand Up @@ -134,23 +143,7 @@ const createWithTheme = <T>(
}
}

// This is ugly, but we need to hoist static properties manually
for (const prop in Comp) {
if (prop !== 'displayName' && prop !== 'contextTypes') {
if (prop === 'propTypes') {
// Only the underlying component will receive the theme prop
/* $FlowFixMe */
const { theme, ...propTypes } = Comp[prop]; // eslint-disable-line no-shadow, no-unused-vars
/* $FlowFixMe */
ThemedComponent[prop] = propTypes;
} else {
/* $FlowFixMe */
ThemedComponent[prop] = Comp[prop];
}
}
}

return ThemedComponent;
return hoistNonReactStatics(ThemedComponent, Comp);
};

export default createWithTheme;