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;
+}