diff --git a/README.md b/README.md index 62bc9e82..8e0cd18f 100644 --- a/README.md +++ b/README.md @@ -2,29 +2,67 @@ **Experimental / Proof of concept** -A component-based React SDK for building and styling web applications and SDKs. -Inspired by `react-native`. +A React SDK for creating web applications and toolkits. Inspired by `react-native`. -This library provides: +It includes the following components: -* An initial implementation of the `react-native` components `Image`, `Text`, - and `View`. -* An initial implementation of defining explicit and default `style` propTypes. -* A proof of concept implementation for converting inline `style` definitions - to CSS classes. +* `View`: a flexbox primitive +* `Text`: a text primitive +* `Image`: an image primitive +* (`Component`: base / implementation) + +And uses a [styling strategy](docs/styling-strategy.md) that maps inline styles +to single-purpose CSS rules. This proof of concept uses a ~3KB (gzipped) precomputed CSS bundle; a complete implementation is likely to produce a slightly larger CSS file and fewer inline styles. -Other React styling strategies: -[react-native](https://facebook.github.io/react-native/), -[react-style](https://github.com/js-next/react-style), -[jsxstyle](https://github.com/petehunt/jsxstyle), -[react-inline](https://github.com/martinandert/react-inline), and -[react-free-style](https://github.com/blakeembrey/react-free-style/) +## Components -### 1. Write styles using plain JavaScript objects +All components define explicit `style` PropTypes according to the [`StyleProp` +spec](docs/StyleProp.spec.md). + +### Component + +TODO + +[`Component`](docs/Component.md) + +### View + +TODO + +A flexbox container; foundational layout building block. + +[`View` spec](docs/View.spec.md) + +See this [guide to flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/). + +### Text + +TODO + +[`Text` spec](docs/View.spec.md) + +### Image + +TODO + +[`Image` spec](docs/View.spec.md) + +## Styling + +Styling is identical to using inline styles in React, but most inline styles +are converted to unique CSS classes. **This is only true for the SDK +components**. + +The companion stylesheet can be referenced as an external resource, inlined, or +injected by JS. + +See the [styling strategy docs](docs/styling-strategy.md) for more details. + +### Use plain JavaScript objects Use JavaScript to write style definitions in React components: @@ -47,22 +85,23 @@ const style = { }; ``` -### 2. Set styles using the `style` attribute +### Use the `style` attribute The `style` attribute is a simple API for creating element-scoped styles. ```js -... +import {View} from 'react-web-sdk'; - - ... -... + ); + } +} ``` -It's easy to combine and override style objects: +Combine and override style objects: ```js import baseStyle from './baseStyle'; @@ -74,461 +113,20 @@ const buttonStyle = { } ``` -So far, this is identical to using inline styles in a React component. - -### 3. Map inline styles to static CSS rules - -Using the `style` attribute would normally produce inline styles. Libraries -like [react-style](https://github.com/js-next/react-style) implement a -`Stylesheet.create` API and provide a means to extract styles to CSS at build -time. The extraction maps whole style objects to multi-declaration CSS rules. - -For example: - -```js -// relies on a new construct -Stylesheet.create({ - root: { - background: 'transparent', - display: 'flex', - flexGrow: 1, - justifyContent: 'center' - } -}); -``` - -Yields: - -```css -/* produces new css for every new style rule */ -.foo { - background: transparent; - display: flex; - flex-grow: 1; - justify-content: center; -} -``` - -Each component adds new rules to the stylesheet. This can lead the extracted -stylesheet to become increasingly large and prone to change. - -Libraries like [Atomic CSS](http://acss.io/), -[Basscss](http://www.basscss.com/), [SUIT CSS](https://suitcss.github.io/), and -[tachyons](http://tachyons.io/) are attempts to limit style scope and limit -stylesheet growth. But they are CSS utility libraries, each with a particular -set of classes and features to learn. All of them require developers to -manually connect CSS classes for given styles. - -The `react-web-sdk` library's implementation is a proof-of-concept for the -strategy of automatically mapping style `key`-`value` pairs to -single-declaration CSS rules. - -```css -.background-transparent { background: transparent } -.display-block { display: block } -.flexGrow-1 { flex-grow: 1 } -.justifyContent-center { justify-content: center } -``` - -Mapping declarations to single-purpose classes greatly reduces the total amount -of CSS needed. It avoids the repetition of declarations between rules for -unrelated elements, and prevents style collisions. A build step hashes the class names. - -```html -
...
-``` - -The current implementation uses a precomputed CSS library of known and common -declarations – 200+ single-declaration rules, with obfuscated selectors. This -handles a signficant portion of possible declarations. But it falls through to -inline styles significantly more often than an better implementation using -static analysis to generate the CSS library at build time. - -### 4. Use inline styles for dynamic values - -Inline styles are used for values that cannot be resolved at build time. - -```js - 0.5 ? 'red' : 'black')}}>... -``` - -### 5. Add `style` prop types - -Each Component defines `StylePropTypes` and filters out any style properties -that are not part of its style API. For example, `View` does not support any -typographic properties. - -See the section below on `StylePropTypes`. - -### Media Queries? - -We can use `mediaMatch` to orchestrate both style and DOM changes. The -component can be re-rendered when the styles change. This also avoids the need -for additional CSS. Adding media queries as keys on the `style` object (like -`react-style` does) might be a good, or it might not be. I'd prefer a solution -that accomodates both style and DOM changes, rather than one or the other. -Perhaps `this.context` or a higher-order component that passes viewport data to -components via `props`. - ---- - -## Components - -### `Component` - -A component that manages styles across the `className` and `style` properties. -The building block upon which all other components in `react-web-sdk` are -build. - -#### PropTypes - -All other props are transferred directly to the `element`. - -+ `className`: `string` -+ `element`: `func` or `string` -+ `style`: `object` - -#### Examples - -```js -import {Component, pickProps} from 'react-web-sdk'; -import React, {PropTypes} from 'react'; - -const ExampleStylePropTypes = { opacity: PropTypes.number }; -const ExampleStyleDefaultProps = { opacity: 1 }; - -class Example extends React.Component { - static propTypes = { - style: PropTypes.shape(ExampleStylePropTypes) - } - - render() { - // only apply supported styles - const supportedStyle = pickProps(this.props.style, ExampleStylePropTypes); - // merge with default styles - const style = { ...ExampleStyleDefaultProps, ...supportedStyle } - - return ( - - ); - } -} -``` - - -### `Image` - -TODO - -#### PropTypes - -All other props are transferred directly to the `element`. - -+ `accessibilityLabel`: `string` -+ `async`: `bool` (TODO) -+ `className`: `string` -+ `source`: `string` -+ `style`: `ImageStylePropTypes` - -#### ImageStylePropTypes - -+ `BackgroundPropTypes` -+ `BorderThemePropTypes` -+ `LayoutPropTypes` -+ `opacity`: `string` - -#### Examples - -```js -import {Image, StylePropTypes} from 'react-web-sdk'; -import React, {PropTypes} from 'react'; - -const AvatarStylePropTypes = { - ...StylePropTypes.BorderThemePropTypes -}; - -const AvatarStyleDefaultProps = { - borderColor: 'white', - borderWidth: '5px' -}; - -class Avatar extends React.Component { - static propTypes = { - size: PropTypes.oneOf(['small', 'normal', 'large']), - style: PropTypes.shape(AvatarStylePropTypes), - user: PropTypes.object - } - - static defaultProps = { - size: 'normal' - } - - render() { - const supportedStyle = pickProps(this.props.style, AvatarStylePropTypes); - const style = { ...AvatarStyleDefaultProps, ...supportedStyle } - - return ( - - ); - } -} -``` - - -### `Text` - -Text layout and styles. - -#### PropTypes - -All other props are transferred directly to the `element`. - -+ `className`: `string` -+ `element`: `func` or `string` (default `"div"`) -+ `style`: `TextStylePropTypes` - -#### TextStylePropTypes - -+ ViewStylePropTypes -+ TypographicPropTypes - -#### Examples - -```js -import {StylePropTypes, Text} from 'react-web-sdk'; -import React, {PropTypes} from 'react'; - -class PrettyText extends React.Component { - static propTypes = { - color: PropTypes.oneOf(['white', 'gray', 'red']), - size: PropTypes.oneOf(['small', 'normal', 'large']), - weight: PropTypes.oneOf(['light', 'normal', 'bold']) - } - - static defaultProps = { - color: 'gray', - size: 'normal', - weight: 'normal' - } - - render() { - const { color, size, style, weight, ...other } = this.props; - - return ( - - ); - } -} - -const localStyle = { - color: { - white: { color: 'white' }, - gray: { color: 'gray' }, - red: { color: 'red' } - }, - size: { - small: { fontSize: '0.85rem', padding: '0.5rem' }, - normal: { fontSize: '1rem', padding: '0.75rem' }, - large: { fontSize: '1.5rem', padding: '1rem' } - }, - weight: { - light: { fontWeight: '300' }, - normal: { fontWeight: '400' }, - bold: { fontWeight: '700' } - } -} -``` - - -### `View` - -`View` is a flexbox container and the fundamental building block for UI. It is -designed to be nested inside other `View`'s and to have 0-to-many children of -any type. - -#### PropTypes - -All other props are transferred directly to the `element`. - -+ `className`: `string` -+ `element`: `func` or `string` (default `"div"`) -+ `pointerEvents`: `oneOf('all', 'box-only', 'box-none', 'none')` -+ `style`: `ViewStylePropTypes` - -#### ViewStylePropTypes - -+ BackgroundPropTypes -+ BorderThemePropTypes -+ LayoutPropTypes -+ `boxShadow`: `string` -+ `color`: `string` -+ `opacity`: `number` - -#### ViewStyleDefaultProps - -Implements the default styles from -[facebook/css-layout](https://github.com/facebook/css-layout). - -```js -const ViewStyleDefaultProps = { - alignItems: 'stretch', // 1 - borderWidth: 0, - borderStyle: 'solid', - boxSizing: 'border-box', // 2 - display: 'flex', // 3 - flexBasis: 'auto', // 1 - flexDirection: 'column', // 1 - flexShrink: 0, // 1 - listStyle: 'none', - margin: 0, - padding: 0, - position: 'relative' // 4 -}; -``` - -1. All the flex elements are oriented from top-to-bottom, left-to-right and do - not shrink. This is how things are laid out using the default CSS settings - and what you'd expect. - -2. The most convenient way to express the relation between width and other - box-model properties. - -3. Everything is `display:flex` by default. All the behaviors of `block` and - `inline-block` can be expressed in term of flex but not the opposite. - -4. Everything is `position:relative`. This makes `position:absolute` target the - direct parent and not some parent which is either relative or absolute. If - you want to position an element relative to something else, you should move - it in the DOM instead of relying of CSS. It also makes `top`, `left`, - `right`, `bottom` do something when not specifying `position:absolute`. - -#### Examples - -```js -// TODO -``` - ---- - ## Utilities -### Object property filtering +#### `getOtherProps(reactComponentInstance)` -Create a new object that includes or excludes a list of properties. +Returns an object containing all the props from `this.props` that are not +defined in `propTypes`. -* `.getOtherProps(reactComponentInstance)` (strips propTypes from `this.props`) -* `.pickProps(obj, arrayOfIncludedProps)` -* `.omitProps(obj, arrayOfExcludedProps)` +#### `omitProps(obj, arrayOfExcludedProps)` ---- +Returns an object with the specified props excluded. -## StylePropTypes +#### `pickProps(obj, arrayOfIncludedProps)` -### Background - -+ `backgroundColor`: `string` -+ `backgroundImage`: `string` -+ `backgroundPosition`: `string` -+ `backgroundRepeat`: `string` -+ `backgroundSize`: `string` - -### BorderTheme - -+ `borderColor`: `string` -+ `borderTopColor`: `string` -+ `borderRightColor`: `string` -+ `borderBottomColor`: `string` -+ `borderLeftColor`: `string` -+ `borderStyle`: `string` -+ `borderRadius`: `number` or `string` -+ `borderTopLeftRadius`: `number` or `string` -+ `borderTopRightRadius`: `number` or `string` -+ `borderBottomLeftRadius`: `number` or `string` -+ `borderBottomRightRadius`: `number` or `string` - -### BoxModel - -+ `borderWidth`: `number` or `string` -+ `borderTopWidth`: `number` or `string` -+ `borderRightWidth`: `number` or `string` -+ `borderBottomWidth`: `number` or `string` -+ `borderLeftWidth`: `number` or `string` -+ `boxSizing`: `oneOf('border-box', 'content-box')` -+ `display`: `oneOf('block', 'flex', 'inline', 'inline-block', 'inline-flex')` -+ `height`: `number` or `string` -+ `margin`: `number` or `string` -+ `marginTop`: `number` or `string` -+ `marginRight`: `number` or `string` -+ `marginBottom`: `number` or `string` -+ `marginLeft`: `number` or `string` -+ `padding`: `number` or `string` -+ `paddingTop`: `number` or `string` -+ `paddingRight`: `number` or `string` -+ `paddingBottom`: `number` or `string` -+ `paddingLeft`: `number` or `string` -+ `width`: `number` or `string` - -### Flexbox - -* `alignContent`: `oneOf('center', 'flex-end', 'flex-start', 'stretch', 'space-around', 'space-between')` -* `alignItems`: `oneOf('baseline', 'center', 'flex-end', 'flex-start', 'stretch')` -* `alignSelf`: `oneOf('auto', 'baseline', 'center', 'flex-end', 'flex-start', 'stretch')` -* `flexBasis`: `string` -* `flexDirection`: `oneOf('column', 'column-reverse', 'row', 'row-reverse')` -* `flexGrow`: `number` -* `flexShrink`: `number` -* `flexWrap`: `oneOf('nowrap', 'wrap', 'wrap-reverse')` -* `justifyContent`: `oneOf('center', 'flex-end', 'flex-start', 'space-around', 'space-between')` -* `order`: `number` - -See this [guide to flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/). - -### Layout - -* BoxModel -* Flexbox -* Position - -### Position - -* `position`: `oneOf('absolute', 'fixed', 'relative')` -* `bottom`: `number` or `string` -* `left`: `number` or `string` -* `right`: `number` or `string` -* `top`: `number` or `string` -* `zIndex`: `number` - -### Typographic - -* `direction`: `oneOf('auto', 'ltr', 'rtl')` -* `fontFamily`: `string` -* `fontSize`: `string` -* `fontWeight`: `oneOf('100', '200', '300', '400', '500', '600', '700', '800', '900', 'bold', 'normal')` -* `fontStyle`: `oneOf('normal', 'italic')` -* `letterSpacing`: `string` -* `lineHeight`: `number` or `string` -* `textAlign`: `oneOf('auto', 'left', 'right', 'center')` -* `textDecoration`: `oneOf('none', 'underline')` -* `textTransform`: `oneOf('capitalize', 'lowercase', 'none', 'uppercase')` -* `wordWrap`: `oneOf('break-word', 'normal')` - ---- +Returns an object with the specified props included. ## Development diff --git a/docs/Component.md b/docs/Component.md new file mode 100644 index 00000000..c7183bf0 --- /dev/null +++ b/docs/Component.md @@ -0,0 +1,44 @@ +# `Component` + +This component is part of the implementation for managing styles across the +`className` and `style` properties. It is the building block upon which all +other components in `react-web-sdk` are built. + +## PropTypes + +All other props are transferred directly to the `element`. + ++ `element`: `func` or `string` ++ `style`: `object` + +#### Examples + +```js +import {Component, pickProps} from 'react-web-sdk'; +import React, {PropTypes} from 'react'; + +const ExampleStylePropTypes = { opacity: PropTypes.number }; +const ExampleStyleDefaultProps = { opacity: 1 }; + +class Example extends React.Component { + static propTypes = { + style: PropTypes.shape(ExampleStylePropTypes) + } + + render() { + const { style, ...other } = this.props; + // only apply supported styles + const supportedStyle = pickProps(style, ExampleStylePropTypes); + // merge with default styles + const mergedStyle = { ...ExampleStyleDefaultProps, ...supportedStyle } + + return ( + + ); + } +} +``` diff --git a/docs/Image.spec.md b/docs/Image.spec.md new file mode 100644 index 00000000..0cdf8fa6 --- /dev/null +++ b/docs/Image.spec.md @@ -0,0 +1,66 @@ +# Image spec + +#### PropTypes + +All other props are transferred directly to the `element`. + ++ `accessibilityLabel`: `string` ++ `async`: `bool` (TODO) ++ `className`: `string` ++ `source`: `string` ++ `style`: `ImageStylePropTypes` + +#### ImageStylePropTypes + ++ `BackgroundPropTypes` ++ `BorderThemePropTypes` ++ `LayoutPropTypes` ++ `opacity`: `string` + +#### Examples + +```js +import {Image} from 'react-web-sdk'; +import React, {PropTypes} from 'react'; + +class Avatar extends React.Component { + static propTypes = { + size: PropTypes.oneOf(['small', 'normal', 'large']), + user: PropTypes.object + } + + static defaultProps = { + size: 'normal' + } + + render() { + return ( + + ); + } +} + +const style = { + base: { + borderColor: 'white', + borderRadius: '5px', + borderWidth: '5px' + }, + small: { + height: '32px', + width: '32px' + }, + normal: { + height: '48px', + width: '48px' + }, + large: { + height: '64px', + width: '32px' + } +} +``` diff --git a/docs/StyleProp.spec.md b/docs/StyleProp.spec.md new file mode 100644 index 00000000..cf88300e --- /dev/null +++ b/docs/StyleProp.spec.md @@ -0,0 +1,89 @@ +## StyleProp spec + +### Background + ++ `backgroundColor`: `string` ++ `backgroundImage`: `string` ++ `backgroundPosition`: `string` ++ `backgroundRepeat`: `string` ++ `backgroundSize`: `string` + +### BorderTheme + ++ `borderColor`: `string` ++ `borderTopColor`: `string` ++ `borderRightColor`: `string` ++ `borderBottomColor`: `string` ++ `borderLeftColor`: `string` ++ `borderStyle`: `string` ++ `borderRadius`: `number` or `string` ++ `borderTopLeftRadius`: `number` or `string` ++ `borderTopRightRadius`: `number` or `string` ++ `borderBottomLeftRadius`: `number` or `string` ++ `borderBottomRightRadius`: `number` or `string` + +### BoxModel + ++ `borderWidth`: `number` or `string` ++ `borderTopWidth`: `number` or `string` ++ `borderRightWidth`: `number` or `string` ++ `borderBottomWidth`: `number` or `string` ++ `borderLeftWidth`: `number` or `string` ++ `boxSizing`: `oneOf('border-box', 'content-box')` ++ `display`: `oneOf('block', 'flex', 'inline', 'inline-block', 'inline-flex')` ++ `height`: `number` or `string` ++ `margin`: `number` or `string` ++ `marginTop`: `number` or `string` ++ `marginRight`: `number` or `string` ++ `marginBottom`: `number` or `string` ++ `marginLeft`: `number` or `string` ++ `padding`: `number` or `string` ++ `paddingTop`: `number` or `string` ++ `paddingRight`: `number` or `string` ++ `paddingBottom`: `number` or `string` ++ `paddingLeft`: `number` or `string` ++ `width`: `number` or `string` + +### Flexbox + +* `alignContent`: `oneOf('center', 'flex-end', 'flex-start', 'stretch', 'space-around', 'space-between')` +* `alignItems`: `oneOf('baseline', 'center', 'flex-end', 'flex-start', 'stretch')` +* `alignSelf`: `oneOf('auto', 'baseline', 'center', 'flex-end', 'flex-start', 'stretch')` +* `flex`: `string` +* `flexBasis`: `string` +* `flexDirection`: `oneOf('column', 'column-reverse', 'row', 'row-reverse')` +* `flexGrow`: `number` +* `flexShrink`: `number` +* `flexWrap`: `oneOf('nowrap', 'wrap', 'wrap-reverse')` +* `justifyContent`: `oneOf('center', 'flex-end', 'flex-start', 'space-around', 'space-between')` +* `order`: `number` + +### Layout + +* BoxModel +* Flexbox +* Position + +### Position + +* `position`: `oneOf('absolute', 'fixed', 'relative')` +* `bottom`: `number` or `string` +* `left`: `number` or `string` +* `right`: `number` or `string` +* `top`: `number` or `string` +* `zIndex`: `number` + +### Typographic + +* `direction`: `oneOf('auto', 'ltr', 'rtl')` +* `fontFamily`: `string` +* `fontSize`: `string` +* `fontWeight`: `oneOf('100', '200', '300', '400', '500', '600', '700', '800', '900', 'bold', 'normal')` +* `fontStyle`: `oneOf('normal', 'italic')` +* `letterSpacing`: `string` +* `lineHeight`: `number` or `string` +* `textAlign`: `oneOf('auto', 'left', 'right', 'center')` +* `textDecoration`: `oneOf('none', 'underline')` +* `textTransform`: `oneOf('capitalize', 'lowercase', 'none', 'uppercase')` +* `wordWrap`: `oneOf('break-word', 'normal')` + diff --git a/docs/Text.spec.md b/docs/Text.spec.md new file mode 100644 index 00000000..08bef696 --- /dev/null +++ b/docs/Text.spec.md @@ -0,0 +1,70 @@ +# Text spec + +Text layout and styles. + +#### PropTypes + +All other props are transferred directly to the `element`. + ++ `element`: `func` or `string` (default `"div"`) ++ `style`: `TextStylePropTypes` + +#### TextStylePropTypes + ++ ViewStylePropTypes ++ TypographicPropTypes + +## Examples + +```js +import {Text} from 'react-web-sdk'; +import React, {PropTypes} from 'react'; + +class PrettyText extends React.Component { + static propTypes = { + color: PropTypes.oneOf(['white', 'gray', 'red']), + size: PropTypes.oneOf(['small', 'normal', 'large']), + weight: PropTypes.oneOf(['light', 'normal', 'bold']) + } + + static defaultProps = { + color: 'gray', + size: 'normal', + weight: 'normal' + } + + render() { + const { color, size, style, weight, ...other } = this.props; + + return ( + + ); + } +} + +const localStyle = { + color: { + white: { color: 'white' }, + gray: { color: 'gray' }, + red: { color: 'red' } + }, + size: { + small: { fontSize: '0.85rem', padding: '0.5rem' }, + normal: { fontSize: '1rem', padding: '0.75rem' }, + large: { fontSize: '1.5rem', padding: '1rem' } + }, + weight: { + light: { fontWeight: '300' }, + normal: { fontWeight: '400' }, + bold: { fontWeight: '700' } + } +} +``` diff --git a/docs/View.spec.md b/docs/View.spec.md new file mode 100644 index 00000000..12c39fcc --- /dev/null +++ b/docs/View.spec.md @@ -0,0 +1,66 @@ +# View spec + +`View` is a flexbox container and the fundamental building block for UI. It is +designed to be nested inside other `View`'s and to have 0-to-many children of +any type. + +## PropTypes + +All other props are transferred directly to the `element`. + ++ `element`: `func` or `string` (default `"div"`) ++ `pointerEvents`: `oneOf('all', 'box-only', 'box-none', 'none')` ++ `style`: `ViewStylePropTypes` + +## ViewStylePropTypes + ++ BackgroundPropTypes ++ BorderThemePropTypes ++ LayoutPropTypes ++ `boxShadow`: `string` ++ `color`: `string` ++ `opacity`: `number` + +## ViewStyleDefaultProps + +Implements the default styles from +[facebook/css-layout](https://github.com/facebook/css-layout). + +1. All the flex elements are oriented from top-to-bottom, left-to-right and do + not shrink. This is how things are laid out using the default CSS settings + and what you'd expect. + +2. The most convenient way to express the relation between width and other + box-model properties. + +3. Everything is `display:flex` by default. All the behaviors of `block` and + `inline-block` can be expressed in term of flex but not the opposite. + +4. Everything is `position:relative`. This makes `position:absolute` target the + direct parent and not some parent which is either relative or absolute. If + you want to position an element relative to something else, you should move + it in the DOM instead of relying of CSS. It also makes `top`, `left`, + `right`, `bottom` do something when not specifying `position:absolute`. + +```js +const ViewStyleDefaultProps = { + alignItems: 'stretch', // 1 + borderWidth: 0, + borderStyle: 'solid', + boxSizing: 'border-box', // 2 + display: 'flex', // 3 + flexBasis: 'auto', // 1 + flexDirection: 'column', // 1 + flexShrink: 0, // 1 + listStyle: 'none', + margin: 0, + padding: 0, + position: 'relative' // 4 +}; +``` + +## Examples + +```js +// TODO +``` diff --git a/docs/sdk-components.png b/docs/sdk-components.png new file mode 100644 index 00000000..66da579a Binary files /dev/null and b/docs/sdk-components.png differ diff --git a/docs/sdk-styling-strategy.png b/docs/sdk-styling-strategy.png new file mode 100644 index 00000000..850ef677 Binary files /dev/null and b/docs/sdk-styling-strategy.png differ diff --git a/docs/styling-strategy.md b/docs/styling-strategy.md new file mode 100644 index 00000000..66f03c92 --- /dev/null +++ b/docs/styling-strategy.md @@ -0,0 +1,126 @@ +# Styling strategy + +Using the `style` attribute would normally produce inline styles. There are +several existing approaches to using the `style` attribute, some of which +convert inline styles to static CSS: +[jsxstyle](https://github.com/petehunt/jsxstyle), +[react-free-style](https://github.com/blakeembrey/react-free-style/), +[react-inline](https://github.com/martinandert/react-inline), +[react-native](https://facebook.github.io/react-native/), +[react-style](https://github.com/js-next/react-style), +[stilr](https://github.com/kodyl/stilr). + +## Style syntax: native vs proprietary data structure + +React Web SDK (in a divergence from React Native) represents style using plain +JS objects: + +```js +... + +const styles = { + root: { + background: 'transparent', + display: 'flex', + flexGrow: 1, + justifyContent: 'center' + } +}; +``` + +Most approaches to managing style in React introduce a proprietary data +structure, often via an implementation of `Stylesheet.create`. + +```js +... + +const styles = Stylesheet.create({ + root: { + background: 'transparent', + display: 'flex', + flexGrow: 1, + justifyContent: 'center' + } +}); +``` + +## JS-to-CSS: conversion strategies + +One strategy for converting styles from JS to CSS is to map style objects to +CSS rules. Another strategy is to map declarations to declarataions. + +![](sdk-styling-strategy.png) + +Mapping entire `style` objects to CSS rules can lead to increasingly large CSS +files. Each new component adds new rules to the stylesheet. + +React Web SDK includes a proof-of-concept for the strategy of automatically +mapping unique declarations to declarations, via a unique selector for each +declaration. This strategy results in smaller CSS files because an application +has fewer unique declarations than total declarations. Creating a new +component with no new unique declarations results in no change to the CSS file. + +For example: + +```js +... + +const styles = { + root: { + background: 'transparent', + display: 'flex', + flexGrow: 1, + justifyContent: 'center' + } +}; +``` + +Yields: + +```html +... +``` + +And is backed by: + +```css +._abcde { background: transparent } +._fghij { display: flex } +._klmno { flex-grow: 1 } +._pqrst { justify-content: center } +``` + +The current implementation uses a precomputed CSS library – 200+ +single-declaration rules, with obfuscated selectors. This handles a signficant +portion of possible declarations. But a build-time implementation would produce +more accurate CSS files and fall through to inline styles significantly less +often. + + +(CSS libraries like [Atomic CSS](http://acss.io/), +[Basscss](http://www.basscss.com/), [SUIT CSS](https://suitcss.github.io/), and +[tachyons](http://tachyons.io/) are attempts to limit style scope and limit +stylesheet growth in a similar way. But they're CSS utility libraries, each with a +particular set of classes and features to learn. All of them require developers +to manually connect CSS classes for given styles.) + +## Dynamic styles: use inline styles + +Some styles cannot be resolved ahead of time and continue to rely on inline +styles: + +```js + 0.5 ? 'red' : 'black') }}>... +``` + +## Media Queries, pseudo-classes, and pseudo-elements + +Media Queries could be replaced with `mediaMatch`. This would have the added +benefit of co-locating breakpoint-specific DOM and style changes. Perhaps Media +Query data could be accessed on `this.content`? + +Pseudo-classes like `:hover` and `:focus` can be handled with JavaScript. + +Pseudo-elements should be avoided in general, but for particular cases like +`::placeholder` it might be necessary to reimplement it in the SDK's +`TextInput` component (see React Native's API). diff --git a/example.js b/example.js index 2b155732..91fd0e1b 100644 --- a/example.js +++ b/example.js @@ -8,7 +8,7 @@ class Example extends React.Component { {[1,2,3,4,5,6].map((item) => { return ( - {item} + {item} ); })} diff --git a/lib/components/Component.js b/lib/components/Component.js index f3d97661..14f5f01b 100644 --- a/lib/components/Component.js +++ b/lib/components/Component.js @@ -48,10 +48,8 @@ class Component extends React.Component { } _separateClassNamesAndStyles() { - const classNameProp = this.props.className; const styleProp = this.props.style; - - let classNames = [ classNameProp ]; + let classNames = [ this.props.className ]; let inlineStyles = {}; for (let prop in styleProp) {