Skip to content

Commit d3df699

Browse files
committed
Add README and finish initial implementation 🙌
1 parent ab80dc2 commit d3df699

File tree

4 files changed

+330
-37
lines changed

4 files changed

+330
-37
lines changed

‎README.md

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,174 @@
11
# React CSS Themr
2+
3+
Easy theming and composition for CSS Modules.
4+
5+
```
6+
$ npm install --save react-css-themr
7+
```
8+
9+
**Note: Feedback and contributions on the docs are highly appreciated. Please open issues for comments of reach me.**
10+
11+
## Why?
12+
13+
When you use [CSS Modules](https://github.com/css-modules/css-modules) to style your components, a classnames object is usually imported from the same component. Since css classes are scoped by default, there is no easy way to make your component customizable for the outside world.
14+
15+
## The approach
16+
17+
Taking ideas from [future-react-ui](https://github.com/nikgraf/future-react-ui) and [react-themeable](https://github.com/markdalgleish/react-themeable), a component should be shipped without styles. This means we can consider the styles as an injectable dependency. In CSS Modules you can consider the imported classnames object as a theme for a component. Therefore, every styled component should define a classname API to be used in the rendering function.
18+
19+
The most immediate way of providing a classname object is via props. In case you want to import a component with a theme already injected, you have to write a higher order component that does the job. This is ok for your own components, but for ui-kits like [React Toolbox](www.react-toolbox.com) or [Belle](http://http://nikgraf.github.io/belle/), you'd have to write a wrapper for every single component you want to use. In this fancy, you can understand the theme as a set of related classname objects for different components. It makes sense to group them together in a single object and move it through the component tree using a context. This way, you can provide a theme either via context, hoc or props.
20+
21+
The approach of react-css-themr consists of a provider and a decorator. The provider sets a context theme. The decorator adds to your components the logic to figure out which theme should be used, depending on configuration, context and props.
22+
23+
## Combining CSS modules
24+
25+
There are three possible sources for your component. Sorted by priority: context, configuration and props. Any of them can be missing. In case multiple themes are present, you may want to compose the final classnames object in three different ways:
26+
27+
- Override: the theme object with the highest priority is the one used.
28+
- Softly merging: theme objects are merged but if a key is present in more than one object, the final value corresponds to the theme with highest priority.
29+
- Deeply merging: theme objects are merged and if a key is present in more than one object, the values for each objects are concatenated.
30+
31+
You can choose whatever you want. We consider the last one as the most flexible so it's selected by default.
32+
33+
## How does it work?
34+
35+
Say you have a `Button` component you want to make themeable. You should pass a unique name identifier that will be used to retrieve its theme from context in case it is present.
36+
37+
```jsx
38+
// Button.js
39+
import React, { Component } from 'react';
40+
import { themr } from 'react-themr';
41+
42+
@themr('MyThemedButton')
43+
class Button extends Component {
44+
render() {
45+
const { theme, icon, children } = this.props;
46+
return (
47+
<button className={theme.button}>
48+
{ icon ? <i className={theme.icon}>{icon}</i> : null}
49+
<span className={theme.content}>{children}</span>
50+
</button>
51+
)
52+
}
53+
}
54+
55+
export default Button;
56+
```
57+
58+
The component is defining an API for theming that consists of three classnames: button, icon and content. Now, a component can use a button with a success theme like:
59+
60+
```jsx
61+
import Button from './Button';
62+
import successTheme from './SuccessButton.css';
63+
64+
export default (props) => (
65+
<div {...props}>
66+
<p>Do you like it?</p>
67+
<Button theme={successTheme}>Yeah!</Button>
68+
</div>
69+
);
70+
```
71+
72+
### Default theming
73+
74+
If you use a component with a base theme, you may to want import the component with the theme already injected. Then you can compose its style via props with another theme object. In this case the base css will always be bundled:
75+
76+
```jsx
77+
// SuccessButton.js
78+
import React, { Component } from 'react';
79+
import { themr } from 'react-themr';
80+
import successTheme from './SuccessButton.css';
81+
82+
@themr('MySuccessButton', successTheme)
83+
class Button extends Component {
84+
render() {
85+
const { theme, icon, children } = this.props;
86+
return (
87+
<button className={theme.button}>
88+
{ icon ? <i className={theme.icon}>{icon}</i> : null}
89+
<span className={theme.content}>{children}</span>
90+
</button>
91+
)
92+
}
93+
}
94+
95+
export default Button;
96+
```
97+
98+
Imagine you want to make the success button uppercase for an specific case . You can include the classname mixed with other classnames:
99+
100+
```jsx
101+
import React from 'react';
102+
import SuccessButton from 'SuccessButon';
103+
import style from './Section.css';
104+
105+
export default () => (
106+
<section className={style.section}>
107+
<SuccessButton theme={style}>Yai!</SuccessButton>
108+
</section>
109+
);
110+
```
111+
112+
And being `Section.css` something like:
113+
114+
```scss
115+
.section { border: 1px solid red; }
116+
.button { text-transform: uppercase; }
117+
```
118+
119+
The final classnames object for the `Button` component would include class values from `SuccessButton.css` and `Section.css` so it would be uppercase!
120+
121+
### Context theming
122+
123+
Although context theming is not limited to ui-kits, it's very useful to avoid declaring hoc for every component. For example, in react-toolbox, you can define a context theme like:
124+
125+
```jsx
126+
import React from 'react';
127+
import { render } from 'react-dom';
128+
import { ThemeProvider } from 'react-css-themr';
129+
import App from './app'
130+
131+
const contextTheme = {
132+
RTButton: require('react-toolbox/lib/button/style.scss'),
133+
RTDialog: require('react-toolbox/lib/dialog/style.scss')
134+
};
135+
136+
const content = (
137+
<ThemeProvider theme={contextTheme}>
138+
<App />
139+
</ThemeProvider>
140+
);
141+
142+
render(content, document.getElementById('app'));
143+
```
144+
145+
The main idea is to inject classnames objects for each component via context. This way you can have the whole theme in a single place and forget about including styles in every require. Any component `Button` or `Dialog` from will use the provided styles in the context.
146+
147+
## API
148+
149+
### `<ThemeProvider theme>`
150+
151+
Makes available a `theme` context to use in styled components. The shape of the theme object consists of an object whose keys are identifiers for styled components provided with the `themr` function with each theme as the corresponding value. Useful for ui-kits.
152+
153+
### `themr(Identifier, [defaultTheme], [options])`
154+
155+
Returns a `function` to wrap a component and make it themeable.
156+
157+
The returned component accepts a `theme` and `composeTheme` apart from the props of the original component. They are used to provide a `theme` to the component and to configure the style composition, which can be configured via options too. The function arguments are:
158+
159+
- `Identifier` *(String)* used to provide a unique identifier to the component that will be used to get a theme from context.
160+
- `[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.
161+
- `[options]` (*Object*) is an option object that for now only accepts one value: `composeTheme` which accepts:
162+
- `deeply` to deeply merge themes.
163+
- `softly` to softly merge themes.
164+
- `false` to disable theme merging.
165+
166+
## About
167+
168+
The project is originally authored by [Javi Velasco](www.javivelasco.com) as an effort of providing a better customization experience for [React Toolbox](www.react-toolbox.com). Any comments, improvements or feedback is highly appreciated.
169+
170+
Thanks to [Nik Graf](www.twitter.com/nikgraf) and [Mark Dalgleish](www.twitter.com/markdalgleish) for their thoughts about theming and customization for React components.
171+
172+
## License
173+
174+
This project is licensed under the terms of the [MIT license](https://github.com/javivelasco/react-css-themr/blob/master/LICENSE).

‎package.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "react-css-themr",
33
"version": "0.0.1",
44
"description": "React CSS Themr",
5-
"main": "lib/index.js",
5+
"main": "./lib",
66
"files": [
77
"lib",
88
"src"
@@ -16,17 +16,16 @@
1616
"author": "Javi Velasco <javier.velasco86@gmail.com> (http://javivelasco.com/)",
1717
"license": "MIT",
1818
"devDependencies": {
19-
"babel": "^6.5.2",
2019
"babel-cli": "^6.7.7",
2120
"babel-core": "^6.7.7",
2221
"babel-eslint": "^6.0.3",
23-
"babel-plugin-add-module-exports": "^0.1.2",
2422
"babel-plugin-transform-decorators-legacy": "^1.3.4",
2523
"babel-preset-es2015": "^6.6.0",
2624
"babel-preset-react": "^6.5.0",
2725
"babel-preset-stage-0": "^6.5.0",
28-
"chai": "^3.5.0",
2926
"eslint": "^2.8.0",
27+
"eslint-plugin-babel": "^3.2.0",
28+
"eslint-plugin-react": "^5.0.1",
3029
"expect": "^1.18.0",
3130
"jsdom": "^8.4.0",
3231
"mocha": "^2.4.5",

‎src/components/themr.js

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,59 @@
11
import React, { Component, PropTypes } from 'react';
22

3+
const COMPOSE_DEEPLY = 'deeply';
4+
const COMPOSE_SOFTLY = 'softly';
5+
const DONT_COMPOSE = false;
6+
37
const DEFAULT_OPTIONS = {
4-
composeTheme: true
8+
composeTheme: COMPOSE_DEEPLY
59
};
610

711
export default (componentName, localTheme, options = DEFAULT_OPTIONS) => (ThemedComponent) => {
812
const { composeTheme: optionComposeTheme } = options;
13+
validateComposeOption(optionComposeTheme);
914
return class Themed extends Component {
1015
static contextTypes = {
1116
themr: PropTypes.object
1217
};
1318

1419
static propTypes = {
15-
composeTheme: PropTypes.bool,
20+
composeTheme: PropTypes.oneOf([ COMPOSE_DEEPLY, COMPOSE_SOFTLY, DONT_COMPOSE ]),
1621
theme: PropTypes.object
1722
};
1823

1924
static defaultProps = {
2025
composeTheme: optionComposeTheme
2126
};
2227

28+
getThemeNotComposed() {
29+
if (this.props.theme) return this.props.theme;
30+
if (localTheme) return localTheme;
31+
return contextTheme;
32+
}
33+
34+
getContextTheme() {
35+
return this.context.themr
36+
? this.context.themr.theme[componentName]
37+
: {}
38+
}
39+
2340
getTheme() {
24-
if (!this.props.composeTheme && this.props.theme) return this.props.theme;
25-
if (!this.props.composeTheme && localTheme) return localTheme;
26-
const contextTheme = localTheme
27-
? themeable(this.context.themr.theme[componentName], localTheme)
28-
: this.context.themr.theme[componentName];
29-
return themeable(contextTheme, this.props.theme);
41+
return this.props.composeTheme === COMPOSE_SOFTLY
42+
? Object.assign({}, this.getContextTheme(), localTheme, this.props.theme)
43+
: themeable(themeable(this.getContextTheme(), localTheme), this.props.theme);
3044
}
3145

3246
render () {
3347
return React.createElement(ThemedComponent, {
3448
...this.props,
35-
theme: this.getTheme()
49+
theme: this.props.composeTheme
50+
? this.getTheme()
51+
: this.getThemeNotComposed()
3652
});
3753
}
3854
}
3955
};
4056

41-
4257
function themeable(style = {}, theme) {
4358
if (!theme) return style;
4459
return [...Object.keys(theme), ...Object.keys(style)].reduce((result, key) => (
@@ -47,3 +62,13 @@ function themeable(style = {}, theme) {
4762
: { ...result, [key]: theme[key] || style[key] }
4863
), {});
4964
}
65+
66+
function validateComposeOption(composeTheme) {
67+
if ([COMPOSE_DEEPLY, COMPOSE_SOFTLY, DONT_COMPOSE].indexOf(composeTheme) === -1) {
68+
throw new Error(
69+
`Invalid composeTheme option for react-css-themr. Valid composition options\
70+
are ${COMPOSE_DEEPLY}, ${COMPOSE_SOFTLY} and ${DONT_COMPOSE}. The given\
71+
option was ${composeTheme}`
72+
);
73+
}
74+
}

0 commit comments

Comments
 (0)