diff --git a/docs/apis/StyleSheet.md b/docs/apis/StyleSheet.md index 4cc94d49..d4a340a3 100644 --- a/docs/apis/StyleSheet.md +++ b/docs/apis/StyleSheet.md @@ -9,6 +9,8 @@ outside of the render loop and are applied as inline styles. Read more about to **create**(obj: {[key: string]: any}) +Each key of the object passed to `create` must define a style object. + ## Example ```js @@ -24,12 +26,25 @@ const styles = StyleSheet.create({ }, activeTitle: { color: 'red', - }, + } }) ``` Use styles: +```js + + + +``` + +Or: + ```js ) } diff --git a/docs/components/Text.md b/docs/components/Text.md index 82a3ae1f..90eeb876 100644 --- a/docs/components/Text.md +++ b/docs/components/Text.md @@ -82,14 +82,14 @@ export default class PrettyText extends Component { color: PropTypes.oneOf(['white', 'gray', 'red']), size: PropTypes.oneOf(['small', 'normal', 'large']), weight: PropTypes.oneOf(['light', 'normal', 'bold']) - } + }; static defaultProps = { ...Text.defaultProps, color: 'gray', size: 'normal', weight: 'normal' - } + }; render() { const { color, size, style, weight, ...other } = this.props; @@ -97,32 +97,32 @@ export default class PrettyText extends Component { return ( ); } } -const styles = StyleSheet.create({ - 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' } - } +const colorStyles = StyleSheet.create({ + white: { color: 'white' }, + gray: { color: 'gray' }, + red: { color: 'red' } +}) + +const sizeStyles = StyleSheet.create({ + small: { fontSize: '0.85rem', padding: '0.5rem' }, + normal: { fontSize: '1rem', padding: '0.75rem' }, + large: { fontSize: '1.5rem', padding: '1rem' } +}) + +const weightStyles = StyleSheet.create({ + light: { fontWeight: '300' }, + normal: { fontWeight: '400' }, + bold: { fontWeight: '700' } }) ``` diff --git a/docs/components/TextInput.md b/docs/components/TextInput.md index 51301b5a..f9d53060 100644 --- a/docs/components/TextInput.md +++ b/docs/components/TextInput.md @@ -176,10 +176,10 @@ export default class TextInputExample extends Component { onBlur={this._onBlur.bind(this)} onFocus={this._onFocus.bind(this)} placeholder={`What's happening?`} - style={{ - ...styles.default - ...(this.state.isFocused && styles.focused) - }} + style={[ + styles.default + this.state.isFocused && styles.focused + ]} /> ); } diff --git a/docs/guides/style.md b/docs/guides/style.md index 93c24822..9ae1b920 100644 --- a/docs/guides/style.md +++ b/docs/guides/style.md @@ -1,8 +1,8 @@ # Style -React Native for Web relies on JavaScript to let you style your application. -Along with a novel JS-to-CSS conversion strategy, this allows you to avoid -issues arising from the [7 deadly sins of +React Native for Web relies on JavaScript to define styles for your +application. Along with a novel JS-to-CSS conversion strategy, this allows you +to avoid issues arising from the [7 deadly sins of CSS](https://speakerdeck.com/vjeux/react-css-in-js): 1. Global namespace @@ -53,16 +53,16 @@ A common pattern is to conditionally add style based on a condition: ```js // either - - -// or + +// or + ``` ## Composing styles @@ -82,7 +82,7 @@ class List extends React.Component { return ( {elements.map((element) => - + )} ); diff --git a/examples/components/App.js b/examples/components/App.js index 06f81a4c..81d9d849 100644 --- a/examples/components/App.js +++ b/examples/components/App.js @@ -18,15 +18,15 @@ export default class App extends React.Component { render() { const { mediaQuery } = this.props - const rootStyles = { - ...(styles.root.common), - ...(mediaQuery.small.matches && styles.root.mqSmall), - ...(mediaQuery.large.matches && styles.root.mqLarge) - } + const finalRootStyles = [ + rootStyles.common, + mediaQuery.small.matches && rootStyles.mqSmall, + mediaQuery.large.matches && rootStyles.mqLarge + ] return ( - + React Native for Web React Native Web takes the core components from React @@ -200,7 +200,7 @@ export default class App extends React.Component { style={styles.scrollViewStyle} > {Array.from({ length: 50 }).map((item, i) => ( - + {i} ))} @@ -212,19 +212,20 @@ export default class App extends React.Component { } } -const styles = StyleSheet.create({ - root: { - common: { - marginVertical: 0, - marginHorizontal: 'auto' - }, - mqSmall: { - maxWidth: '400px' - }, - mqLarge: { - maxWidth: '600px' - } +const rootStyles = StyleSheet.create({ + common: { + marginVertical: 0, + marginHorizontal: 'auto' }, + mqSmall: { + maxWidth: '400px' + }, + mqLarge: { + maxWidth: '600px' + } +}) + +const styles = StyleSheet.create({ row: { flexDirection: 'row', flexWrap: 'wrap' diff --git a/examples/components/Heading.js b/examples/components/Heading.js index a5dcf22e..cee5ad3f 100644 --- a/examples/components/Heading.js +++ b/examples/components/Heading.js @@ -1,24 +1,25 @@ import React, { StyleSheet, Text } from '../../src' +const sizeStyles = StyleSheet.create({ + xlarge: { + fontSize: '2rem', + marginBottom: '1em' + }, + large: { + fontSize: '1.5rem', + marginBottom: '1em', + marginTop: '1em' + }, + normal: { + fontSize: '1.25rem', + marginBottom: '0.5em', + marginTop: '0.5em' + } +}) + const styles = StyleSheet.create({ root: { fontFamily: '"Helvetica Neue", arial, sans-serif' - }, - size: { - xlarge: { - fontSize: '2rem', - marginBottom: '1em' - }, - large: { - fontSize: '1.5rem', - marginBottom: '1em', - marginTop: '1em' - }, - normal: { - fontSize: '1.25rem', - marginBottom: '0.5em', - marginTop: '0.5em' - } } }) @@ -26,7 +27,7 @@ const Heading = ({ children, size = 'normal' }) => ( ) diff --git a/examples/components/MediaQueryWidget.js b/examples/components/MediaQueryWidget.js index 487024eb..acbbb20e 100644 --- a/examples/components/MediaQueryWidget.js +++ b/examples/components/MediaQueryWidget.js @@ -5,12 +5,15 @@ const styles = StyleSheet.create({ alignItems: 'center', borderWidth: 1, marginVertical: 10, - padding: 10, - textAlign: 'center' + padding: 10 }, heading: { fontWeight: 'bold', - padding: 5 + padding: 5, + textAlign: 'center' + }, + text: { + textAlign: 'center' } }) @@ -28,7 +31,7 @@ const MediaQueryWidget = ({ mediaQuery = {} }) => { return ( Active Media Query - {`"${active.alias}"`} {active.mql && active.mql.media} + {`"${active.alias}"`} {active.mql && active.mql.media} ) } diff --git a/src/apis/AppRegistry/renderApplication.js b/src/apis/AppRegistry/renderApplication.js index f82c8286..844eeaf0 100644 --- a/src/apis/AppRegistry/renderApplication.js +++ b/src/apis/AppRegistry/renderApplication.js @@ -13,14 +13,13 @@ import ReactDOMServer from 'react-dom/server' import ReactNativeApp from './ReactNativeApp' import StyleSheet from '../../apis/StyleSheet' -const STYLESHEET_ID = 'react-stylesheet' -const renderStyleSheetToString = () => `` +const renderStyleSheetToString = () => `` export default function renderApplication(RootComponent: Component, initialProps: Object, rootTag: any) { invariant(rootTag, 'Expect to have a valid rootTag, instead got ', rootTag) // insert style sheet if needed - const styleElement = document.getElementById(STYLESHEET_ID) + const styleElement = document.getElementById(StyleSheet.elementId) if (!styleElement) { rootTag.insertAdjacentHTML('beforebegin', renderStyleSheetToString()) } const component = ( diff --git a/src/apis/StyleSheet/BorderPropTypes.js b/src/apis/StyleSheet/BorderPropTypes.js new file mode 100644 index 00000000..715baffb --- /dev/null +++ b/src/apis/StyleSheet/BorderPropTypes.js @@ -0,0 +1,25 @@ +import { PropTypes } from 'react' +import ColorPropType from '../../apis/StyleSheet/ColorPropType' + +const numberOrString = PropTypes.oneOfType([ PropTypes.number, PropTypes.string ]) +const BorderStylePropType = PropTypes.oneOf([ 'solid', 'dotted', 'dashed' ]) + +const BorderPropTypes = { + borderColor: ColorPropType, + borderTopColor: ColorPropType, + borderRightColor: ColorPropType, + borderBottomColor: ColorPropType, + borderLeftColor: ColorPropType, + borderRadius: numberOrString, + borderTopLeftRadius: numberOrString, + borderTopRightRadius: numberOrString, + borderBottomLeftRadius: numberOrString, + borderBottomRightRadius: numberOrString, + borderStyle: BorderStylePropType, + borderTopStyle: BorderStylePropType, + borderRightStyle: BorderStylePropType, + borderBottomStyle: BorderStylePropType, + borderLeftStyle: BorderStylePropType +} + +export default BorderPropTypes diff --git a/src/apis/StyleSheet/ColorPropType.js b/src/apis/StyleSheet/ColorPropType.js new file mode 100644 index 00000000..9cbe37dc --- /dev/null +++ b/src/apis/StyleSheet/ColorPropType.js @@ -0,0 +1,5 @@ +import { PropTypes } from 'react' + +const ColorPropType = PropTypes.string + +export default ColorPropType diff --git a/src/apis/StyleSheet/LayoutPropTypes.js b/src/apis/StyleSheet/LayoutPropTypes.js new file mode 100644 index 00000000..734a7724 --- /dev/null +++ b/src/apis/StyleSheet/LayoutPropTypes.js @@ -0,0 +1,54 @@ +import { PropTypes } from 'react' + +const { number, oneOf, oneOfType, string } = PropTypes +const numberOrString = oneOfType([ number, string ]) + +const LayoutPropTypes = { + // box model + borderWidth: numberOrString, + borderBottomWidth: numberOrString, + borderLeftWidth: numberOrString, + borderRightWidth: numberOrString, + borderTopWidth: numberOrString, + boxSizing: string, + height: numberOrString, + margin: numberOrString, + marginBottom: numberOrString, + marginHorizontal: numberOrString, + marginLeft: numberOrString, + marginRight: numberOrString, + marginTop: numberOrString, + marginVertical: numberOrString, + maxHeight: numberOrString, + maxWidth: numberOrString, + minHeight: numberOrString, + minWidth: numberOrString, + padding: numberOrString, + paddingBottom: numberOrString, + paddingHorizontal: numberOrString, + paddingLeft: numberOrString, + paddingRight: numberOrString, + paddingTop: numberOrString, + paddingVertical: numberOrString, + width: numberOrString, + // flexbox + alignContent: oneOf([ 'center', 'flex-end', 'flex-start', 'space-around', 'space-between', 'stretch' ]), + alignItems: oneOf([ 'baseline', 'center', 'flex-end', 'flex-start', 'stretch' ]), + alignSelf: oneOf([ 'auto', 'baseline', 'center', 'flex-end', 'flex-start', 'stretch' ]), + flex: number, + 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, + // position + bottom: numberOrString, + left: numberOrString, + position: oneOf([ 'absolute', 'fixed', 'relative', 'static' ]), + right: numberOrString, + top: numberOrString +} + +export default LayoutPropTypes diff --git a/src/apis/StyleSheet/Store.js b/src/apis/StyleSheet/Store.js index 5f23c37f..01353ec2 100644 --- a/src/apis/StyleSheet/Store.js +++ b/src/apis/StyleSheet/Store.js @@ -1,5 +1,4 @@ import hyphenate from './hyphenate' -import normalizeValue from './normalizeValue' import prefixer from './prefixer' export default class Store { @@ -14,18 +13,16 @@ export default class Store { } get(property, value) { - const normalizedValue = normalizeValue(property, value) - const key = this._getDeclarationKey(property, normalizedValue) + const key = this._getDeclarationKey(property, value) return this._classNames[key] } set(property, value) { if (value != null) { - const normalizedValue = normalizeValue(property, value) const values = this._getPropertyValues(property) || [] - if (values.indexOf(normalizedValue) === -1) { - values.push(normalizedValue) - this._setClassName(property, normalizedValue) + if (values.indexOf(value) === -1) { + values.push(value) + this._setClassName(property, value) this._setPropertyValues(property, values) } } @@ -80,7 +77,7 @@ export default class Store { } _setPropertyValues(property, values) { - this._declarations[property] = values.map(value => normalizeValue(property, value)) + this._declarations[property] = values.map(value => value) } _setClassName(property, value) { diff --git a/src/apis/StyleSheet/StylePropTypes.js b/src/apis/StyleSheet/StylePropTypes.js deleted file mode 100644 index 42bf8231..00000000 --- a/src/apis/StyleSheet/StylePropTypes.js +++ /dev/null @@ -1,112 +0,0 @@ -import { PropTypes } from 'react' - -const { number, oneOf, oneOfType, string } = PropTypes -const numberOrString = oneOfType([ number, string ]) - -/** - * Any properties marked @private are used internally in resets or property - * mappings. - * - * https://developer.mozilla.org/en-US/docs/Web/CSS/Reference - */ -export default { - alignContent: oneOf([ 'center', 'flex-end', 'flex-start', 'space-around', 'space-between', 'stretch' ]), - alignItems: oneOf([ 'baseline', 'center', 'flex-end', 'flex-start', 'stretch' ]), - alignSelf: oneOf([ 'auto', 'baseline', 'center', 'flex-end', 'flex-start', 'stretch' ]), - appearance: string, - backfaceVisibility: string, - backgroundAttachment: oneOf([ 'fixed', 'local', 'scroll' ]), - backgroundClip: string, - backgroundColor: string, - backgroundImage: string, - backgroundOrigin: oneOf([ 'border-box', 'content-box', 'padding-box' ]), - backgroundPosition: string, - backgroundRepeat: string, - backgroundSize: string, - borderColor: string, - borderBottomColor: string, - borderLeftColor: string, - borderRightColor: string, - borderTopColor: string, - borderRadius: numberOrString, - borderTopLeftRadius: numberOrString, - borderTopRightRadius: numberOrString, - borderBottomLeftRadius: numberOrString, - borderBottomRightRadius: numberOrString, - borderStyle: string, - borderBottomStyle: string, - borderLeftStyle: string, - borderRightStyle: string, - borderTopStyle: string, - borderWidth: numberOrString, - borderBottomWidth: numberOrString, - borderLeftWidth: numberOrString, - borderRightWidth: numberOrString, - borderTopWidth: numberOrString, - bottom: numberOrString, - boxShadow: string, - boxSizing: oneOf([ 'border-box', 'content-box' ]), - clear: string, - color: string, - cursor: string, - display: string, - direction: string, /* @private */ - flex: number, - flexBasis: string, - flexDirection: oneOf([ 'column', 'column-reverse', 'row', 'row-reverse' ]), - flexGrow: number, - flexShrink: number, - flexWrap: oneOf([ 'nowrap', 'wrap', 'wrap-reverse' ]), - float: oneOf([ 'left', 'none', 'right' ]), - font: string, /* @private */ - fontFamily: string, - fontSize: numberOrString, - fontStyle: string, - fontWeight: string, - height: numberOrString, - justifyContent: oneOf([ 'center', 'flex-end', 'flex-start', 'space-around', 'space-between' ]), - left: numberOrString, - letterSpacing: string, - lineHeight: numberOrString, - listStyle: string, - margin: numberOrString, - marginBottom: numberOrString, - marginHorizontal: numberOrString, - marginLeft: numberOrString, - marginRight: numberOrString, - marginTop: numberOrString, - marginVertical: numberOrString, - maxHeight: numberOrString, - maxWidth: numberOrString, - minHeight: numberOrString, - minWidth: numberOrString, - opacity: numberOrString, - order: numberOrString, - outline: string, - overflow: string, - overflowX: string, - overflowY: string, - padding: numberOrString, - paddingBottom: numberOrString, - paddingHorizontal: numberOrString, - paddingLeft: numberOrString, - paddingRight: numberOrString, - paddingTop: numberOrString, - paddingVertical: numberOrString, - position: oneOf([ 'absolute', 'fixed', 'relative', 'static' ]), - right: numberOrString, - textAlign: oneOf([ 'center', 'inherit', 'justify', 'justify-all', 'left', 'right' ]), - textDecoration: string, - textOverflow: string, - textShadow: string, - textTransform: oneOf([ 'capitalize', 'lowercase', 'none', 'uppercase' ]), - top: numberOrString, - userSelect: string, - verticalAlign: string, - visibility: oneOf([ 'hidden', 'visible' ]), - whiteSpace: string, - width: numberOrString, - wordWrap: string, - writingDirection: string, - zIndex: numberOrString -} diff --git a/src/apis/StyleSheet/StyleSheetPropType.js b/src/apis/StyleSheet/StyleSheetPropType.js new file mode 100644 index 00000000..0e2467f6 --- /dev/null +++ b/src/apis/StyleSheet/StyleSheetPropType.js @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * @flow + */ + +import createStrictShapeTypeChecker from './createStrictShapeTypeChecker' +import flattenStyle from './flattenStyle' + +export default function StyleSheetPropType(shape) { + const shapePropType = createStrictShapeTypeChecker(shape) + return function (props, propName, componentName, location?) { + let newProps = props + if (props[propName]) { + // Just make a dummy prop object with only the flattened style + newProps = {} + newProps[propName] = flattenStyle(props[propName]) + } + return shapePropType(newProps, propName, componentName, location) + } +} diff --git a/src/apis/StyleSheet/StyleSheetRegistry.js b/src/apis/StyleSheet/StyleSheetRegistry.js new file mode 100644 index 00000000..87275546 --- /dev/null +++ b/src/apis/StyleSheet/StyleSheetRegistry.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2016-present, Nicolas Gallagher. + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * @flow + */ + +import flattenStyle from './flattenStyle' +import prefixer from './prefixer' + +export default class StyleSheetRegistry { + static registerStyle(style: Object, store): number { + if (process.env.NODE_ENV !== 'production') { + Object.freeze(style) + } + + const normalizedStyle = flattenStyle(style) + Object.keys(normalizedStyle).forEach((prop) => { + // add each declaration to the store + store.set(prop, normalizedStyle[prop]) + }) + } + + static getStyleAsNativeProps(style, store) { + let _className + let _style = {} + const classList = [] + const normalizedStyle = flattenStyle(style) + + for (const prop in normalizedStyle) { + let styleClass = store.get(prop, normalizedStyle[prop]) + + if (styleClass) { + classList.push(styleClass) + } else { + _style[prop] = normalizedStyle[prop] + } + } + + _className = classList.join(' ') + _style = prefixer.prefix(_style) + + return { className: _className, style: _style } + } +} diff --git a/src/apis/StyleSheet/StyleSheetValidation.js b/src/apis/StyleSheet/StyleSheetValidation.js new file mode 100644 index 00000000..2b5fa8ee --- /dev/null +++ b/src/apis/StyleSheet/StyleSheetValidation.js @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2016-present, Nicolas Gallagher. + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + + * @flow + */ + +import { PropTypes } from 'react' +import ImageStylePropTypes from '../../components/Image/ImageStylePropTypes' +import TextStylePropTypes from '../../components/Text/TextStylePropTypes' +import ViewStylePropTypes from '../../components/View/ViewStylePropTypes' +import invariant from 'invariant' + +export default class StyleSheetValidation { + static validateStyleProp(prop, style, caller) { + if (process.env.NODE_ENV !== 'production') { + if (allStylePropTypes[prop] === undefined) { + const message1 = `"${prop}" is not a valid style property.` + const message2 = '\nValid style props: ' + JSON.stringify(Object.keys(allStylePropTypes).sort(), null, ' ') + styleError(message1, style, caller, message2) + } + const error = allStylePropTypes[prop](style, prop, caller, 'prop') + if (error) { + styleError(error.message, style, caller) + } + } + } + + static validateStyle(name, styles) { + if (process.env.NODE_ENV !== 'production') { + for (const prop in styles[name]) { + StyleSheetValidation.validateStyleProp(prop, styles[name], 'StyleSheet ' + name) + } + } + } + + static addValidStylePropTypes(stylePropTypes) { + for (const key in stylePropTypes) { + allStylePropTypes[key] = stylePropTypes[key] + } + } +} + +const styleError = (message1, style, caller, message2) => { + invariant( + false, + message1 + '\n' + (caller || '<>') + ': ' + + JSON.stringify(style, null, ' ') + (message2 || '') + ) +} + +const allStylePropTypes = {} + +StyleSheetValidation.addValidStylePropTypes(ImageStylePropTypes) +StyleSheetValidation.addValidStylePropTypes(TextStylePropTypes) +StyleSheetValidation.addValidStylePropTypes(ViewStylePropTypes) +StyleSheetValidation.addValidStylePropTypes({ + appearance: PropTypes.string, + clear: PropTypes.string, + cursor: PropTypes.string, + display: PropTypes.string, + direction: PropTypes.string, /* @private */ + float: PropTypes.oneOf([ 'left', 'none', 'right' ]), + font: PropTypes.string, /* @private */ + listStyle: PropTypes.string, + verticalAlign: PropTypes.string +}) diff --git a/src/apis/StyleSheet/TransformPropTypes.js b/src/apis/StyleSheet/TransformPropTypes.js new file mode 100644 index 00000000..53b933c5 --- /dev/null +++ b/src/apis/StyleSheet/TransformPropTypes.js @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * @flow + */ + +import { PropTypes } from 'react' + +const ArrayOfNumberPropType = PropTypes.arrayOf(PropTypes.number) +const numberOrString = PropTypes.oneOfType([ PropTypes.number, PropTypes.string ]) + +const TransformMatrixPropType = function ( + props : Object, + propName : string, + componentName : string +) : ?Error { + if (props.transform && props.transformMatrix) { + return new Error( + 'transformMatrix and transform styles cannot be used on the same ' + + 'component' + ) + } + return ArrayOfNumberPropType(props, propName, componentName) +} + +const TransformPropTypes = { + transform: PropTypes.arrayOf( + PropTypes.oneOfType([ + PropTypes.shape({ perspective: numberOrString }), + PropTypes.shape({ rotate: numberOrString }), + PropTypes.shape({ rotateX: numberOrString }), + PropTypes.shape({ rotateY: numberOrString }), + PropTypes.shape({ rotateZ: numberOrString }), + PropTypes.shape({ scale: numberOrString }), + PropTypes.shape({ scaleX: numberOrString }), + PropTypes.shape({ scaleY: numberOrString }), + PropTypes.shape({ skewX: numberOrString }), + PropTypes.shape({ skewY: numberOrString }), + PropTypes.shape({ translateX: numberOrString }), + PropTypes.shape({ translateY: numberOrString }) + ]) + ), + transformMatrix: TransformMatrixPropType +} + +export default TransformPropTypes diff --git a/src/apis/StyleSheet/__tests__/Store-test.js b/src/apis/StyleSheet/__tests__/Store-test.js index 6922220f..b83cdf0c 100644 --- a/src/apis/StyleSheet/__tests__/Store-test.js +++ b/src/apis/StyleSheet/__tests__/Store-test.js @@ -66,20 +66,6 @@ suite('apis/StyleSheet/Store', () => { }) }) - test('value normalization', () => { - const store = new Store() - store.set('flexGrow', 0) - store.set('margin', 0) - assert.deepEqual(store._declarations, { - flexGrow: [ 0 ], - margin: [ '0px' ] - }) - assert.deepEqual(store._classNames, { - 'flexGrow:0': 'flexGrow:0', - 'margin:0px': 'margin:0px' - }) - }) - test('replaces space characters', () => { const store = new Store() @@ -95,7 +81,7 @@ suite('apis/StyleSheet/Store', () => { store.set('backgroundColor', 'rgba(0,0,0,0)') store.set('color', '#fff') store.set('fontFamily', '"Helvetica Neue", Arial, sans-serif') - store.set('marginBottom', 0) + store.set('marginBottom', '0px') store.set('width', '100%') const expected = '/* 6 unique declarations */\n' + @@ -112,10 +98,10 @@ suite('apis/StyleSheet/Store', () => { test('obfuscated style sheet', () => { const store = new Store({}, { obfuscateClassNames: true }) store.set('alignItems', 'center') - store.set('marginBottom', 0) - store.set('margin', 1) - store.set('margin', 2) - store.set('margin', 3) + store.set('marginBottom', '0px') + store.set('margin', '1px') + store.set('margin', '2px') + store.set('margin', '3px') const expected = '/* 5 unique declarations */\n' + '._s_1{align-items:center;}\n' + diff --git a/src/apis/StyleSheet/__tests__/expandStyle-test.js b/src/apis/StyleSheet/__tests__/expandStyle-test.js index 521e29bb..74df33b1 100644 --- a/src/apis/StyleSheet/__tests__/expandStyle-test.js +++ b/src/apis/StyleSheet/__tests__/expandStyle-test.js @@ -14,14 +14,14 @@ suite('apis/StyleSheet/expandStyle', () => { } const expected = { - borderTopWidth: 1, - borderLeftWidth: 2, - borderRightWidth: 2, - borderBottomWidth: 2, - marginTop: 50, - marginBottom: 25, - marginLeft: 10, - marginRight: 10 + borderTopWidth: '1px', + borderLeftWidth: '2px', + borderRightWidth: '2px', + borderBottomWidth: '2px', + marginTop: '50px', + marginBottom: '25px', + marginLeft: '10px', + marginRight: '10px' } assert.deepEqual(expandStyle(initial), expected) diff --git a/src/apis/StyleSheet/__tests__/flattenStyle-test.js b/src/apis/StyleSheet/__tests__/flattenStyle-test.js new file mode 100644 index 00000000..0ebaa74c --- /dev/null +++ b/src/apis/StyleSheet/__tests__/flattenStyle-test.js @@ -0,0 +1,51 @@ +/* eslint-env mocha */ + +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import assert from 'assert' +import flattenStyle from '../flattenStyle' + +suite('flattenStyle', () => { + test('should merge style objects', () => { + const style1 = {opacity: 1} + const style2 = {order: 2} + const flatStyle = flattenStyle([style1, style2]) + assert.equal(flatStyle.opacity, 1) + assert.equal(flatStyle.order, 2) + }) + + test('should override style properties', () => { + const style1 = {backgroundColor: '#000', order: 1} + const style2 = {backgroundColor: '#023c69', order: null} + const flatStyle = flattenStyle([style1, style2]) + assert.equal(flatStyle.backgroundColor, '#023c69') + assert.strictEqual(flatStyle.order, null) + }) + + test('should overwrite properties with `undefined`', () => { + const style1 = {backgroundColor: '#000'} + const style2 = {backgroundColor: undefined} + const flatStyle = flattenStyle([style1, style2]) + assert.strictEqual(flatStyle.backgroundColor, undefined) + }) + + test('should not fail on falsy values', () => { + assert.doesNotThrow(() => flattenStyle([null, false, undefined])) + }) + + test('should recursively flatten arrays', () => { + const style1 = {order: 2} + const style2 = {opacity: 1} + const style3 = {order: 3} + const flatStyle = flattenStyle([null, [], [style1, style2], style3]) + assert.equal(flatStyle.order, 3) + assert.equal(flatStyle.opacity, 1) + }) +}) diff --git a/src/apis/StyleSheet/__tests__/getStyleObjects-test.js b/src/apis/StyleSheet/__tests__/getStyleObjects-test.js deleted file mode 100644 index b29759a1..00000000 --- a/src/apis/StyleSheet/__tests__/getStyleObjects-test.js +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-env mocha */ - -import assert from 'assert' -import getStyleObjects from '../getStyleObjects' - -const fixture = { - rule: { - margin: 0, - padding: 0 - }, - nested: { - auto: { - backgroundSize: 'auto' - }, - contain: { - backgroundSize: 'contain' - } - }, - position: { - left: { left: 0 }, - right: { right: 0 } - } -} - -suite('apis/StyleSheet/getStyleObjects', () => { - test('returns only style objects', () => { - const actual = getStyleObjects(fixture) - assert.deepEqual(actual, [ - { margin: 0, padding: 0 }, - { backgroundSize: 'auto' }, - { backgroundSize: 'contain' }, - { left: 0 }, - { right: 0 } - ]) - }) -}) diff --git a/src/apis/StyleSheet/__tests__/index-test.js b/src/apis/StyleSheet/__tests__/index-test.js index da00a625..cfbbad37 100644 --- a/src/apis/StyleSheet/__tests__/index-test.js +++ b/src/apis/StyleSheet/__tests__/index-test.js @@ -17,7 +17,7 @@ suite('apis/StyleSheet', () => { setup(() => { document.body.appendChild(div) StyleSheet.create(styles) - div.innerHTML = `` + div.innerHTML = `` }) teardown(() => { @@ -32,7 +32,7 @@ suite('apis/StyleSheet', () => { StyleSheet.create({ root: { color: 'red' } }) assert.equal( - document.getElementById('react-stylesheet').textContent, + document.getElementById(StyleSheet.elementId).textContent, `${resetCSS}\n${predefinedCSS}\n` + `/* 5 unique declarations */\n` + `.borderBottomWidth\\:1px{border-bottom-width:1px;}\n` + @@ -45,8 +45,8 @@ suite('apis/StyleSheet', () => { }) test('resolve', () => { - const props = { className: 'className', style: styles.root } - const expected = { className: 'className borderTopWidth:1px borderRightWidth:1px borderBottomWidth:1px borderLeftWidth:1px', style: {} } + const props = { style: styles.root } + const expected = { className: 'borderTopWidth:1px borderRightWidth:1px borderBottomWidth:1px borderLeftWidth:1px', style: {} } StyleSheet.create(styles) assert.deepEqual(StyleSheet.resolve(props), expected) }) diff --git a/src/apis/StyleSheet/__tests__/isObject-test.js b/src/apis/StyleSheet/__tests__/isObject-test.js deleted file mode 100644 index 756216c0..00000000 --- a/src/apis/StyleSheet/__tests__/isObject-test.js +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint-env mocha */ - -import assert from 'assert' -import isObject from '../isObject' - -suite('apis/StyleSheet/isObject', () => { - test('returns "true" for objects', () => { - assert.ok(isObject({}) === true) - }) - test('returns "false" for non-objects', () => { - assert.ok(isObject(function () {}) === false) - assert.ok(isObject([]) === false) - assert.ok(isObject('') === false) - }) -}) diff --git a/src/apis/StyleSheet/__tests__/isStyleObject-test.js b/src/apis/StyleSheet/__tests__/isStyleObject-test.js deleted file mode 100644 index aded3cc0..00000000 --- a/src/apis/StyleSheet/__tests__/isStyleObject-test.js +++ /dev/null @@ -1,29 +0,0 @@ -/* eslint-env mocha */ - -import assert from 'assert' -import isStyleObject from '../isStyleObject' - -const styles = { - root: { - margin: 0 - }, - align: { - left: { - textAlign: 'left' - }, - right: { - textAlign: 'right' - } - } -} - -suite('apis/StyleSheet/isStyleObject', () => { - test('returns "false" for non-style objects', () => { - assert.ok(isStyleObject(styles) === false) - assert.ok(isStyleObject(styles.align) === false) - }) - test('returns "true" for style objects', () => { - assert.ok(isStyleObject(styles.root) === true) - assert.ok(isStyleObject(styles.align.left) === true) - }) -}) diff --git a/src/apis/StyleSheet/createStrictShapeTypeChecker.js b/src/apis/StyleSheet/createStrictShapeTypeChecker.js new file mode 100644 index 00000000..3fa66f8d --- /dev/null +++ b/src/apis/StyleSheet/createStrictShapeTypeChecker.js @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ + +import ReactPropTypeLocationNames from 'react/lib/ReactPropTypeLocationNames' +import invariant from 'invariant' + +export default function createStrictShapeTypeChecker(shapeTypes) { + function checkType(isRequired, props, propName, componentName, location?) { + if (!props[propName]) { + if (isRequired) { + invariant( + false, + `Required object \`${propName}\` was not specified in ` + + `\`${componentName}\`.` + ) + } + return + } + const propValue = props[propName] + const propType = typeof propValue + const locationName = location && ReactPropTypeLocationNames[location] || '(unknown)' + if (propType !== 'object') { + invariant( + false, + `Invalid ${locationName} \`${propName}\` of type \`${propType}\` ` + + `supplied to \`${componentName}\`, expected \`object\`.` + ) + } + // We need to check all keys in case some are required but missing from + // props. + const allKeys = { ...props[propName], ...shapeTypes } + for (const key in allKeys) { + const checker = shapeTypes[key] + if (!checker) { + invariant( + false, + `Invalid props.${propName} key \`${key}\` supplied to \`${componentName}\`.` + + `\nBad object: ` + JSON.stringify(props[propName], null, ' ') + + `\nValid keys: ` + JSON.stringify(Object.keys(shapeTypes), null, ' ') + ) + } + const error = checker(propValue, key, componentName, location) + if (error) { + invariant( + false, + error.message + `\nBad object: ` + JSON.stringify(props[propName], null, ' ') + ) + } + } + } + + function chainedCheckType( + props: {[key: string]: any}, + propName: string, + componentName: string, + location?: string + ): ?Error { + return checkType(false, props, propName, componentName, location) + } + chainedCheckType.isRequired = checkType.bind(null, true) + return chainedCheckType +} diff --git a/src/apis/StyleSheet/expandStyle.js b/src/apis/StyleSheet/expandStyle.js index 31b2c6c6..6937dece 100644 --- a/src/apis/StyleSheet/expandStyle.js +++ b/src/apis/StyleSheet/expandStyle.js @@ -1,3 +1,5 @@ +import normalizeValue from './normalizeValue' + const styleShortHands = { borderColor: [ 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor' ], borderRadius: [ 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius' ], @@ -37,7 +39,8 @@ const expandStyle = (style) => { return sortedProps.reduce((resolvedStyle, key) => { const expandedProps = styleShortHands[key] - const value = style[key] + const value = normalizeValue(key, style[key]) + if (expandedProps) { expandedProps.forEach((prop, i) => { resolvedStyle[expandedProps[i]] = value diff --git a/src/apis/StyleSheet/flattenStyle.js b/src/apis/StyleSheet/flattenStyle.js new file mode 100644 index 00000000..82fe814b --- /dev/null +++ b/src/apis/StyleSheet/flattenStyle.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2016-present, Nicolas Gallagher. + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * @flow + */ +import invariant from 'invariant' +import expandStyle from './expandStyle' + +export default function flattenStyle(style): ?Object { + if (!style) { + return undefined + } + + invariant(style !== true, 'style may be false but not true') + + if (!Array.isArray(style)) { + // we must expand styles during the flattening because expanded styles + // override shorthands + return expandStyle(style) + } + + const result = {} + for (let i = 0; i < style.length; ++i) { + const computedStyle = flattenStyle(style[i]) + if (computedStyle) { + for (const key in computedStyle) { + result[key] = computedStyle[key] + } + } + } + return result +} diff --git a/src/apis/StyleSheet/getStyleObjects.js b/src/apis/StyleSheet/getStyleObjects.js deleted file mode 100644 index ebec0aba..00000000 --- a/src/apis/StyleSheet/getStyleObjects.js +++ /dev/null @@ -1,22 +0,0 @@ -import isObject from './isObject' -import isStyleObject from './isStyleObject' - -/** - * Recursively check for objects that are style rules. - */ -const getStyleObjects = (styles: Object): Array => { - const keys = Object.keys(styles) - return keys.reduce((rules, key) => { - const possibleRule = styles[key] - if (isObject(possibleRule)) { - if (isStyleObject(possibleRule)) { - rules.push(possibleRule) - } else { - rules = rules.concat(getStyleObjects(possibleRule)) - } - } - return rules - }, []) -} - -export default getStyleObjects diff --git a/src/apis/StyleSheet/index.js b/src/apis/StyleSheet/index.js index 1507d7fd..a39f0f7a 100644 --- a/src/apis/StyleSheet/index.js +++ b/src/apis/StyleSheet/index.js @@ -1,9 +1,12 @@ import { resetCSS, predefinedCSS, predefinedClassNames } from './predefs' -import expandStyle from './expandStyle' -import getStyleObjects from './getStyleObjects' -import prefixer from './prefixer' +import flattenStyle from './flattenStyle' import Store from './Store' -import StylePropTypes from './StylePropTypes' +import StyleSheetRegistry from './StyleSheetRegistry' +import StyleSheetValidation from './StyleSheetValidation' + +const ELEMENT_ID = 'react-stylesheet' +let isRendered = false +let lastStyleSheet = '' /** * Initialize the store with pointer-event styles mapping to our custom pointer @@ -13,7 +16,6 @@ const initialState = { classNames: predefinedClassNames } const options = { obfuscateClassNames: !(process.env.NODE_ENV !== 'production') } const createStore = () => new Store(initialState, options) let store = createStore() -let isRendered = false /** * Destroy existing styles @@ -32,51 +34,26 @@ const _renderToString = () => { return `${resetCSS}\n${predefinedCSS}\n${css}` } -/** - * Process all unique declarations - */ const create = (styles: Object): Object => { - const rules = getStyleObjects(styles) - - rules.forEach((rule) => { - const style = expandStyle(rule) - - Object.keys(style).forEach((property) => { - if (!StylePropTypes[property]) { - console.error(`ReactNativeWeb: the style property "${property}" is not supported`) - } else { - const value = style[property] - // add each declaration to the store - store.set(property, value) - } - }) - }) + for (const key in styles) { + StyleSheetValidation.validateStyle(key, styles) + StyleSheetRegistry.registerStyle(styles[key], store) + } // update the style sheet in place if (isRendered) { - const stylesheet = document.getElementById('react-stylesheet') + const stylesheet = document.getElementById(ELEMENT_ID) if (stylesheet) { - stylesheet.textContent = _renderToString() + const newStyleSheet = _renderToString() + if (lastStyleSheet !== newStyleSheet) { + stylesheet.textContent = newStyleSheet + lastStyleSheet = newStyleSheet + } } else if (process.env.NODE_ENV !== 'production') { console.error('ReactNative: cannot find "react-stylesheet" element') } } - if (process.env.NODE_ENV !== 'production') { - const deepFreeze = (obj) => { - const propNames = Object.getOwnPropertyNames(obj) - propNames.forEach((name) => { - const prop = obj[name] - if (typeof prop === 'object' && prop !== null && !Object.isFrozen(prop)) { - deepFreeze(prop) - } - }) - return Object.freeze(obj) - } - - deepFreeze(styles) - } - return styles } @@ -84,34 +61,15 @@ const create = (styles: Object): Object => { * Accepts React props and converts inline styles to single purpose classes * where possible. */ -const resolve = ({ className = '', style = {} }) => { - let _className - let _style = {} - const expandedStyle = expandStyle(style) - - const classList = [ className ] - for (const prop in expandedStyle) { - if (!StylePropTypes[prop]) { - continue - } - let styleClass = store.get(prop, expandedStyle[prop]) - - if (styleClass) { - classList.push(styleClass) - } else { - _style[prop] = expandedStyle[prop] - } - } - - _className = classList.join(' ') - _style = prefixer.prefix(_style) - - return { className: _className, style: _style } +const resolve = ({ style = {} }) => { + return StyleSheetRegistry.getStyleAsNativeProps(style, store) } export default { _destroy, _renderToString, create, + elementId: ELEMENT_ID, + flatten: flattenStyle, resolve } diff --git a/src/apis/StyleSheet/isObject.js b/src/apis/StyleSheet/isObject.js deleted file mode 100644 index 056792fd..00000000 --- a/src/apis/StyleSheet/isObject.js +++ /dev/null @@ -1,5 +0,0 @@ -const isObject = (obj) => { - return Object.prototype.toString.call(obj) === '[object Object]' -} - -export default isObject diff --git a/src/apis/StyleSheet/isStyleObject.js b/src/apis/StyleSheet/isStyleObject.js deleted file mode 100644 index 96341815..00000000 --- a/src/apis/StyleSheet/isStyleObject.js +++ /dev/null @@ -1,11 +0,0 @@ -import isObject from './isObject' - -const isStyleObject = (obj) => { - const values = Object.keys(obj).map((key) => obj[key]) - for (let i = 0; i < values.length; i += 1) { - if (isObject(values[i])) { return false } - } - return true -} - -export default isStyleObject diff --git a/src/apis/StyleSheet/normalizeValue.js b/src/apis/StyleSheet/normalizeValue.js index c91e9698..31f2e2ae 100644 --- a/src/apis/StyleSheet/normalizeValue.js +++ b/src/apis/StyleSheet/normalizeValue.js @@ -23,11 +23,11 @@ const unitlessNumbers = { strokeWidth: true } -const normalizeValues = (property, value) => { +const normalizeValue = (property, value) => { if (!unitlessNumbers[property] && typeof value === 'number') { value = `${value}px` } return value } -export default normalizeValues +export default normalizeValue diff --git a/src/components/ActivityIndicator/index.js b/src/components/ActivityIndicator/index.js index a9c16e5b..a26c5215 100644 --- a/src/components/ActivityIndicator/index.js +++ b/src/components/ActivityIndicator/index.js @@ -24,7 +24,7 @@ export default class ActivityIndicator extends Component { color: PropTypes.string, hidesWhenStopped: PropTypes.bool, size: PropTypes.oneOf(['small', 'large']), - style: PropTypes.object + style: View.propTypes.style }; static defaultProps = { @@ -65,25 +65,20 @@ export default class ActivityIndicator extends Component { } = this.props return ( - + { this._indicatorRef = c }} - style={{ - ...styles.indicator[size], - ...(hidesWhenStopped && !animating && styles.hidesWhenStopped), - borderColor: color - }} + style={[ + indicatorStyles[size], + hidesWhenStopped && !animating && styles.hidesWhenStopped, + { borderColor: color } + ]} /> ) } } -const indicatorStyle = StyleSheet.create({ - borderRadius: 100, - borderWidth: 3 -}) - const styles = StyleSheet.create({ container: { alignItems: 'center', @@ -91,18 +86,20 @@ const styles = StyleSheet.create({ }, hidesWhenStopped: { visibility: 'hidden' - }, - indicator: { - small: { - ...indicatorStyle, - width: 20, - height: 20 - }, - large: { - ...indicatorStyle, - borderWidth: 4, - width: 36, - height: 36 - } + } +}) + +const indicatorStyles = StyleSheet.create({ + small: { + borderRadius: 100, + borderWidth: 3, + width: 20, + height: 20 + }, + large: { + borderRadius: 100, + borderWidth: 4, + width: 36, + height: 36 } }) diff --git a/src/components/CoreComponent/index.js b/src/components/CoreComponent/index.js index e786d00a..323bc163 100644 --- a/src/components/CoreComponent/index.js +++ b/src/components/CoreComponent/index.js @@ -1,5 +1,4 @@ import React, { Component, PropTypes } from 'react' -import StylePropTypes from '../../apis/StyleSheet/StylePropTypes' import StyleSheet from '../../apis/StyleSheet' const roleComponents = { @@ -24,9 +23,8 @@ export default class CoreComponent extends Component { accessibilityLiveRegion: PropTypes.oneOf([ 'assertive', 'off', 'polite' ]), accessibilityRole: PropTypes.string, accessible: PropTypes.bool, - className: PropTypes.string, component: PropTypes.oneOfType([ PropTypes.func, PropTypes.string ]), - style: PropTypes.object, + style: PropTypes.oneOfType([ PropTypes.array, PropTypes.object ]), testID: PropTypes.string, type: PropTypes.string }; @@ -36,8 +34,6 @@ export default class CoreComponent extends Component { component: 'div' }; - static stylePropTypes = StylePropTypes; - render() { const { accessibilityLabel, diff --git a/src/components/Image/ImageStylePropTypes.js b/src/components/Image/ImageStylePropTypes.js index 699761fa..79aedfb2 100644 --- a/src/components/Image/ImageStylePropTypes.js +++ b/src/components/Image/ImageStylePropTypes.js @@ -1,4 +1,23 @@ -import View from '../View' +import { PropTypes } from 'react' +import ColorPropType from '../../apis/StyleSheet/ColorPropType' +import LayoutPropTypes from '../../apis/StyleSheet/LayoutPropTypes' +import TransformPropTypes from '../../apis/StyleSheet/TransformPropTypes' + +const hiddenOrVisible = PropTypes.oneOf([ 'hidden', 'visible' ]) + export default { - ...(View.stylePropTypes) + ...LayoutPropTypes, + ...TransformPropTypes, + backfaceVisibility: hiddenOrVisible, + backgroundColor: ColorPropType, + /** + * @platform web + */ + boxShadow: PropTypes.string, + opacity: PropTypes.number, + overflow: hiddenOrVisible, + /** + * @platform web + */ + visibility: hiddenOrVisible } diff --git a/src/components/Image/__tests__/index-test.js b/src/components/Image/__tests__/index-test.js index 6c388d36..63789762 100644 --- a/src/components/Image/__tests__/index-test.js +++ b/src/components/Image/__tests__/index-test.js @@ -65,10 +65,6 @@ suite('components/Image', () => { test('prop "source"') - test('prop "style"', () => { - utils.assertProps.style(Image) - }) - test('prop "testID"', () => { const testID = 'testID' const result = utils.shallowRender() diff --git a/src/components/Image/index.js b/src/components/Image/index.js index 975e1369..463c540a 100644 --- a/src/components/Image/index.js +++ b/src/components/Image/index.js @@ -1,9 +1,9 @@ /* global window */ -import { pickProps } from '../../modules/filterObjectProps' import StyleSheet from '../../apis/StyleSheet' import CoreComponent from '../CoreComponent' import ImageStylePropTypes from './ImageStylePropTypes' import React, { Component, PropTypes } from 'react' +import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType' import View from '../View' const STATUS_ERRORED = 'ERRORED' @@ -12,46 +12,6 @@ const STATUS_LOADING = 'LOADING' const STATUS_PENDING = 'PENDING' const STATUS_IDLE = 'IDLE' -const imageStyleKeys = Object.keys(ImageStylePropTypes) - -const styles = StyleSheet.create({ - initial: { - alignSelf: 'flex-start', - backgroundColor: 'transparent', - backgroundPosition: 'center', - backgroundRepeat: 'no-repeat', - backgroundSize: 'cover' - }, - img: { - borderWidth: 0, - height: 'auto', - maxHeight: '100%', - maxWidth: '100%', - opacity: 0 - }, - children: { - bottom: 0, - left: 0, - position: 'absolute', - right: 0, - top: 0 - }, - resizeMode: { - contain: { - backgroundSize: 'contain' - }, - cover: { - backgroundSize: 'cover' - }, - none: { - backgroundSize: 'auto' - }, - stretch: { - backgroundSize: '100% 100%' - } - } -}) - export default class Image extends Component { constructor(props, context) { super(props, context) @@ -74,18 +34,15 @@ export default class Image extends Component { onLoadStart: PropTypes.func, resizeMode: PropTypes.oneOf(['contain', 'cover', 'none', 'stretch']), source: PropTypes.object, - style: PropTypes.shape(ImageStylePropTypes), + style: StyleSheetPropType(ImageStylePropTypes), testID: CoreComponent.propTypes.testID }; - static stylePropTypes = ImageStylePropTypes; - static defaultProps = { accessible: true, defaultSource: {}, resizeMode: 'cover', - source: {}, - style: styles.initial + source: {} }; _createImageLoader() { @@ -177,7 +134,6 @@ export default class Image extends Component { const isLoaded = this.state.status === STATUS_LOADED const defaultImage = defaultSource.uri || null const displayImage = !isLoaded ? defaultImage : source.uri - const resolvedStyle = pickProps(style, imageStyleKeys) const backgroundImage = displayImage ? `url("${displayImage}")` : null /** @@ -189,16 +145,15 @@ export default class Image extends Component { */ return ( @@ -209,3 +164,42 @@ export default class Image extends Component { ) } } + +const styles = StyleSheet.create({ + initial: { + alignSelf: 'flex-start', + backgroundColor: 'transparent', + backgroundPosition: 'center', + backgroundRepeat: 'no-repeat', + backgroundSize: 'cover' + }, + img: { + borderWidth: 0, + height: 'auto', + maxHeight: '100%', + maxWidth: '100%', + opacity: 0 + }, + children: { + bottom: 0, + left: 0, + position: 'absolute', + right: 0, + top: 0 + } +}) + +const resizeModeStyles = StyleSheet.create({ + contain: { + backgroundSize: 'contain' + }, + cover: { + backgroundSize: 'cover' + }, + none: { + backgroundSize: 'auto' + }, + stretch: { + backgroundSize: '100% 100%' + } +}) diff --git a/src/components/ListView/index.js b/src/components/ListView/index.js index 841c66e9..01896c75 100644 --- a/src/components/ListView/index.js +++ b/src/components/ListView/index.js @@ -4,7 +4,7 @@ import ScrollView from '../ScrollView' export default class ListView extends Component { static propTypes = { children: PropTypes.any, - style: PropTypes.style + style: ScrollView.propTypes.style }; static defaultProps = { diff --git a/src/components/ScrollView/ScrollViewStylePropTypes.js b/src/components/ScrollView/ScrollViewStylePropTypes.js deleted file mode 100644 index 699761fa..00000000 --- a/src/components/ScrollView/ScrollViewStylePropTypes.js +++ /dev/null @@ -1,4 +0,0 @@ -import View from '../View' -export default { - ...(View.stylePropTypes) -} diff --git a/src/components/ScrollView/__tests__/index-test.js b/src/components/ScrollView/__tests__/index-test.js index 5f162ce2..c6c9bc72 100644 --- a/src/components/ScrollView/__tests__/index-test.js +++ b/src/components/ScrollView/__tests__/index-test.js @@ -1,11 +1,5 @@ /* eslint-env mocha */ -import * as utils from '../../../modules/specHelpers' - -import ScrollView from '../' - suite('components/ScrollView', () => { - test('prop "style"', () => { - utils.assertProps.style(ScrollView) - }) + test('NO TEST COVERAGE') }) diff --git a/src/components/ScrollView/index.js b/src/components/ScrollView/index.js index dde78b45..4c9fef3a 100644 --- a/src/components/ScrollView/index.js +++ b/src/components/ScrollView/index.js @@ -1,21 +1,15 @@ -import { pickProps } from '../../modules/filterObjectProps' import debounce from 'lodash.debounce' import React, { Component, PropTypes } from 'react' -import ScrollViewStylePropTypes from './ScrollViewStylePropTypes' import StyleSheet from '../../apis/StyleSheet' import View from '../View' -const scrollViewStyleKeys = Object.keys(ScrollViewStylePropTypes) - const styles = StyleSheet.create({ initial: { - flexGrow: 1, - flexShrink: 1, + flex: 1, overflow: 'auto' }, initialContentContainer: { - flexGrow: 1, - flexShrink: 1 + flex: 1 }, row: { flexDirection: 'row' @@ -25,20 +19,20 @@ const styles = StyleSheet.create({ export default class ScrollView extends Component { static propTypes = { children: PropTypes.any, - contentContainerStyle: PropTypes.shape(ScrollViewStylePropTypes), + contentContainerStyle: View.propTypes.style, horizontal: PropTypes.bool, onScroll: PropTypes.func, scrollEnabled: PropTypes.bool, scrollEventThrottle: PropTypes.number, - style: PropTypes.shape(ScrollViewStylePropTypes) + style: View.propTypes.style }; static defaultProps = { - contentContainerStyle: styles.initialContentContainer, + contentContainerStyle: {}, horizontal: false, scrollEnabled: true, scrollEventThrottle: 0, - style: styles.initial + style: {} }; constructor(...args) { @@ -108,28 +102,25 @@ export default class ScrollView extends Component { style } = this.props - const resolvedStyle = pickProps(style, scrollViewStyleKeys) - const resolvedContentContainerStyle = pickProps(contentContainerStyle, scrollViewStyleKeys) - return ( this._onScroll(e)} onTouchMove={(e) => this._maybePreventScroll(e)} onWheel={(e) => this._maybePreventScroll(e)} - style={{ - ...styles.initial, - ...resolvedStyle - }} + style={[ + styles.initial, + style + ]} > {children ? ( ) : null} diff --git a/src/components/Text/TextStylePropTypes.js b/src/components/Text/TextStylePropTypes.js index 9751dabc..afd3a27a 100644 --- a/src/components/Text/TextStylePropTypes.js +++ b/src/components/Text/TextStylePropTypes.js @@ -1,23 +1,28 @@ -import { pickProps } from '../../modules/filterObjectProps' -import CoreComponent from '../CoreComponent' -import View from '../View' +import { PropTypes } from 'react' +import ColorPropType from '../../apis/StyleSheet/ColorPropType' +import ViewStylePropTypes from '../View/ViewStylePropTypes' + +const { number, oneOf, oneOfType, string } = PropTypes +const numberOrString = oneOfType([ number, string ]) export default { - ...View.stylePropTypes, - ...pickProps(CoreComponent.stylePropTypes, [ - 'color', - 'fontFamily', - 'fontSize', - 'fontStyle', - 'fontWeight', - 'letterSpacing', - 'lineHeight', - 'textAlign', - 'textDecoration', - 'textShadow', - 'textTransform', - 'whiteSpace', - 'wordWrap', - 'writingDirection' - ]) + ...ViewStylePropTypes, + color: ColorPropType, + fontFamily: string, + fontSize: numberOrString, + fontStyle: string, + fontWeight: string, + letterSpacing: numberOrString, + lineHeight: numberOrString, + textAlign: oneOf([ 'center', 'inherit', 'justify', 'justify-all', 'left', 'right' ]), + /** + * @platform web + */ + textDecoration: string, + textOverflow: string, + textShadow: string, + textTransform: oneOf([ 'capitalize', 'lowercase', 'none', 'uppercase' ]), + whiteSpace: string, + wordWrap: string, + writingDirection: string } diff --git a/src/components/Text/__tests__/index-test.js b/src/components/Text/__tests__/index-test.js index a816641c..b455af05 100644 --- a/src/components/Text/__tests__/index-test.js +++ b/src/components/Text/__tests__/index-test.js @@ -43,10 +43,6 @@ suite('components/Text', () => { } }) - test('prop "style"', () => { - utils.assertProps.style(Text) - }) - test('prop "testID"', () => { const testID = 'testID' const result = utils.shallowRender() diff --git a/src/components/Text/index.js b/src/components/Text/index.js index 15e0965f..f1189fa3 100644 --- a/src/components/Text/index.js +++ b/src/components/Text/index.js @@ -1,11 +1,9 @@ -import { pickProps } from '../../modules/filterObjectProps' import CoreComponent from '../CoreComponent' import React, { Component, PropTypes } from 'react' import StyleSheet from '../../apis/StyleSheet' +import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType' import TextStylePropTypes from './TextStylePropTypes' -const textStyleKeys = Object.keys(TextStylePropTypes) - const styles = StyleSheet.create({ initial: { color: 'inherit', @@ -26,23 +24,18 @@ const styles = StyleSheet.create({ export default class Text extends Component { static propTypes = { - _className: PropTypes.string, // escape-hatch for code migrations accessibilityLabel: CoreComponent.propTypes.accessibilityLabel, accessibilityRole: CoreComponent.propTypes.accessibilityRole, accessible: CoreComponent.propTypes.accessible, children: PropTypes.any, numberOfLines: PropTypes.number, onPress: PropTypes.func, - style: PropTypes.shape(TextStylePropTypes), + style: StyleSheetPropType(TextStylePropTypes), testID: CoreComponent.propTypes.testID }; - static stylePropTypes = TextStylePropTypes; - static defaultProps = { - _className: '', - accessible: true, - style: styles.initial + accessible: true }; _onPress(e) { @@ -51,27 +44,22 @@ export default class Text extends Component { render() { const { - _className, numberOfLines, onPress, style, ...other } = this.props - const className = `${_className} Text`.trim() - const resolvedStyle = pickProps(style, textStyleKeys) - return ( ) } diff --git a/src/components/TextInput/TextInputStylePropTypes.js b/src/components/TextInput/TextInputStylePropTypes.js deleted file mode 100644 index e36cd937..00000000 --- a/src/components/TextInput/TextInputStylePropTypes.js +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react' -import Text from '../Text' - -export default { - ...Text.stylePropTypes, - outline: React.PropTypes.string -} diff --git a/src/components/TextInput/__tests__/index-test.js b/src/components/TextInput/__tests__/index-test.js index bf5f65a6..63f37012 100644 --- a/src/components/TextInput/__tests__/index-test.js +++ b/src/components/TextInput/__tests__/index-test.js @@ -4,6 +4,7 @@ import * as utils from '../../../modules/specHelpers' import assert from 'assert' import React from 'react' import ReactTestUtils from 'react-addons-test-utils' +import StyleSheet from '../../../apis/StyleSheet' import TextInput from '../' @@ -192,10 +193,10 @@ suite('components/TextInput', () => { const placeholder = 'placeholder' let result = findShallowPlaceholder(utils.shallowRender()) - assert.equal(result.props.style.color, 'darkgray') + assert.equal(StyleSheet.flatten(result.props.style).color, 'darkgray') result = findShallowPlaceholder(utils.shallowRender()) - assert.equal(result.props.style.color, 'red') + assert.equal(StyleSheet.flatten(result.props.style).color, 'red') }) test('prop "secureTextEntry"', () => { @@ -220,10 +221,6 @@ suite('components/TextInput', () => { assert.equal(input.selectionStart, 0) }) - test('prop "style"', () => { - utils.assertProps.style(TextInput) - }) - test('prop "testID"', () => { const testID = 'testID' const result = utils.shallowRender() diff --git a/src/components/TextInput/index.js b/src/components/TextInput/index.js index 9492f82d..5f9737e7 100644 --- a/src/components/TextInput/index.js +++ b/src/components/TextInput/index.js @@ -1,44 +1,11 @@ -import { pickProps } from '../../modules/filterObjectProps' import CoreComponent from '../CoreComponent' import React, { Component, PropTypes } from 'react' import ReactDOM from 'react-dom' import StyleSheet from '../../apis/StyleSheet' import Text from '../Text' import TextareaAutosize from 'react-textarea-autosize' -import TextInputStylePropTypes from './TextInputStylePropTypes' import View from '../View' -const textInputStyleKeys = Object.keys(TextInputStylePropTypes) - -const styles = StyleSheet.create({ - initial: { - ...View.defaultProps.style, - borderColor: 'black', - borderWidth: 1 - }, - input: { - appearance: 'none', - backgroundColor: 'transparent', - borderWidth: 0, - boxSizing: 'border-box', - color: 'inherit', - flexGrow: 1, - font: 'inherit', - padding: 0, - zIndex: 1 - }, - placeholder: { - bottom: 0, - color: 'darkgray', - left: 0, - overflow: 'hidden', - position: 'absolute', - right: 0, - top: 0, - whiteSpace: 'pre' - } -}) - export default class TextInput extends Component { constructor(props, context) { super(props, context) @@ -66,20 +33,18 @@ export default class TextInput extends Component { placeholderTextColor: PropTypes.string, secureTextEntry: PropTypes.bool, selectTextOnFocus: PropTypes.bool, - style: PropTypes.shape(TextInputStylePropTypes), + style: Text.propTypes.style, testID: CoreComponent.propTypes.testID, value: PropTypes.string }; - static stylePropTypes = TextInputStylePropTypes; - static defaultProps = { editable: true, keyboardType: 'default', multiline: false, numberOfLines: 2, secureTextEntry: false, - style: styles.initial + style: {} }; _onBlur(e) { @@ -142,7 +107,6 @@ export default class TextInput extends Component { value } = this.props - const resolvedStyle = pickProps(style, textInputStyleKeys) let type switch (keyboardType) { @@ -198,26 +162,56 @@ export default class TextInput extends Component { const props = multiline ? propsMultiline : propsSingleline return ( - - + {placeholder && this.state.showPlaceholder && {placeholder}} - + ) } } + +const styles = StyleSheet.create({ + initial: { + borderColor: 'black', + borderWidth: 1 + }, + wrapper: { + flexGrow: 1 + }, + input: { + appearance: 'none', + backgroundColor: 'transparent', + borderWidth: 0, + boxSizing: 'border-box', + color: 'inherit', + flexGrow: 1, + font: 'inherit', + padding: 0, + zIndex: 1 + }, + placeholder: { + bottom: 0, + color: 'darkgray', + left: 0, + overflow: 'hidden', + position: 'absolute', + right: 0, + top: 0, + whiteSpace: 'pre' + } +}) diff --git a/src/components/Touchable/__tests__/index-test.js b/src/components/Touchable/__tests__/index-test.js index 634b50a9..1c4e489f 100644 --- a/src/components/Touchable/__tests__/index-test.js +++ b/src/components/Touchable/__tests__/index-test.js @@ -30,6 +30,6 @@ suite('components/Touchable', () => { test('prop "children"', () => { const result = utils.shallowRender() - assert.deepEqual(result.props.children, children) + assert.deepEqual(result.props.children, children) }) }) diff --git a/src/components/Touchable/index.js b/src/components/Touchable/index.js index 438e8d7e..8f68fa27 100644 --- a/src/components/Touchable/index.js +++ b/src/components/Touchable/index.js @@ -1,15 +1,7 @@ import React, { Component, PropTypes } from 'react' +import StyleSheet from '../../apis/StyleSheet' import Tappable from 'react-tappable' import View from '../View' -import StyleSheet from '../../apis/StyleSheet' - -const styles = StyleSheet.create({ - initial: { - ...View.defaultProps.style, - cursor: 'pointer', - userSelect: undefined - } -}) export default class Touchable extends Component { constructor(props, context) { @@ -48,16 +40,16 @@ export default class Touchable extends Component { delayLongPress: 500, delayPressIn: 0, delayPressOut: 100, - style: styles.initial + style: {} }; _getChildren() { const { activeOpacity, children } = this.props return React.cloneElement(React.Children.only(children), { - style: { - ...children.props.style, - ...(this.state.isActive && { opacity: activeOpacity }) - } + style: [ + children.props.style, + this.state.isActive && { opacity: activeOpacity } + ] }) } @@ -97,7 +89,7 @@ export default class Touchable extends Component { } = this.props /** - * Creates a wrapping element that can receive beyboard focus. The + * Creates a wrapping element that can receive keyboard focus. The * highlight is applied as a background color on this wrapper. The opacity * is set on the child element, allowing it to have its own background * color. @@ -120,13 +112,20 @@ export default class Touchable extends Component { onTouchStart={this._onPressIn} pressDelay={delayLongPress} pressMoveThreshold={5} - style={{ - ...styles.initial, - ...style, - backgroundColor: this.state.isActive ? activeUnderlayColor : style.backgroundColor - }} + style={StyleSheet.flatten([ + styles.initial, + style, + activeUnderlayColor && this.state.isActive && { backgroundColor: activeUnderlayColor } + ])} tabIndex='0' /> ) } } + +const styles = StyleSheet.create({ + initial: { + cursor: 'pointer', + userSelect: undefined + } +}) diff --git a/src/components/View/ViewStylePropTypes.js b/src/components/View/ViewStylePropTypes.js index c1b07e6d..22400739 100644 --- a/src/components/View/ViewStylePropTypes.js +++ b/src/components/View/ViewStylePropTypes.js @@ -1,91 +1,37 @@ -import { pickProps } from '../../modules/filterObjectProps' -import CoreComponent from '../CoreComponent' +import { PropTypes } from 'react' +import BorderPropTypes from '../../apis/StyleSheet/BorderPropTypes' +import ColorPropType from '../../apis/StyleSheet/ColorPropType' +import LayoutPropTypes from '../../apis/StyleSheet/LayoutPropTypes' +import TransformPropTypes from '../../apis/StyleSheet/TransformPropTypes' + +const { number, oneOf, string } = PropTypes +const autoOrHiddenOrVisible = oneOf([ 'auto', 'hidden', 'visible' ]) +const hiddenOrVisible = oneOf([ 'hidden', 'visible' ]) export default { - ...pickProps(CoreComponent.stylePropTypes, [ - 'alignContent', - 'alignItems', - 'alignSelf', - 'backfaceVisibility', - // background - 'backgroundAttachment', - 'backgroundClip', - 'backgroundColor', - 'backgroundImage', - 'backgroundPosition', - 'backgroundOrigin', - 'backgroundRepeat', - 'backgroundSize', - // border-color - 'borderColor', - 'borderTopColor', - 'borderRightColor', - 'borderBottomColor', - 'borderLeftColor', - // border-radius - 'borderRadius', - 'borderTopLeftRadius', - 'borderTopRightRadius', - 'borderBottomLeftRadius', - 'borderBottomRightRadius', - // border style - 'borderStyle', - 'borderBottomStyle', - 'borderLeftStyle', - 'borderRightStyle', - 'borderTopStyle', - // border width - 'borderWidth', - 'borderBottomWidth', - 'borderLeftWidth', - 'borderRightWidth', - 'borderTopWidth', - 'bottom', - 'boxShadow', - 'boxSizing', - 'cursor', - 'flex', - 'flexBasis', - 'flexDirection', - 'flexGrow', - 'flexShrink', - 'flexWrap', - 'height', - 'justifyContent', - 'left', - // margin - 'margin', - 'marginHorizontal', - 'marginVertical', - 'marginBottom', - 'marginLeft', - 'marginRight', - 'marginTop', - // max/min - 'maxHeight', - 'maxWidth', - 'minHeight', - 'minWidth', - 'opacity', - 'order', - 'overflow', - 'overflowX', - 'overflowY', - // padding - 'padding', - 'paddingHorizontal', - 'paddingVertical', - 'paddingBottom', - 'paddingLeft', - 'paddingRight', - 'paddingTop', - 'position', - 'right', - 'top', - 'transform', - 'userSelect', - 'visibility', - 'width', - 'zIndex' - ]) + ...BorderPropTypes, + ...LayoutPropTypes, + ...TransformPropTypes, + backfaceVisibility: hiddenOrVisible, + backgroundColor: ColorPropType, + opacity: number, + overflow: autoOrHiddenOrVisible, + /* + * @platform web + */ + backgroundAttachment: string, + backgroundClip: string, + backgroundImage: string, + backgroundPosition: string, + backgroundOrigin: oneOf([ 'border-box', 'content-box', 'padding-box' ]), + backgroundRepeat: string, + backgroundSize: string, + boxShadow: string, + cursor: string, + outline: string, + overflowX: autoOrHiddenOrVisible, + overflowY: autoOrHiddenOrVisible, + userSelect: string, + visibility: hiddenOrVisible, + zIndex: number } diff --git a/src/components/View/__tests__/index-test.js b/src/components/View/__tests__/index-test.js index 3c199776..a30fb45b 100644 --- a/src/components/View/__tests__/index-test.js +++ b/src/components/View/__tests__/index-test.js @@ -3,6 +3,7 @@ import * as utils from '../../../modules/specHelpers' import assert from 'assert' import React from 'react' +import StyleSheet from '../../../apis/StyleSheet' import View from '../' @@ -39,11 +40,7 @@ suite('components/View', () => { test('prop "pointerEvents"', () => { const result = utils.shallowRender() - assert.equal(result.props.style.pointerEvents, 'box-only') - }) - - test('prop "style"', () => { - utils.assertProps.style(View) + assert.equal(StyleSheet.flatten(result.props.style).pointerEvents, 'box-only') }) test('prop "testID"', () => { diff --git a/src/components/View/index.js b/src/components/View/index.js index cb482a3c..939ddf91 100644 --- a/src/components/View/index.js +++ b/src/components/View/index.js @@ -1,10 +1,46 @@ -import { pickProps } from '../../modules/filterObjectProps' import CoreComponent from '../CoreComponent' import React, { Component, PropTypes } from 'react' import StyleSheet from '../../apis/StyleSheet' +import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType' import ViewStylePropTypes from './ViewStylePropTypes' -const viewStyleKeys = Object.keys(ViewStylePropTypes) +export default class View extends Component { + static propTypes = { + accessibilityLabel: CoreComponent.propTypes.accessibilityLabel, + accessibilityLiveRegion: CoreComponent.propTypes.accessibilityLiveRegion, + accessibilityRole: CoreComponent.propTypes.accessibilityRole, + accessible: CoreComponent.propTypes.accessible, + children: PropTypes.any, + pointerEvents: PropTypes.oneOf(['auto', 'box-none', 'box-only', 'none']), + style: StyleSheetPropType(ViewStylePropTypes), + testID: CoreComponent.propTypes.testID + }; + + static defaultProps = { + accessible: true + }; + + render() { + const { + pointerEvents, + style, + ...other + } = this.props + + const pointerEventsStyle = pointerEvents && { pointerEvents } + + return ( + + ) + } +} const styles = StyleSheet.create({ // https://github.com/facebook/css-layout#default-values @@ -29,50 +65,3 @@ const styles = StyleSheet.create({ textAlign: 'inherit' } }) - -export default class View extends Component { - static propTypes = { - _className: PropTypes.string, // escape-hatch for code migrations - accessibilityLabel: CoreComponent.propTypes.accessibilityLabel, - accessibilityLiveRegion: CoreComponent.propTypes.accessibilityLiveRegion, - accessibilityRole: CoreComponent.propTypes.accessibilityRole, - accessible: CoreComponent.propTypes.accessible, - children: PropTypes.any, - pointerEvents: PropTypes.oneOf(['auto', 'box-none', 'box-only', 'none']), - style: PropTypes.shape(ViewStylePropTypes), - testID: CoreComponent.propTypes.testID - }; - - static stylePropTypes = ViewStylePropTypes; - - static defaultProps = { - _className: '', - accessible: true, - style: styles.initial - }; - - render() { - const { - _className, - pointerEvents, - style, - ...other - } = this.props - - const className = `${_className} View`.trim() - const pointerEventsStyle = pointerEvents && { pointerEvents } - const resolvedStyle = pickProps(style, viewStyleKeys) - - return ( - - ) - } -} diff --git a/src/modules/filterObjectProps/__tests__/index-test.js b/src/modules/filterObjectProps/__tests__/index-test.js deleted file mode 100644 index f70dd06a..00000000 --- a/src/modules/filterObjectProps/__tests__/index-test.js +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-env mocha */ - -import { omitProps, pickProps } from '..' -import assert from 'assert' - -suite('pickProps', () => { - test('return value', () => { - const obj = { a: 1, b: 2, c: { cc: { ccc: 3 } } } - const props = [ 'a', 'b' ] - const expected = { a: 1, b: 2 } - const actual = pickProps(obj, props) - - assert.deepEqual(actual, expected) - }) -}) - -suite('omitProps', () => { - test('return value', () => { - const obj = { a: 1, b: 2, c: { cc: { ccc: 3 } } } - const props = [ 'a', 'b' ] - const expected = { c: { cc: { ccc: 3 } } } - const actual = omitProps(obj, props) - - assert.deepEqual(actual, expected) - }) -}) diff --git a/src/modules/filterObjectProps/index.js b/src/modules/filterObjectProps/index.js deleted file mode 100644 index 7f8e0785..00000000 --- a/src/modules/filterObjectProps/index.js +++ /dev/null @@ -1,25 +0,0 @@ -function filterProps(obj, propKeys: Array, excluded = false) { - const filtered = {} - for (const prop in obj) { - if (Object.prototype.hasOwnProperty.call(obj, prop)) { - const isMatch = propKeys.indexOf(prop) > -1 - if (excluded && isMatch) { - continue - } else if (!excluded && !isMatch) { - continue - } - - filtered[prop] = obj[prop] - } - } - - return filtered -} - -export function pickProps(obj, propKeys) { - return filterProps(obj, propKeys) -} - -export function omitProps(obj, propKeys) { - return filterProps(obj, propKeys, true) -}