diff --git a/README.md b/README.md index 8b262d0..bb68e7a 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ [![Build Status](https://travis-ci.org/vitalets/react-native-extended-stylesheet.svg?branch=master)](https://travis-ci.org/vitalets/react-native-extended-stylesheet) [![Coverage Status](https://coveralls.io/repos/github/vitalets/react-native-extended-stylesheet/badge.svg?branch=master)](https://coveralls.io/github/vitalets/react-native-extended-stylesheet?branch=master) -Extend [React Native](https://facebook.github.io/react-native/) stylesheets with variables, relative units, percents, -math operations, scaling and other stuff to control app styling. +Extend [React Native](https://facebook.github.io/react-native/) stylesheets with media-queries, variables, themes, +relative units, percents, math operations, scaling and other styling stuff. @@ -17,10 +17,10 @@ math operations, scaling and other stuff to control app styling. - [math operations](#math-operations) - [rem units](#rem-units) - [percents](#percents) + - [media queries](#media-queries) - [scaling](#scaling) - [_underscored styles](#underscored-styles) - [pseudo classes (:nth-child)](#pseudo-classes-nth-child) - - [OS specific props](#os-specific-props) - [value as a function](#value-as-a-function) - [caching](#caching) - [outline for debug](#outline-for-debug) @@ -68,20 +68,16 @@ npm i react-native-extended-stylesheet --save } ``` -2. Call `EStyleSheet.build()` in entry point of your app: +2. Call `EStyleSheet.build()` in entry point of your app to actually calculate styles: ```js // app.js import EStyleSheet from 'react-native-extended-stylesheet'; - // build styles + // calculate styles EStyleSheet.build(); - - // build styles with passed variables - EStyleSheet.build({ - textColor: '#0275d8' - }); ``` + \[[top](#)\] ## Features @@ -112,13 +108,32 @@ export default { } // app entry -import theme from '.theme'; +import theme from './theme'; EStyleSheet.build(theme); ``` + +You can define nested variables and access them via dot path: +```js +// entry +EStyleSheet.build({ + button: { + size: 10 + } +}); + +// component +const styles = EStyleSheet.create({ + text: { + color: '$button.size' + } +}); +``` + \[[top](#)\] ### Local variables Local variables can be defined directly in sylesheet and have priority over global variables. +To define local variable just start it with `$`: ```js const styles = EStyleSheet.create({ $textColor: '#0275d8', @@ -167,8 +182,8 @@ EStyleSheet.build({ \[[top](#)\] ### Percents -Percents are useful only for **single-orientation apps** as calculation performed once on start using screen dimensions. -You can apply it to top-level components to render layout. +Percent values are useful for **single-orientation apps** because calculation is performed on app start only. +They are calculated relative to **screen width/height** (not parent component!). ```js const styles = EStyleSheet.create({ column: { @@ -178,11 +193,11 @@ const styles = EStyleSheet.create({ } }); ``` -Supporting orientation change is always design-decision but sometimes it's really unneeded and makes life much easier. +Note: supporting orientation change is always design-decision but sometimes it's really unneeded and makes life much easier. How to lock orientaion for [IOS](http://stackoverflow.com/a/24205653/740245), [Android](http://stackoverflow.com/a/4675801/740245). **Percents in nested components** -If you need sub-components with percentage props based on parent, you can easily achieve it with variables. +If you need sub-components with percentage props based on parent, you can achieve it with variables. For example, to render 2 sub-columns with 30%/70% width of parent: ```js const styles = EStyleSheet.create({ @@ -213,8 +228,46 @@ render() { ``` \[[top](#)\] +### Media queries +Media queries are supported in standard format (thanks for idea to [@grabbou](https://github.com/grabbou), +[#5](https://github.com/vitalets/react-native-extended-stylesheet/issues/5)). +They allows to have different styles for different screens, platform, orienation etc. + +Supported values are: + +* media type: `ios|android` +* `width`, `min-width`, `max-width` +* `height`, `min-height`, `max-height` +* `orientation` (`landscape|portrait`) +* `aspect-ratio` + +You can define media queries on sheet level or style level: +```js +const styles = EStyleSheet.create({ + column: { + width: '80%', + }, + '@media (min-width: 350) and (max-width: 500)': { // media query on sheet level + column: { + width: '90%', + } + }, + header: { + fontSize: 18, + '@media ios': { // media query on style level + color: 'green', + }, + '@media android': { + color: 'blue', + }, + } +}); +``` +See full example [here](examples/media-queries). +\[[top](#)\] + ### Scaling -You can easily scale your components by setting special `$scale` variable. +You can apply scale to components by setting special `$scale` variable. ```js const styles = EStyleSheet.create({ $scale: 1.5, @@ -225,7 +278,7 @@ const styles = EStyleSheet.create({ } }); ``` -This also helps to create reusable components that could be scaled depending on prop. +This helps to create reusable components that could be scaled depending on prop: ```js class Button extends React.Component { static propTypes = { @@ -251,12 +304,16 @@ let getStyle = function (scale = 1) { }); } ``` +To cache calculated styles please have a look on [caching](#caching) section. \[[top](#)\] ### Underscored styles -Usual react-native stylesheets are calculated to integer numbers and original values are unavailable. But sometimes they are needed. Let's take an example: -You want to render text and icon with the same size and color. You can take this [awesome icon library](https://github.com/oblador/react-native-vector-icons) and see that `` component has `size` and `color` props. -It would be convenient to define style for text and keep icon's size and color in sync. +Original react-native stylesheets are calculated to integer numbers and original values are unavailable. +But sometimes they are needed. Let's take an example: +You want to render text and icon with the same size and color. +You can take this [awesome icon library](https://github.com/oblador/react-native-vector-icons) +and see that `` component has `size` and `color` props. +It would be convenient to define style for text and keep icon's size/color in sync. ```js const styles = EStyleSheet.create({ text: { @@ -271,7 +328,7 @@ styles = { text: 0 } ``` -But extended stylesheet saves original values under `_text` property: +But extended stylesheet saves calculated values under `_text` property: ```js styles = { text: 0, @@ -324,22 +381,8 @@ render() { ``` \[[top](#)\] -### OS specific props -If you want different values of the same prop for IOS / Android, just name prop with appropriate suffix: -```js -const styles = EStyleSheet.create({ - container: { - marginTopIOS: 10, - marginTopAndroid: 0 - } -}); -``` -The output style will have only one property `marginTop` depending on OS. -\[[top](#)\] - ### Value as a function -For the deepest customization you can specify any value as a function that will be executed on EStyleSheet build. -It is practically useful if you need to calculate value depending on some global variable. +For the deepest customization you can specify any value as a function that will be executed on EStyleSheet build. For example, you may *darken* or *lighten* color of variable via [npm color package](https://www.npmjs.com/package/color): ```js import Color from 'color'; @@ -364,7 +407,8 @@ render() { \[[top](#)\] ### Caching -If you use dynamic styles depending on runtime prop or you are making reusable component with dynamic styling you may need stylesheet creation in every `render()` call. Let's take example from [scaling](#scaling) section: +If you use dynamic styles depending on runtime prop or you are making reusable component with dynamic styling +you may need stylesheet creation in every `render()` call. Let's take example from [scaling](#scaling) section: ```js class Button extends React.Component { static propTypes = { @@ -403,7 +447,8 @@ let getStyle = EStyleSheet.memoize(function (scale = 1) { }); }); ``` -Now if you call `getStyle(1.5)` 3 times actually style will be created on the first call and two other calls will get it from cache. +Now if you call `getStyle(1.5)` 3 times actually style will be created on the first call +and two other calls will get it from cache. \[[top](#)\] ### Outline for debug diff --git a/example/screenshot.png b/example/screenshot.png deleted file mode 100644 index 3b8410e..0000000 Binary files a/example/screenshot.png and /dev/null differ diff --git a/example/theme.js b/example/theme.js deleted file mode 100644 index c05af42..0000000 --- a/example/theme.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - textColor: 'black', - buttonColor: '#679267', - outline: 0, -} \ No newline at end of file diff --git a/example/README.md b/examples/README.md similarity index 80% rename from example/README.md rename to examples/README.md index 04041f8..d32398d 100644 --- a/example/README.md +++ b/examples/README.md @@ -1,4 +1,4 @@ -## How to run this example +## How to run examples 1. Create new react-native project @@ -17,7 +17,7 @@ ```js import {AppRegistry} from 'react-native'; - import App from 'react-native-extended-stylesheet/example/app'; + import App from 'react-native-extended-stylesheet/examples/simple/app'; AppRegistry.registerComponent('ExtendedStyleSheetExample', () => App); ``` - \ No newline at end of file + diff --git a/examples/media-queries/app.js b/examples/media-queries/app.js new file mode 100644 index 0000000..7799b7c --- /dev/null +++ b/examples/media-queries/app.js @@ -0,0 +1,14 @@ +import React from 'react-native'; +import EStyleSheet from '../../src'; +import MyComponent from './component'; + +// calc styles +EStyleSheet.build(); + +export default class extends React.Component { + render() { + return ( + + ); + } +} diff --git a/examples/media-queries/component.js b/examples/media-queries/component.js new file mode 100644 index 0000000..3541187 --- /dev/null +++ b/examples/media-queries/component.js @@ -0,0 +1,50 @@ +import React, {View, Text} from 'react-native'; +import EStyleSheet from '../../src'; + +export default class extends React.Component { + render() { + return ( + + + Width: {styles._column.width}, + margin: {styles._column.marginHorizontal} + + + ); + } +} + +const styles = EStyleSheet.create({ + column: { + alignItems: 'center', + borderWidth: 1, + marginTop: '10%', + }, + '@media (max-width: 350)': { // media query on sheet level + column: { + width: '70%', + marginHorizontal: '15%', + } + }, + '@media (min-width: 350) and (max-width: 500)': { // media query on sheet level + column: { + width: '80%', + marginHorizontal: '10%', + } + }, + '@media (min-width: 500)': { // media query on sheet level + column: { + width: '90%', + marginHorizontal: '5%', + } + }, + header: { + fontSize: 18, + '@media ios': { // media query on style level + color: 'green', + }, + '@media android': { // media query on style level + color: 'blue', + }, + } +}); diff --git a/examples/readme/app.js b/examples/readme/app.js new file mode 100644 index 0000000..4f9e845 --- /dev/null +++ b/examples/readme/app.js @@ -0,0 +1,18 @@ +import React from 'react-native'; +import EStyleSheet from '../../src'; +import MyComponent from './component'; + +EStyleSheet.build({ + textColor: 'black', + buttonColor: '#679267', + outline: 0, + rem: 18, +}); + +export default class extends React.Component { + render() { + return ( + + ); + } +} diff --git a/example/component.js b/examples/readme/component.js similarity index 81% rename from example/component.js rename to examples/readme/component.js index fff3d15..80dc3b3 100644 --- a/example/component.js +++ b/examples/readme/component.js @@ -1,5 +1,5 @@ import React, {View, Text, TouchableHighlight} from 'react-native'; -import EStyleSheet from '../src'; +import EStyleSheet from '../../src'; const items = [ 'first-child', @@ -16,9 +16,13 @@ export default class extends React.Component { return ( Extended StyleSheets - Container: width=80%, margin=10% + + Percent values: + width=80%, + margin=10% + - Stripped rows: + Stripped rows via pseudo-classes {items.map((item, index) => { return ( @@ -27,13 +31,14 @@ export default class extends React.Component { ); })} - Button: width=$size, + Circle button via variables{'\n'} + width=$size, height=$size, borderRadius=0.5*$size Like it! - Circle button scaled to 1.4x + Scaled to 1.4x Like it! diff --git a/example/app.js b/examples/rem/app.js similarity index 66% rename from example/app.js rename to examples/rem/app.js index 3a5aedc..6971b8c 100644 --- a/example/app.js +++ b/examples/rem/app.js @@ -1,15 +1,15 @@ import React, {Dimensions} from 'react-native'; -import EStyleSheet from '../src'; - +import EStyleSheet from '../../src'; import MyComponent from './component'; -import theme from './theme'; // define REM depending on screen width -let {width} = Dimensions.get('window'); +const {width} = Dimensions.get('window'); const rem = width > 340 ? 18 : 17; // calc styles -EStyleSheet.build({...theme, rem}); +EStyleSheet.build({ + rem: rem, +}); export default class extends React.Component { render() { @@ -17,4 +17,4 @@ export default class extends React.Component { ); } -} \ No newline at end of file +} diff --git a/examples/rem/component.js b/examples/rem/component.js new file mode 100644 index 0000000..68ecc16 --- /dev/null +++ b/examples/rem/component.js @@ -0,0 +1,17 @@ +import React, {Text} from 'react-native'; +import EStyleSheet from '../../src'; + +export default class extends React.Component { + render() { + return ( + Font size via REM + ); + } +} + +const styles = EStyleSheet.create({ + text: { + padding: '0.5rem', + fontSize: '1rem', + } +}); diff --git a/examples/screenshot.png b/examples/screenshot.png new file mode 100644 index 0000000..73f5d07 Binary files /dev/null and b/examples/screenshot.png differ diff --git a/examples/simple/app.js b/examples/simple/app.js new file mode 100644 index 0000000..9f8389f --- /dev/null +++ b/examples/simple/app.js @@ -0,0 +1,16 @@ +import React from 'react-native'; +import EStyleSheet from '../../src'; +import MyComponent from './component'; + +// calc styles +EStyleSheet.build({ + fontColor: 'black' +}); + +export default class extends React.Component { + render() { + return ( + + ); + } +} diff --git a/examples/simple/component.js b/examples/simple/component.js new file mode 100644 index 0000000..1421b18 --- /dev/null +++ b/examples/simple/component.js @@ -0,0 +1,27 @@ +import React, {View, Text} from 'react-native'; +import EStyleSheet from '../../src'; + +export default class extends React.Component { + render() { + return ( + + Welcome to Extended StyleSheet! + + ); + } +} + +const styles = EStyleSheet.create({ + column: { + width: '80%', + marginHorizontal: '10%', + marginTop: '10%', + backgroundColor: '#e6e6e6', + alignItems: 'center', + padding: '0.5rem', + }, + header: { + fontSize: '1rem', + color: '$fontColor', + } +}); diff --git a/package.json b/package.json index d4b6528..3e6175d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "scripts": { "code": "eslint src && jscs src", "code-fix": "eslint src --fix && jscs src --fix", - "test": "jest", + "test": "jest --coverage", "coveralls": "coveralls < coverage/lcov.info", "release": "npm run code && npm test && npm version patch && npm publish && git push --follow-tags" }, @@ -23,6 +23,7 @@ }, "license": "MIT", "dependencies": { + "css-mediaquery": "^0.1.2", "object-resolve-path": "^1.1.0" }, "devDependencies": { @@ -57,7 +58,7 @@ "test.js" ], "verbose": true, - "collectCoverage": true + "collectCoverage": false }, "keywords": [ "react", diff --git a/src/__tests__/api.test.js b/src/__tests__/api.test.js index b2b229c..8713985 100644 --- a/src/__tests__/api.test.js +++ b/src/__tests__/api.test.js @@ -78,9 +78,9 @@ describe('EStyleSheet API', function () { const res2 = api.create({$b: '$a'}); expect(res1).toEqual({$b: 1}); expect(res2).toEqual({$b: 1}); - api.build({a: 2}); - expect(res1).toEqual({$b: 2}); - expect(res2).toEqual({$b: 2}); + api.build({a: 1}); + expect(res1).toEqual({$b: 1}); + expect(res2).toEqual({$b: 1}); }); it('should calculate value', function () { diff --git a/src/__tests__/sheet.test.js b/src/__tests__/sheet.test.js index 625912d..bc5bad1 100644 --- a/src/__tests__/sheet.test.js +++ b/src/__tests__/sheet.test.js @@ -14,11 +14,7 @@ describe('sheet', function () { } }; const variables = {$a: 2, $d: 2, $e: 'abc'}; - const sheet = new Sheet(source); - - sheet.calc(variables); - - const result = sheet.getResult(); + const result = new Sheet(source).calc(variables); expect(result).toEqual({ $a: 1, $b: 2, @@ -39,11 +35,7 @@ describe('sheet', function () { borderWidth: '$b', } }; - const sheet = new Sheet(source); - - sheet.calc(); - - const result = sheet.getResult(); + const result = new Sheet(source).calc(); expect(result).toEqual({ $b: 2, _text: { @@ -52,4 +44,27 @@ describe('sheet', function () { }); }); + it('should support media queries', function () { + const source = { + $b: 2, + '@media ios': { + $b: 3 + }, + button: { + prop: 2, + '@media ios': { + prop: '$b' + }, + } + }; + const result = new Sheet(source).calc(); + expect(result).toEqual({ + $b: 3, + _button: { + prop: 3, + }, + button: 0, + }); + }); + }); diff --git a/src/__tests__/style.test.js b/src/__tests__/style.test.js index 6dd016d..d24f3b7 100644 --- a/src/__tests__/style.test.js +++ b/src/__tests__/style.test.js @@ -9,8 +9,6 @@ describe('style', function () { $b: '$d', fontSize: '$a', borderWidth: '$b', - propAndroid: 1, - propIOS: 2, color: '$e', }; let varsArr = [{$a: 3, $d: 3, $e: 'abc'}]; @@ -25,7 +23,6 @@ describe('style', function () { calculatedProps: { fontSize: 1, borderWidth: 3, - prop: 2, color: 'abc', } }); @@ -113,4 +110,24 @@ describe('style', function () { expect(Math.random.mock.calls.length).toBe(1); }); + it('should support media queries', function () { + const source = { + $b: 2, + c: 1, + '@media ios': { + $b: 3, + c: '$b', + } + }; + const result = new Style(source).calc(); + expect(result).toEqual({ + calculatedVars: { + $b: 3, + }, + calculatedProps: { + c: 3, + } + }); + }); + }); diff --git a/src/__tests__/utils.test.js b/src/__tests__/utils.test.js new file mode 100644 index 0000000..ec3452c --- /dev/null +++ b/src/__tests__/utils.test.js @@ -0,0 +1,28 @@ +import utils from '../utils'; + +describe('utils', function () { + + describe('excludeKeys', function () { + + it('should exclude by array', function () { + const obj = {a: 1, b: 2}; + const keys = ['a', 'c']; + expect(utils.excludeKeys(obj, keys)).toEqual({b: 2}); + }); + + it('should exclude by obj', function () { + const obj = {a: 1, b: 2}; + const keys = {a: 2, c: 3}; + expect(utils.excludeKeys(obj, keys)).toEqual({b: 2}); + }); + + it('should work correct with empty keys', function () { + const obj = {a: 1, b: 2}; + expect(utils.excludeKeys(obj)).toEqual(obj); + expect(utils.excludeKeys(obj, null)).toEqual(obj); + expect(utils.excludeKeys(obj, {})).toEqual(obj); + expect(utils.excludeKeys(obj, [])).toEqual(obj); + }); + }); + +}); diff --git a/src/api.js b/src/api.js index 37f7fb7..9bab280 100644 --- a/src/api.js +++ b/src/api.js @@ -34,8 +34,9 @@ export default class { let sheet = new Sheet(obj); if (this.builded) { sheet.calc(this.globalVars); + } else { + this.sheets.push(sheet); } - this.sheets.push(sheet); return sheet.getResult(); } @@ -90,6 +91,7 @@ export default class { _calcSheets() { this.sheets.forEach(sheet => sheet.calc(this.globalVars)); + this.sheets.length = 0; } _callListeners(event) { diff --git a/src/replacers/__tests__/media-queries.test.js b/src/replacers/__tests__/media-queries.test.js new file mode 100644 index 0000000..47d7ddd --- /dev/null +++ b/src/replacers/__tests__/media-queries.test.js @@ -0,0 +1,114 @@ +const rn = { + Platform: { + OS: 'ios' + }, + Dimensions: { + get: () => { + return {width: 110, height: 100}; + } + } +}; + +jest.setMock('react-native', rn); + +delete require.cache['../media-queries']; +const mq = require('../media-queries').default; + +describe('media-queries', function () { + + it('should extract and apply media queries', function () { + const obj = { + a: 1, + b: 2, + e: { + x: 1, + y: 2, + }, + '@media (min-width: 50) and (min-height: 100)': { + a: 2, + c: 3, + d: 4, + e: { + x: 2, + z: 3, + } + }, + '@media ios': { + d: 5, + } + }; + expect(mq.process(obj)).toEqual({ + a: 2, + b: 2, + c: 3, + d: 5, + e: { + x: 2, + y: 2, + z: 3, + } + }); + }); + + it('should process width', function () { + const obj = { + '@media (min-width: 50) and (max-width: 150)': { + a: 1, + }, + '@media (min-width: 150) and (max-width: 200)': { + a: 2, + } + }; + expect(mq.process(obj)).toEqual({a: 1}); + }); + + it('should process height', function () { + const obj = { + '@media (min-height: 50) and (max-height: 150)': { + a: 1, + }, + '@media (min-height: 150) and (max-height: 200)': { + a: 2, + } + }; + expect(mq.process(obj)).toEqual({a: 1}); + }); + + it('should process orientation', function () { + const obj = { + '@media (orientation: landscape)': { + a: 1, + }, + '@media (orientation: portrait)': { + a: 2, + } + }; + expect(mq.process(obj)).toEqual({a: 1}); + }); + + it('should process type', function () { + const obj = { + '@media ios': { + a: 1, + }, + '@media android': { + a: 2, + } + }; + expect(mq.process(obj)).toEqual({a: 1}); + }); + + it('should ignore invalid media queries', function () { + const obj = { + a: 0, + '@media sdfgsdfg': { + a: 1, + }, + '@media (min-width)': { + a: 2, + } + }; + expect(mq.process(obj)).toEqual({a: 0}); + }); + +}); diff --git a/src/replacers/__tests__/osprop.test.js b/src/replacers/__tests__/osprop.test.js deleted file mode 100644 index 0509532..0000000 --- a/src/replacers/__tests__/osprop.test.js +++ /dev/null @@ -1,41 +0,0 @@ -// as osprop detects os on start, mock react-native nere manually -let Platform = { - OS: 'ios' -}; -jest.setMock('react-native', {Platform}); - -describe('osprop', function () { - beforeEach(function () { - delete require.cache['../osprop']; - }); - - it('should replace android prop on android', function () { - Platform.OS = 'android'; - const osprop = require('../osprop').default; - expect(osprop.replace('propAndroid')).toBe('prop'); - }); - - it('should remove android prop on ios', function () { - Platform.OS = 'ios'; - const osprop = require('../osprop').default; - expect(osprop.replace('propAndroid')).toBe(''); - }); - - it('should replace ios prop on ios', function () { - Platform.OS = 'ios'; - const osprop = require('../osprop').default; - expect(osprop.replace('propIOS')).toBe('prop'); - }); - - it('should remove ios prop on android', function () { - Platform.OS = 'android'; - const osprop = require('../osprop').default; - expect(osprop.replace('propIOS')).toBe(''); - }); - - it('should keep non-os props', function () { - Platform.OS = 'android'; - const osprop = require('../osprop').default; - expect(osprop.replace('prop')).toBe('prop'); - }); -}); diff --git a/src/replacers/__tests__/vars.test.js b/src/replacers/__tests__/vars.test.js index aeb0a21..30d9f6f 100644 --- a/src/replacers/__tests__/vars.test.js +++ b/src/replacers/__tests__/vars.test.js @@ -1,4 +1,3 @@ - import vars from '../vars'; describe('vars', function () { @@ -24,16 +23,8 @@ describe('vars', function () { } }; expect(vars.extract(obj)).toEqual({ - extractedProps: { - c: 3, - d: { - $e: 1, - }, - }, - extractedVars: { - $a: 1, - $b: 2 - } + $a: 1, + $b: 2 }); }); @@ -78,7 +69,7 @@ describe('vars', function () { it('should add prefix', function () { let obj = { a: 1, - b: '2', + $b: '2', d: { e: 1, } diff --git a/src/replacers/media-queries.js b/src/replacers/media-queries.js new file mode 100644 index 0000000..4b0d1c1 --- /dev/null +++ b/src/replacers/media-queries.js @@ -0,0 +1,90 @@ +/** + * Media queries + * Supported values: + * - (type) ios, android + * - height, min-height, max-height + * - width, min-width, max-width + * - orientation + * - aspect-ratio + */ + +import {Dimensions, Platform} from 'react-native'; +import mediaQuery from 'css-mediaquery'; +import utils from '../utils'; + +const PREFIX = '@media'; + +export default { + process +}; + +/** + * Is string is media query + * @param {String} str + */ +function isMediaQuery(str) { + return typeof str === 'string' && str.indexOf(PREFIX) === 0; +} + +/** + * Process and apply media queries in object + * @param {Object} obj + * @returns {null|Object} + */ +function process(obj) { + const mqKeys = []; + + // copy non-media-query stuff + const res = Object.keys(obj).reduce((res, key) => { + if (!isMediaQuery(key)) { + res[key] = obj[key]; + } else { + mqKeys.push(key); + } + return res; + }, {}); + + // apply media query stuff + if (mqKeys.length) { + const matchObject = getMatchObject(); + mqKeys.forEach(key => { + const mqStr = key.replace(PREFIX, ''); + const isMatch = mediaQuery.match(mqStr, matchObject); + if (isMatch) { + merge(res, obj[key]); + } + }); + } + + return res; +} + +/** + * Returns object to match media query + * @returns {Object} + */ +function getMatchObject() { + const win = Dimensions.get('window'); + return { + width: win.width, + height: win.height, + orientation: win.width > win.height ? 'landscape' : 'portrait', + 'aspect-ratio': win.width / win.height, + type: Platform.OS, + }; +} + +/** + * Merge media query obj into parent obj + * @param {Object} obj + * @param {Object} mqObj + */ +function merge(obj, mqObj) { + Object.keys(mqObj).forEach(key => { + if (utils.isObject(obj[key]) && utils.isObject(mqObj[key])) { + Object.assign(obj[key], mqObj[key]); + } else { + obj[key] = mqObj[key]; + } + }); +} diff --git a/src/replacers/osprop.js b/src/replacers/osprop.js deleted file mode 100644 index 02594c1..0000000 --- a/src/replacers/osprop.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * OS dependent props - */ -import {Platform} from 'react-native'; - -const isIOS = Platform.OS === 'ios'; -const isAndroid = !isIOS; - -export default { - replace: function (prop) { - const l = prop.length; - if (prop.substr(l - 7) === 'Android') { - return isAndroid ? prop.substr(0, l - 7) : ''; - } - if (prop.substr(l - 3) === 'IOS') { - return isIOS ? prop.substr(0, l - 3) : ''; - } - return prop; - } -}; diff --git a/src/replacers/vars.js b/src/replacers/vars.js index 132edc0..05ee469 100644 --- a/src/replacers/vars.js +++ b/src/replacers/vars.js @@ -1,3 +1,7 @@ +/** + * Variables + */ + import resolvePath from 'object-resolve-path'; const PREFIX = '$'; @@ -32,21 +36,18 @@ function calc(str, varsArr) { } /** - * Extract variables / props from mixed object + * Extract variables from mixed object * @param {Object} obj + * @returns {null|Object} */ function extract(obj) { - let extractedProps = {}; - let extractedVars = null; - Object.keys(obj).forEach(key => { + return Object.keys(obj).reduce((res, key) => { if (isVar(key)) { - extractedVars = extractedVars || {}; - extractedVars[key] = obj[key]; - } else { - extractedProps[key] = obj[key]; + res = res || {}; + res[key] = obj[key]; } - }); - return {extractedProps, extractedVars}; + return res; + }, null); } /** @@ -86,7 +87,8 @@ function get(name, varsArr) { */ function addPrefix(obj) { return Object.keys(obj).reduce((res, key) => { - res[`${PREFIX}${key}`] = obj[key]; + const resKey = key.charAt(0) !== PREFIX ? PREFIX + key : key; + res[resKey] = obj[key]; return res; }, {}); } diff --git a/src/sheet.js b/src/sheet.js index 258b56a..e1ffa9f 100644 --- a/src/sheet.js +++ b/src/sheet.js @@ -1,6 +1,8 @@ import {StyleSheet} from 'react-native'; import Style from './style'; +import utils from './utils'; import vars from './replacers/vars'; +import mediaQueries from './replacers/media-queries'; export default class { /** @@ -9,9 +11,11 @@ export default class { */ constructor(source) { this.source = source; - this.result = {}; + this.result = Object.create(null); this.nativeSheet = {}; this.varsArr = []; + this.extractedVars = null; + this.processedSource = null; } /** @@ -19,26 +23,33 @@ export default class { * @param {Object} inVars */ calc(inVars) { - let {extractedProps: extractedStyles, extractedVars} = vars.extract(this.source); - this.varsArr = inVars ? [inVars] : []; - if (extractedVars) { - this.calcVars(extractedVars); - } - this.calcStyles(extractedStyles); + this.processSource(); + this.calcVars(inVars); + this.calcStyles(); this.calcNative(); + return this.getResult(); + } + + processSource() { + this.processedSource = mediaQueries.process(this.source); } - calcVars(extractedVars) { - let varsArrForVars = [extractedVars].concat(this.varsArr); - let {calculatedVars} = new Style(extractedVars, varsArrForVars).calc(); - Object.assign(this.result, calculatedVars); - this.varsArr = [calculatedVars].concat(this.varsArr); + calcVars(inVars) { + this.varsArr = inVars ? [inVars] : []; + this.extractedVars = vars.extract(this.processedSource); + if (this.extractedVars) { + let varsArrForVars = [this.extractedVars].concat(this.varsArr); + let {calculatedVars} = new Style(this.extractedVars, varsArrForVars).calc(); + Object.assign(this.result, calculatedVars); + this.varsArr = [calculatedVars].concat(this.varsArr); + } } - calcStyles(extractedStyles) { + calcStyles() { + const extractedStyles = utils.excludeKeys(this.processedSource, this.extractedVars); Object.keys(extractedStyles).forEach(key => { - let {calculatedProps, calculatedVars} = new Style(extractedStyles[key], this.varsArr).calc(); - let merged = Object.assign({}, calculatedVars, calculatedProps); + const {calculatedProps, calculatedVars} = new Style(extractedStyles[key], this.varsArr).calc(); + const merged = Object.assign({}, calculatedVars, calculatedProps); if (key.charAt(0) === '_') { this.result[key] = merged; } else { diff --git a/src/style.js b/src/style.js index ed50519..cdeb3a3 100644 --- a/src/style.js +++ b/src/style.js @@ -2,9 +2,10 @@ * Style */ -import osprop from './replacers/osprop'; import vars from './replacers/vars'; +import mediaQueries from './replacers/media-queries'; import Value from './value'; +import utils from './utils'; export default class { /** @@ -15,35 +16,44 @@ export default class { constructor(source, varsArr = []) { this.source = source; this.varsArr = varsArr; + this.processedSource = null; + this.extractedVars = null; + this.extractedProps = null; this.calculatedVars = null; this.calculatedProps = null; } /** * Calculates style - * @returns {Object} {calculatedVars, calculatedStyles} + * @returns {Object} */ calc() { - let {extractedProps, extractedVars} = vars.extract(this.source); - if (extractedVars) { - this.calcVars(extractedVars); - } - this.calcProps(extractedProps); + this.processSource(); + this.calcVars(); + this.calcProps(); + this.tryOutline(); return { calculatedVars: this.calculatedVars, calculatedProps: this.calculatedProps, }; } - calcVars(extractedVars) { - let varsArrForVars = [extractedVars].concat(this.varsArr); - this.calculatedVars = calcPlainObject(extractedVars, varsArrForVars); - this.varsArr = [this.calculatedVars].concat(this.varsArr); + processSource() { + this.processedSource = mediaQueries.process(this.source); } - calcProps(extractedProps) { - this.calculatedProps = calcPlainObject(extractedProps, this.varsArr); - this.tryOutline(); + calcVars() { + this.extractedVars = vars.extract(this.processedSource); + if (this.extractedVars) { + const varsArrForVars = [this.extractedVars].concat(this.varsArr); + this.calculatedVars = calcPlainObject(this.extractedVars, varsArrForVars); + this.varsArr = [this.calculatedVars].concat(this.varsArr); + } + } + + calcProps() { + this.extractedProps = utils.excludeKeys(this.processedSource, this.extractedVars); + this.calculatedProps = calcPlainObject(this.extractedProps, this.varsArr); } tryOutline() { @@ -65,11 +75,7 @@ export default class { */ function calcPlainObject(obj, varsArr) { return Object.keys(obj).reduce((res, prop) => { - let value = obj[prop]; - prop = osprop.replace(prop); - if (prop) { - res[prop] = new Value(value, prop, varsArr).calc(); - } + res[prop] = new Value(obj[prop], prop, varsArr).calc(); return res; }, {}); } diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..e4d59ff --- /dev/null +++ b/src/utils.js @@ -0,0 +1,33 @@ +/** + * Utils + */ + +export default { + excludeKeys, + isObject, +}; + +/** + * Returns new object with excluded keys + * @param {Object} obj + * @param {Array|Object} keys + */ +function excludeKeys(obj, keys) { + keys = Array.isArray(keys) + ? keys + : (keys ? Object.keys(keys) : []); + return Object.keys(obj).reduce((res, key) => { + if (keys.indexOf(key) === -1) { + res[key] = obj[key]; + } + return res; + }, {}); +} + +/** + * Is object + * @param {*} obj + */ +function isObject(obj) { + return typeof obj === 'object' && obj !== null; +}