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 a basic cx api so that users can merge emotion and custom classes #397

Merged
merged 14 commits into from
Oct 16, 2017
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ The core idea comes from Sunil Pai’s [glam](https://github.com/threepointone/g

- [`composition`](docs/composition.md)

- Combine styles and class names with [`cx`](docs/cx.md)

- [`keyframes`](docs/keyframes.md)
- [`fontFace`](docs/font-face.md)
- [`injectGlobal`](docs/inject-global.md)
Expand Down
135 changes: 135 additions & 0 deletions docs/cx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
## cx

`cx` is emotion's version of the popular [`classnames` library](https://github.com/JedWatson/classnames).
`cx` is useful when combining multiple class names, even class names from your stylesheets and emotion generated class names.

### Key features
- High performance integration with emotion
- Custom class names. e.g, `.my-bem--class`, are appended in order.
- Combines the actual content of emotion generated class names. Multiple emotion generated class names are input and a unique class name is output.


### API

**`cx`** - `(...args<string|number|function|object|array>): string`

`cx` takes any number of arguments and returns a string class name.

* Falsey values are removed from the final string.
* If an object value is encountered, any key that has a corresponding truthy value is added to the final string.
* If an function value is encountered, the return value is wrapped in `cx` and returned.


### Examples

##### Combining emotion generated class names

```jsx harmony
import { cx, css } from 'emotion'

const cls1 = css`
font-size: 20px;
background: green;
`
const cls2 = css`
font-size: 20px;
background: blue;
`

<div className={cx(cls1, cls2)} />
```

This renders a `div` with a single class name and the following styles would be inserted.

```css
.css-12345 {
font-size: 20px;
background: green;
font-size: 20px;
background: blue;
}
```

If the order of the class names is reversed in the `cx` call the styles would change precedence.

```jsx harmony
import { cx, css } from 'emotion'

const cls1 = css`
font-size: 20px;
background: green;
`
const cls2 = css`
font-size: 20px;
background: blue;
`

<div className={cx(cls2, cls1)} /> // <-- arguments reversed
```

The div will now have a **green** background even though `cls2` was inserted **after** `cls1`.

```css
.css-54321 {
font-size: 20px;
background: blue;
font-size: 20px;
background: green;
}
```

##### Combining both emotion generated class names and custom class names.

```jsx harmony
const cls1 = css`
font-size: 20px;
background: green;
`
const cls2 = css`
font-size: 20px;
background: blue;
`

const cls3 = css`
font-size: 20px;
background: darkorange;
`

const cls4 = css`
font-size: 20px;
background: darkgreen;
`

const foo = true
const bar = false


<div
className={cx(
{ [cls1]: foo },
{ [cls2]: bar },
() => 'modal',
'profile',
[[cls3, [cls4]]]
)}
/>
```

Output:

```css
.css-i43k4 {
font-size: 20px;
background: green;
font-size: 20px;
background: darkorange;
font-size: 20px;
background: darkgreen;
}
```

```jsx harmony
<div
className="modal profile css-i43k4"
/>
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@
"bundlesize": [
{
"path": "./packages/emotion/dist/emotion.umd.min.js",
"threshold": "6 Kb"
"threshold": "6.25 Kb"
},
{
"path": "./packages/react-emotion/dist/emotion.umd.min.js",
Expand Down
41 changes: 41 additions & 0 deletions packages/emotion/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,47 @@ export function merge(className, sourceMap) {
return rawClassName + css(registeredStyles, sourceMap)
}

function classnames() {
let len = arguments.length
let i = 0
let cls = ''
for (; i < len; i++) {
let arg = arguments[i]

if (arg == null) continue
let next = (cls && cls + ' ') || cls

switch (typeof arg) {
case 'boolean':
break
case 'function':
cls = next + classnames(arg())
break
case 'object': {
if (Array.isArray(arg)) {
cls = next + classnames.apply(null, arg)
} else {
for (const k in arg) {
if (arg[k]) {
cls && (cls += ' ')
cls += k
}
}
}
break
}
default: {
cls = next + arg
}
}
}
return cls
}

export function cx(...classNames) {
return merge(classnames(...classNames))
}

export function hydrate(ids) {
ids.forEach(id => {
inserted[id] = true
Expand Down
68 changes: 68 additions & 0 deletions packages/emotion/test/__snapshots__/cx.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`cx all types 1`] = `
.glamor-0 {
font-size: 20px;
background: green;
font-size: 20px;
background: darkorange;
font-size: 20px;
background: darkgreen;
}

<div
className="modal profile glamor-0"
/>
`;

exports[`cx fun fun functions 1`] = `
.glamor-0 {
font-size: 20px;
background: green;
font-size: 20px;
background: darkorange;
font-size: 20px;
background: darkgreen;
}

<div
className="modal profile glamor-0"
/>
`;

exports[`cx merge 2 1`] = `
.glamor-0 {
font-size: 20px;
background: green;
}

<div
className="glamor-0 modal"
/>
`;

exports[`cx merge 3 1`] = `
.glamor-0 {
font-size: 20px;
background: green;
font-size: 20px;
background: blue;
}

<div
className="modal glamor-0"
/>
`;

exports[`cx merge 4 1`] = `
.glamor-0 {
font-size: 20px;
background: green;
font-size: 20px;
background: blue;
}

<div
className="modal profile glamor-0"
/>
`;
119 changes: 119 additions & 0 deletions packages/emotion/test/cx.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React from 'react'
import renderer from 'react-test-renderer'
import { css, cx } from 'emotion'

describe('cx', () => {
test('merge 2', () => {
const cls1 = css`
font-size: 20px;
background: green;
`

const tree = renderer.create(<div className={cx(cls1, 'modal')} />).toJSON()
expect(tree).toMatchSnapshot()
})

test('merge 3', () => {
const cls1 = css`
font-size: 20px;
background: green;
`
const cls2 = css`
font-size: 20px;
background: blue;
`

const tree = renderer
.create(<div className={cx(cls1, cls2, 'modal')} />)
.toJSON()
expect(tree).toMatchSnapshot()
})

test('merge 4', () => {
const cls1 = css`
font-size: 20px;
background: green;
`
const cls2 = css`
font-size: 20px;
background: blue;
`

const tree = renderer
.create(<div className={cx(cls1, cls2, 'modal', 'profile')} />)
.toJSON()
expect(tree).toMatchSnapshot()
})

test('all types', () => {
const cls1 = css`
font-size: 20px;
background: green;
`
const cls2 = css`
font-size: 20px;
background: blue;
`

const cls3 = css`
font-size: 20px;
background: darkorange;
`

const cls4 = css`
font-size: 20px;
background: darkgreen;
`

const foo = true
const bar = false

const tree = renderer
.create(
<div
className={cx(
{ [cls1]: foo },
{ [cls2]: bar },
() => 'modal',
'profile',
[[cls3, [cls4]]]
)}
/>
)
.toJSON()
expect(tree).toMatchSnapshot()
})

test('fun fun functions', () => {
const cls1 = css`
font-size: 20px;
background: green;
`
const cls2 = css`
font-size: 20px;
background: blue;
`

const cls3 = css`
font-size: 20px;
background: darkorange;
`

const cls4 = css`
font-size: 20px;
background: darkgreen;
`

const tree = renderer
.create(
<div
className={cx(() => () => [
() => [cls1, false && cls2, 'modal'],
() => [() => [cls3, () => ({ [cls4]: true }), 'profile']]
])}
/>
)
.toJSON()
expect(tree).toMatchSnapshot()
})
})