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

export emotion-theming from react-emotion #475

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 40 additions & 10 deletions docs/theming.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
## Theming

Themes are provided by the library [`emotion-theming`](https://github.com/emotion-js/emotion/tree/master/packages/emotion-theming).


```bash
npm install -S emotion-theming
```
Theming functionality is provided by `emotion-theming`, and readily available as named exports in `react-emotion` and `preact-emotion`.

Add `ThemeProvider` to the top level of your app and access the theme with `props.theme` in a styled component. The api is laid out in detail [in the documentation](https://github.com/emotion-js/emotion/tree/master/packages/emotion-theming/README.md#api).

```jsx
import styled from 'react-emotion'
import { ThemeProvider } from 'emotion-theming'
import React from 'react'
import styled, { ThemeProvider } from 'react-emotion'

const theme = {
purple: '#9042F0'
}

const H1 = styled(Heading)`
color: ${p => p.theme.purple};
`


const App = () => (
<ThemeProvider theme={theme}>
<H1>
Expand All @@ -27,6 +25,38 @@ const App = () => (
)
```

To access the theme within a normal component, use the `withTheme` higher order component:

```jsx
import PropTypes from 'prop-types'
import React from 'react'
import { ThemeProvider, withTheme } from 'react-emotion'

const theme = {
title: 'Foo'
}

class Header extends React.Component {
static propTypes = {
theme: PropTypes.shape({
title: PropTypes.string,
})
}

render() {
return (
<h1>
{this.props.theme.title}
</h1>
)
}
}

const ThemedHeader = withTheme(Header)


const App = () => (
<ThemeProvider theme={theme}>
<ThemedHeader />
</ThemeProvider>
)
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@
},
{
"path": "./packages/react-emotion/dist/emotion.umd.min.js",
"threshold": "9 Kb"
"threshold": "9.9 Kb"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -600,35 +600,6 @@ exports[`styled prop filtering on composed styled components that are string tag
</a>
`;

exports[`styled random expressions 1`] = `
.glamor-1 {
font-size: 1rem;
margin-top: 0;
margin-right: auto;
margin-bottom: 0;
margin-left: auto;
color: green;
}

@media (min-width: 420px) {
.glamor-1 {
color: blue;
}
}

@media (min-width: 420px) and (min-width: 520px) {
.glamor-1 {
color: green;
}
}

<h1
className="glamor-0 legacy__class glamor-1"
>
hello world
</h1>
`;

exports[`styled random expressions undefined return 1`] = `
.glamor-0 {
color: green;
Expand Down
2 changes: 1 addition & 1 deletion packages/babel-plugin-emotion/test/macro/react.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react'
import renderer from 'react-test-renderer'
import { css } from 'emotion/macro'
import { ThemeProvider } from 'react-emotion'
import styled from 'react-emotion/macro'
import { ThemeProvider } from 'emotion-theming'
import hoistNonReactStatics from 'hoist-non-react-statics'
import { mount } from 'enzyme'
import enzymeToJson from 'enzyme-to-json'
Expand Down
25 changes: 20 additions & 5 deletions packages/emotion-theming/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@

## Install

Theming functionality is available directly via `react-emotion` and `preact-emotion`.

```js
// react
import styled, { channel, ThemeProvider, withTheme } from 'react-emotion'

// preact
import styled, { channel, ThemeProvider, withTheme } from 'preact-emotion'
```

However, if you'd like to use the functionality separately for some reason, the imports are available from `emotion-theming`.

```bash
# add --save if using npm@4 or lower
Expand All @@ -28,6 +39,10 @@ npm i emotion-theming
yarn add emotion-theming
```

```js
import { channel, ThemeProvider, withTheme } from 'emotion-theming'
```

## Usage

Theming is accomplished by placing the `ThemeProvider` component, at the top of the React component tree and wrapping descendants with the `withTheme` higher-order component (HOC). This HOC seamlessly acquires the current theme and injects it as a "prop" into your own component.
Expand Down Expand Up @@ -66,7 +81,7 @@ export default Page extends React.Component {
/** parent.js */
import React from 'react';
import ReactDOM from 'react-dom';
import { ThemeProvider } from 'emotion-theming';
import { ThemeProvider } from 'react-emotion';

import Page from './child.js';

Expand Down Expand Up @@ -104,7 +119,7 @@ A React component that passes the theme object down the component tree via [cont
```jsx
import React from 'react';
import styled from 'react-emotion'
import { ThemeProvider, withTheme } from 'emotion-theming';
import { ThemeProvider, withTheme } from 'react-emotion';

// object-style theme

Expand Down Expand Up @@ -146,7 +161,7 @@ A higher-order component that provides the current theme as a prop to the wrappe
```jsx
import PropTypes from 'prop-types';
import React from 'react';
import { withTheme } from 'emotion-theming';
import { withTheme } from 'react-emotion;

class TellMeTheColor extends React.Component {
render() {
Expand All @@ -169,12 +184,12 @@ const TellMeTheColorWithTheme = withTheme(TellMeTheColor);

### channel: String

The emotion-theming package uses this string as the React context key by default.
Emotion's theming functionality uses this string as the React context key by default.

If you wish to build your own components on top of this library, it is recommended to import the context key from this package instead of hardcoding its value.

```js
import { channel } from 'emotion-theming';
import { channel } from 'react-emotion';

console.log(channel); '__EMOTION_THEMING__';
```
Expand Down
21 changes: 21 additions & 0 deletions packages/emotion-theming/src/factory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import PropTypes from 'prop-types'
import createWithTheme from './with-theme'
import createThemeProvider from './theme-provider'

export default function createTheming(ReactlikeAPI, channel) {
channel = channel || '__EMOTION_THEMING__'

const contextTypes = {
[channel]: PropTypes.object
}

const ThemeProvider = createThemeProvider(ReactlikeAPI, channel, contextTypes)
const withTheme = createWithTheme(ReactlikeAPI, channel, contextTypes)

return {
channel,
contextTypes,
ThemeProvider,
withTheme
}
}
12 changes: 9 additions & 3 deletions packages/emotion-theming/src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export { default as ThemeProvider } from './theme-provider'
export { default as withTheme } from './with-theme'
export { channel, contextTypes } from './utils'
import React from 'react'
import createTheming from './factory'

export const {
channel,
contextTypes,
ThemeProvider,
withTheme
} = createTheming(React)
134 changes: 72 additions & 62 deletions packages/emotion-theming/src/theme-provider.js
Original file line number Diff line number Diff line change
@@ -1,94 +1,104 @@
// adapted from styled-components' ThemeProvider
// https://github.com/styled-components/styled-components/blob/4503cab5b86aa9ef8314c5baa360a2fbb4812485/src/models/ThemeProvider.js

import React, { Component } from 'react'
import createBroadcast from './create-broadcast'
import { channel, contextTypes } from './utils'

const isPlainObject = test =>
Object.prototype.toString.call(test) === '[object Object]'

class ThemeProvider extends Component {
constructor() {
super()
this.getTheme = this.getTheme.bind(this)
}
export default (ReactlikeAPI, channel, contextTypes) => {
class ThemeProvider extends ReactlikeAPI.Component {
constructor() {
super()
this.getTheme = this.getTheme.bind(this)
}

componentWillMount() {
// If there is a ThemeProvider wrapper anywhere around this theme provider, merge this theme
// with the outer theme
if (this.context[channel] !== undefined) {
this.unsubscribeToOuterId = this.context[channel].subscribe(theme => {
this.outerTheme = theme
componentWillMount() {
// If there is a ThemeProvider wrapper anywhere around this theme provider, merge this theme
// with the outer theme
if (this.context[channel] !== undefined) {
this.unsubscribeToOuterId = this.context[channel].subscribe(theme => {
this.outerTheme = theme

if (this.broadcast !== undefined) {
this.publish(this.props.theme)
}
})
}

if (this.broadcast !== undefined) {
this.publish(this.props.theme)
}
})
this.broadcast = createBroadcast(this.getTheme(this.props.theme))
}

this.broadcast = createBroadcast(this.getTheme(this.props.theme))
}

getChildContext() {
return {
[channel]: {
subscribe: this.broadcast.subscribe,
unsubscribe: this.broadcast.unsubscribe
getChildContext() {
return {
[channel]: {
subscribe: this.broadcast.subscribe,
unsubscribe: this.broadcast.unsubscribe
}
}
}
}

componentWillReceiveProps(nextProps) {
if (this.props.theme !== nextProps.theme) {
this.publish(nextProps.theme)
componentWillReceiveProps(nextProps) {
if (this.props.theme !== nextProps.theme) {
this.publish(nextProps.theme)
}
}
}

componentWillUnmount() {
const themeContext = this.context[channel]
if (themeContext !== undefined) {
themeContext.unsubscribe(this.unsubscribeToOuterId)
componentWillUnmount() {
const themeContext = this.context[channel]
if (themeContext !== undefined) {
themeContext.unsubscribe(this.unsubscribeToOuterId)
}
}
}

// Get the theme from the props, supporting both (outerTheme) => {} as well as object notation
getTheme(theme) {
if (typeof theme === 'function') {
const mergedTheme = theme(this.outerTheme)
if (!isPlainObject(mergedTheme)) {
// Get the theme from the props, supporting both (outerTheme) => {} as well as object notation
getTheme(theme) {
if (typeof theme === 'function') {
const mergedTheme = theme(this.outerTheme)
if (
process.env.NODE_ENV !== 'production' &&
!isPlainObject(mergedTheme)
) {
throw new Error(
'[ThemeProvider] Please return an object from your theme function, i.e. theme={() => ({})}!'
)
}
return mergedTheme
}
if (process.env.NODE_ENV !== 'production' && !isPlainObject(theme)) {
throw new Error(
'[ThemeProvider] Please return an object from your theme function, i.e. theme={() => ({})}!'
'[ThemeProvider] Please make your theme prop a plain object'
)
}
return mergedTheme
}
if (!isPlainObject(theme)) {
throw new Error(
'[ThemeProvider] Please make your theme prop a plain object'
)

if (this.outerTheme === undefined) {
return theme
}

return { ...this.outerTheme, ...theme }
}

if (this.outerTheme === undefined) {
return theme
publish(theme) {
this.broadcast.publish(this.getTheme(theme))
}

return { ...this.outerTheme, ...theme }
}
render() {
const children = Array.prototype.concat(this.props.children)

publish(theme) {
this.broadcast.publish(this.getTheme(theme))
}
if (children.length > 1) {
if (process.env.NODE_ENV !== 'production') {
throw new Error(
'[ThemeProvider] ThemeProvider only accepts a single child'
)
}
}

render() {
if (!this.props.children) {
return null
return children[0] || null
}
return React.Children.only(this.props.children)
}
}

ThemeProvider.childContextTypes = contextTypes
ThemeProvider.contextTypes = contextTypes
ThemeProvider.childContextTypes = contextTypes
ThemeProvider.contextTypes = contextTypes

export default ThemeProvider
return ThemeProvider
}
Loading