Skip to content

Commit

Permalink
Accept arrays of styles in css()
Browse files Browse the repository at this point in the history
Accepting both styles and arrays of styles improves developer ergonomics with how I use Aphrodite when passing styles into components. The biggest problem I run into is that the spread operator doesn't work with undefined, so this code fails:

```js
// Component.js
class Component extends React.Component {
  static propTypes = { styles: PropTypes.array };
  render() {
    // this css() call fails because spreading `undefined` is an error
    return <div className={css(styles.myStyle, ...this.props.styles)} />;
  }
}

const styles = StyleSheet.create({...});

// OwnerComponent.js
class OwnerComponent extends React.Component {
  render() {
    return <Component />;  // we're happy with the default styles
  }
}
```

The second, more cosmetic issue, is that even if I'm passing down just one style I need to create an array:

```js
// OwnerComponent.js
class OwnerComponent extends React.Component {
  render() {
    return <Component styles={[styles.customStyle]} />;  // need the [ ]
  }
}

const styles = StyleSheet.create({...});
```

With this PR, you'd write `css(styles.myStyle, this.props.styles)` and it'd address both these issues. Aphrodite would flatten `this.props.styles` if it were an array, it would leave it alone if it were an Aphrodite style object, and it would also leave it alone and filter it out if it were falsy.

As a data point, React Native flattens style arrays (recursively, actually) and it's pretty convenient with little downside. So just wanted to float this idea out there.

Test Plan: added unit tests
  • Loading branch information
ide committed Oct 8, 2016
1 parent 1763b30 commit b4e2a6b
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 4 deletions.
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,41 @@ const className = css(

This is possible because any falsey arguments will be ignored.

## Combining Styles

To combine styles, pass multiple styles or arrays of styles into `css()`. This is common when combining styles from an owner component:

```jsx
class App extends Component {
render() {
return <Marker styles={[styles.large, styles.red]} />;
}
}

class Marker extends Component {
render() {
// css() accepts styles, arrays of styles (including nested arrays),
// and falsy values including undefined.
return <div className={css(styles.marker, this.props.styles)} />;
}
}

const styles = StyleSheet.create({
red: {
backgroundColor: 'red'
},

large: {
height: 20,
width: 20
},

marker: {
backgroundColor: 'blue'
}
};
```
## Server-side rendering
To perform server-side rendering, make a call to `StyleSheetServer.renderStatic`, which takes a callback. Do your rendering inside of the callback and return the generated HTML. All of the calls to `css()` inside of the callback will be collected and the generated css as well as the generated HTML will be returned.
Expand Down
9 changes: 6 additions & 3 deletions src/inject.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import asap from 'asap';

import {generateCSS} from './generate';
import {hashObject} from './util';
import {flattenDeep, hashObject} from './util';

// The current <style> tag we are inserting into, or null if we haven't
// inserted anything yet. We could find this each time using
Expand Down Expand Up @@ -189,10 +189,13 @@ export const addRenderedClassNames = (classNames) => {
*
* @param {boolean} useImportant If true, will append !important to generated
* CSS output. e.g. {color: red} -> "color: red !important".
* @param {Object[]} styleDefinitions style definition objects as returned as
* properties of the return value of StyleSheet.create().
* @param {(Object|Object[])[]} styleDefinitions style definition objects, or
* arbitrarily nested arrays of them, as returned as properties of the
* return value of StyleSheet.create().
*/
export const injectAndGetClassName = (useImportant, styleDefinitions) => {
styleDefinitions = flattenDeep(styleDefinitions);

// Filter out falsy values from the input, to allow for
// `css(a, test && c)`
const validDefinitions = styleDefinitions.filter((def) => def);
Expand Down
3 changes: 3 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export const mapObj = (obj, fn) => pairsToObject(objectToPairs(obj).map(fn))
// [[A], [B, C, [D]]] -> [A, B, C, [D]]
export const flatten = (list) => list.reduce((memo, x) => memo.concat(x), []);

export const flattenDeep = (list) =>
list.reduce((memo, x) => memo.concat(Array.isArray(x) ? flattenDeep(x) : x), []);

const UPPERCASE_RE = /([A-Z])/g;
const MS_RE = /^ms-/;

Expand Down
17 changes: 17 additions & 0 deletions tests/index_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@ describe('css', () => {
assert.equal(css(sheet.red), css(false, sheet.red));
});

it('accepts arrays of styles', () => {
const sheet = StyleSheet.create({
red: {
color: 'red',
},

blue: {
color: 'blue'
}
});

assert.equal(css(sheet.red, sheet.blue), css([sheet.red, sheet.blue]));
assert.equal(css(sheet.red, sheet.blue), css(sheet.red, [sheet.blue]));
assert.equal(css(sheet.red, sheet.blue), css([sheet.red, [sheet.blue]]));
assert.equal(css(sheet.red), css(false, [null, false, sheet.red]));
});

it('succeeds for with empty args', () => {
assert(css() != null);
assert(css(false) != null);
Expand Down
10 changes: 9 additions & 1 deletion tests/util_test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import {assert} from 'chai';

import {recursiveMerge} from '../src/util.js';
import {flattenDeep, recursiveMerge} from '../src/util.js';

describe('Utils', () => {
describe('flattenDeep', () => {
it('flattens arrays at any level', () => {
assert.deepEqual(
flattenDeep([[1, [2, 3, []]], 4, [[5], [6, [7]]]]),
[1, 2, 3, 4, 5, 6, 7]);
});
});

describe('recursiveMerge', () => {
it('merges two objects', () => {
assert.deepEqual(
Expand Down

0 comments on commit b4e2a6b

Please sign in to comment.