├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── examples ├── gh-pages │ ├── README.md │ ├── components │ │ ├── Alignments.js │ │ ├── Box.js │ │ ├── Distribution.js │ │ ├── Header.js │ │ ├── Intro.js │ │ ├── Offset.js │ │ ├── Root.js │ │ └── Spacing.js │ ├── constants.js │ ├── index.html │ ├── index.js │ ├── main.scss │ ├── package.json │ ├── webpack.config.base.js │ ├── webpack.config.development.js │ ├── webpack.config.production.js │ └── webpack.server.js └── react-transform-boilerplate │ ├── .babelrc │ ├── README.md │ ├── index.html │ ├── package.json │ ├── server.js │ ├── src │ ├── App.js │ ├── Box.js │ ├── Content.js │ ├── ToolBar.js │ ├── actionCreators.js │ └── index.js │ └── webpack.config.js ├── package.json ├── src ├── components │ ├── Grid.js │ └── createComponent.js ├── constants.js ├── index.js ├── matchMedia.js ├── reducers │ ├── media.js │ └── reference.js ├── store.js └── utils │ ├── cache.js │ ├── calcPropWithGutter.js │ ├── capitalize.js │ ├── fixUserAgent.js │ ├── invariant.js │ ├── isUAFixNeeded.js │ ├── memoize.js │ └── pick.js ├── test ├── components │ └── Grid.spec.js ├── reducers │ ├── media.js │ └── reference.js └── utils │ ├── cache.spec.js │ ├── calcPropWithGutter.spec.js │ ├── capitalize.spec.js │ ├── fixUserAgent.spec.js │ ├── invariant.spec.js │ ├── isUAFixNeeded.js │ └── pick.spec.js ├── webpack.config.base.js ├── webpack.config.development.js └── webpack.config.production.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "loose": "all" 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [Makefile] 13 | indent_style = tab 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | dist 3 | node_modules 4 | coverage 5 | examples/buildAll.js 6 | examples/gh-pages/node_modules 7 | examples/gh-pages/dist 8 | examples/gh-pages/webpack.server.js 9 | examples/gh-pages/webpack.config.base.js 10 | examples/gh-pages/webpack.config.development.js 11 | examples/gh-pages/webpack.config.production.js 12 | examples/react-transform-boilerplate/node_modules 13 | examples/react-transform-boilerplate/server.js 14 | examples/react-transform-boilerplate/webpack.config.js 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "rules": { 8 | "comma-dangle": 0, 9 | "react/jsx-uses-react": 2, 10 | "react/jsx-uses-vars": 2, 11 | "react/react-in-jsx-scope": 2 12 | }, 13 | "plugins": [ 14 | "react" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | dist 3 | node_modules 4 | coverage 5 | *.log 6 | .DS_Store 7 | .idea 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | node_modules 3 | coverage 4 | *.log 5 | .DS_Store 6 | .idea 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Pierre Brouca 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [React Inline Grid](http://broucz.github.io/react-inline-grid) 2 | 3 | **A predictable gird layout based on flexbox for [React](https://facebook.github.io/react/) applications using inline styles.** 4 | 5 | [![npm version](https://img.shields.io/npm/v/react-inline-grid.svg?style=flat-square)](https://www.npmjs.com/package/react-inline-grid) 6 | 7 | ## Install 8 | `npm install react-inline-grid --save` 9 | 10 | ## API 11 | 12 | ### Sample Usage 13 | 14 | ```js 15 | import React from 'react'; 16 | import ReactDOM from 'react-dom'; 17 | import { Grid, Row, Cell } from 'react-inline-grid'; 18 | 19 | const Layout = React.createClass({ 20 | render() { 21 | return ( 22 | 23 | 24 |
content_a
25 |
content_b
26 |
27 |
28 | ); 29 | } 30 | }); 31 | 32 | ReactDOM.render(, document.body); 33 | ``` 34 | The library exports `Grid`, `Row` and `Cell`. 35 | 36 | ### <Grid /> 37 | Grid wrap inner components with [React Redux](https://github.com/rackt/react-redux#provider-store) ``. 38 | 39 | Using [Redux](https://github.com/rackt/redux), Grid's inner components can react to store update. Here Redux is used to handle [MediaQueryList](https://developer.mozilla.org/en/docs/Web/API/MediaQueryList) changes and update components `style` property: 40 | 41 | ```js 42 | // phone 43 |
44 | 45 | // tablet 46 |
47 | 48 | // desktop 49 |
50 | ``` 51 | 52 | Grid exposes the property `options` allowing you to define custom grid settings. 53 | 54 | `options` shape: 55 | 56 | ```js 57 | { 58 | columns: number // default = 12 - Columns size for the bigger media. 59 | gutter: number // default = 16 - Gutter size in pixel. 60 | margin: number // default = 16 - Margin size in pixel. 61 | deaf: bool // default = false - Ignore MediaQueryList updates. 62 | list: [ // default = [...] - List of target media. 63 | { 64 | name: string // required - Media name. 65 | query: string // required - Media query to test. 66 | gutter: number // default = options -> gutter - Media gutter size in pixel. 67 | margin: number // default = options -> margin - Media margin size in pixel. 68 | } 69 | ] 70 | } 71 | ``` 72 | 73 | If `options` is not provided, or invalid, it will be fixed to apply values inspired by [Google Material Design Lite](http://www.getmdl.io/) grid layout: 74 | 75 | ```js 76 | // options -> list 77 | [ 78 | { 79 | name: 'phone', 80 | gutter: 16, 81 | margin: 16, 82 | columns: 4, 83 | query: '(max-width: 479px)' 84 | }, 85 | { 86 | name: 'tablet', 87 | gutter: 16, 88 | margin: 16, 89 | columns: 8, 90 | query: '(min-width: 480px) and (max-width: 839px)' 91 | }, 92 | { 93 | name: 'desktop', 94 | gutter: 16, 95 | margin: 16, 96 | columns: 12, 97 | query: '(min-width: 840px)' 98 | } 99 | ] 100 | ``` 101 | 102 | If no media match the queries, Grid will define the first `options -> list -> value` as default current media in order to match the "popular" mobile first approch. 103 | 104 | ### <Row /> 105 | 106 | Exposes the property `is` (string) to update the following default style object: 107 | 108 | ```js 109 | { 110 | display: 'flex', 111 | flexFlow: 'row wrap', 112 | alignItems: 'stretch' 113 | } 114 | ``` 115 | `is` specify the `justify-content` style property as: 116 | - `start` 117 | - `center` 118 | - `end` 119 | - `around` 120 | - `between` 121 | 122 | ```js 123 | 124 | 125 |
Content
126 |
127 |
128 | 129 | // not phone 130 |
131 | 132 |
Content
133 |
134 |
135 | 136 | // phone 137 |
138 | 139 |
Content
140 |
141 |
142 | 143 | ``` 144 | 145 | ### <Cell /> 146 | 147 | Exposes the property `is` (string) to update the following default style object: 148 | 149 | ```js 150 | { 151 | boxSizing: 'border-box' 152 | } 153 | ``` 154 | `is` specify cell size and `align-self` style property as: 155 | - `` 156 | - `-` 157 | - `-offset-` 158 | - `top` 159 | - `middle` 160 | - `bottom` 161 | - `stretch` 162 | 163 | ```js 164 | 165 | 166 |
Content
167 |
168 |
169 | 170 | // desktop 171 | 172 |
173 |
Content
174 |
175 |
176 | 177 | // tablet 178 | 179 |
180 |
Content
181 |
182 |
183 | 184 | // phone 185 | 186 |
187 |
Content
188 |
189 |
190 | ``` 191 | 192 | For both `` and ``, `is` property ask for an "already defined" values, the last one is used: 193 | 194 | ```js 195 | 196 |
Content
197 |
198 | 199 | // will be defined as 200 | 201 | 202 |
Content
203 |
204 | ``` 205 | 206 | ## Examples 207 | 208 | The [gh-pages](http://broucz.github.io/react-inline-grid/) page of this repository use some patterns as examples, but feel free to play and test your layouts using the `examples` folder. 209 | 210 | Run the gh-pages example: 211 | 212 | ``` 213 | git clone https://github.com/broucz/react-inline-grid.git 214 | 215 | cd react-inline-grid 216 | npm install 217 | 218 | cd examples/react-transform-boilerplate 219 | npm install 220 | 221 | npm start 222 | open http://localhost:3000/ 223 | ``` 224 | 225 | ## Thanks 226 | 227 | * [Redux](https://github.com/rackt/redux) I learned a lot from package evolution, author [@gaearon](https://github.com/gaearon), contributors, and related discussions. 228 | * [React](https://facebook.github.io/react) for the fun. 229 | * [React Redux](https://github.com/rackt/react-redux) to make it easier. 230 | -------------------------------------------------------------------------------- /examples/gh-pages/README.md: -------------------------------------------------------------------------------- 1 | react-inline-grid-gh-pages 2 | -------------------------------------------------------------------------------- /examples/gh-pages/components/Alignments.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Row, Cell } from 'react-inline-grid'; 3 | import Code from 'react-embed-code'; 4 | import { COLOR } from '../constants'; 5 | import Box from './Box'; 6 | 7 | const { gray, primary } = COLOR; 8 | 9 | const strHorizontal = `Place at the 'start', 'center', or 'end' of a . 10 | `; 11 | 12 | const strVertical = `Place at the 'top', 'middle', or 'bottom' of a . 13 | `; 14 | 15 | const str1 = ` 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | `; 33 | 34 | const str2 = ` 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | `; 61 | 62 | class Alignments extends Component { 63 | render() { 64 | return ( 65 |
66 | 67 | 68 |

Alignments

69 |

Horizontal

70 | 71 |
72 |
73 | 74 | 75 |
76 | 77 | 78 | 79 | 80 | 81 |
82 |
83 |
84 | 85 | 86 |
87 | 88 | 89 | 90 | 91 | 92 |
93 |
94 |
95 | 96 | 97 |
98 | 99 | 100 | 101 | 102 | 103 |
104 |
105 |
106 | 107 | 108 | 109 | 110 |

Vertical

111 | 112 |
113 |
114 | 115 | 116 | 117 |
118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 |
127 |
128 |
129 | 130 | 131 |
132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 |
141 |
142 |
143 | 144 | 145 |
146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 |
155 |
156 |
157 | 158 | 159 | 160 | 161 | 162 | 163 |
164 | ); 165 | } 166 | } 167 | 168 | export default Alignments; 169 | -------------------------------------------------------------------------------- /examples/gh-pages/components/Box.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | const STYLE_PROPS = { 4 | boxDefault: { 5 | color: 'white', 6 | paddingLeft: '8px', 7 | paddingTop: '4px' 8 | }, 9 | box: { height: '30px' }, 10 | big: { height: '90px' }, 11 | huge: { height: '200px' } 12 | }; 13 | 14 | const { boxDefault } = STYLE_PROPS; 15 | 16 | class Box extends Component { 17 | render() { 18 | const { color, size } = this.props; 19 | const boxStyle = 20 | STYLE_PROPS[size] 21 | || STYLE_PROPS.box; 22 | const background = { background: color }; 23 | return ( 24 |
25 | ); 26 | } 27 | } 28 | 29 | Box.propTypes = { 30 | color: PropTypes.string, 31 | size: PropTypes.string 32 | }; 33 | 34 | export default Box; 35 | -------------------------------------------------------------------------------- /examples/gh-pages/components/Distribution.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Row, Cell } from 'react-inline-grid'; 3 | import Code from 'react-embed-code'; 4 | import { COLOR } from '../constants'; 5 | import Box from './Box'; 6 | 7 | const { gray, primary } = COLOR; 8 | 9 | const strAround = ` are positioned with space before && between && and after. 10 | `; 11 | 12 | const strBetween = ` are positioned with space between. 13 | `; 14 | 15 | const str1 = ` 16 | 17 | 18 | 19 | 20 | `; 21 | 22 | const str2 = ` 23 | 24 | 25 | 26 | 27 | `; 28 | 29 | class Distribution extends Component { 30 | render() { 31 | return ( 32 |
33 | 34 | 35 |

Distribution

36 |

Around

37 | 38 |
39 |
40 | 41 | 42 |
43 | 44 | 45 | 46 | 47 | 48 |
49 |
50 |
51 | 52 | 53 | 54 | 55 |

Between

56 | 57 |
58 |
59 | 60 | 61 | 62 |
63 | 64 | 65 | 66 | 67 | 68 |
69 |
70 |
71 | 72 | 73 | 74 | 75 | 76 |
77 | ); 78 | } 79 | } 80 | 81 | export default Distribution; 82 | -------------------------------------------------------------------------------- /examples/gh-pages/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class Header extends Component { 4 | render() { 5 | return ( 6 |
7 |

react-inline-grid

8 |

A predictable gird layout based on flexbox for React applications using inline styles.

9 | View on GitHub 10 |
11 | ); 12 | } 13 | } 14 | 15 | export default Header; 16 | -------------------------------------------------------------------------------- /examples/gh-pages/components/Intro.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Row, Cell } from 'react-inline-grid'; 3 | import { COLOR } from '../constants'; 4 | import Box from './Box'; 5 | 6 | const { gray, primary } = COLOR; 7 | 8 | class Intro extends Component { 9 | render() { 10 | return ( 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | ); 28 | } 29 | } 30 | 31 | export default Intro; 32 | -------------------------------------------------------------------------------- /examples/gh-pages/components/Offset.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Row, Cell } from 'react-inline-grid'; 3 | import Code from 'react-embed-code'; 4 | import { COLOR } from '../constants'; 5 | import Box from './Box'; 6 | 7 | const { primary } = COLOR; 8 | 9 | const strOffset = `Apply 'margin-left' to 10 | `; 11 | 12 | const str = ` 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | `; 28 | 29 | class Offset extends Component { 30 | render() { 31 | return ( 32 |
33 | 34 | 35 |

Offset

36 | 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
50 | ); 51 | } 52 | } 53 | 54 | export default Offset; 55 | -------------------------------------------------------------------------------- /examples/gh-pages/components/Root.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Grid, Row, Cell } from 'react-inline-grid'; 3 | import Header from './Header'; 4 | import Intro from './Intro'; 5 | import Alignments from './Alignments'; 6 | import Offset from './Offset'; 7 | import Distribution from './Distribution'; 8 | import Spacing from './Spacing'; 9 | 10 | class Root extends Component { 11 | render() { 12 | return ( 13 | 14 |
15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 |
29 | ); 30 | } 31 | } 32 | 33 | export default Root; 34 | -------------------------------------------------------------------------------- /examples/gh-pages/components/Spacing.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Row, Cell } from 'react-inline-grid'; 3 | import Code from 'react-embed-code'; 4 | import { COLOR } from '../constants'; 5 | import Box from './Box'; 6 | 7 | const { gray, primary } = COLOR; 8 | 9 | const strSpace = `Remove padding and maring. 10 | `; 11 | 12 | const str = ` 13 | 14 | 15 | 16 | 17 | `; 18 | 19 | class Spacing extends Component { 20 | render() { 21 | return ( 22 |
23 | 24 | 25 |

No Spacing

26 | 27 |
28 |
29 | 30 | 31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 |
39 |
40 |
41 | 42 | 43 | 44 | 45 | 46 |
47 | ); 48 | } 49 | } 50 | 51 | export default Spacing; 52 | -------------------------------------------------------------------------------- /examples/gh-pages/constants.js: -------------------------------------------------------------------------------- 1 | export const COLOR = { 2 | gray: '#bdbdbd', 3 | primary: '#4285F4' 4 | }; 5 | -------------------------------------------------------------------------------- /examples/gh-pages/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | react-inline-grid 8 | 9 | 10 |
11 | 12 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/gh-pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Root from './components/Root'; 4 | 5 | import 'normalize.css'; 6 | import './main.scss'; 7 | 8 | ReactDOM.render(, document.getElementById('mount')); 9 | -------------------------------------------------------------------------------- /examples/gh-pages/main.scss: -------------------------------------------------------------------------------- 1 | $large-breakpoint: 64em; 2 | $medium-breakpoint: 42em; 3 | 4 | @mixin large { 5 | @media screen and (min-width: #{$large-breakpoint}) { 6 | @content; 7 | } 8 | } 9 | 10 | @mixin medium { 11 | @media screen and (min-width: #{$medium-breakpoint}) and (max-width: #{$large-breakpoint}) { 12 | @content; 13 | } 14 | } 15 | 16 | @mixin small { 17 | @media screen and (max-width: #{$medium-breakpoint}) { 18 | @content; 19 | } 20 | } 21 | 22 | * { 23 | box-sizing: border-box; 24 | } 25 | 26 | body { 27 | padding: 0; 28 | margin: 0; 29 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 30 | font-size: 16px; 31 | line-height: 1.5; 32 | color: #606c71; 33 | } 34 | 35 | a { 36 | color: #4285F4; 37 | text-decoration: none; 38 | 39 | &:hover { 40 | text-decoration: underline; 41 | } 42 | } 43 | 44 | .btn { 45 | display: inline-block; 46 | margin-bottom: 1em; 47 | color: rgba(255, 255, 255, 0.7); 48 | background-color: rgba(255, 255, 255, 0.08); 49 | border-color: rgba(255, 255, 255, 0.2); 50 | border-style: solid; 51 | border-width: 1px; 52 | border-radius: 0.3em; 53 | transition: color 0.2s, background-color 0.2s, border-color 0.2s; 54 | 55 | &:hover { 56 | color: rgba(255, 255, 255, 0.8); 57 | text-decoration: none; 58 | background-color: rgba(255, 255, 255, 0.2); 59 | border-color: rgba(255, 255, 255, 0.3); 60 | } 61 | 62 | + .btn { 63 | margin-left: 1em; 64 | } 65 | 66 | @include large { 67 | padding: 0.75em 1em; 68 | } 69 | 70 | @include medium { 71 | padding: 0.6em 0.9em; 72 | font-size: 0.9em; 73 | } 74 | 75 | @include small { 76 | display: block; 77 | width: 100%; 78 | padding: 0.75em; 79 | font-size: 0.9em; 80 | 81 | + .btn { 82 | margin-top: 1em; 83 | margin-left: 0; 84 | } 85 | } 86 | } 87 | 88 | .pageHeader { 89 | color: #fff; 90 | text-align: center; 91 | background: -webkit-linear-gradient(120deg, #4285F4 10%, #4A75BD 90%); 92 | background: -moz-linear-gradient(120deg, #4285F4 10%, #4A75BD 90%); 93 | background: -ms-linear-gradient(120deg, #4285F4 10%, #4A75BD 90%); 94 | background: -o-linear-gradient(120deg, #4285F4 10%, #4A75BD 90%); 95 | background: linear-gradient(120deg, #4285F4 10%, #4A75BD 90%); 96 | 97 | 98 | @include large { 99 | padding: 5em 6em; 100 | } 101 | 102 | @include medium { 103 | padding: 3em 4em; 104 | } 105 | 106 | @include small { 107 | padding: 2em 1em; 108 | } 109 | } 110 | 111 | .pageHeaderTitle { 112 | margin-top: 0; 113 | margin-bottom: 0.1em; 114 | font-family: Menlo, Monaco, Courier, monospace; 115 | 116 | @include large { 117 | font-size: 3.25em; 118 | } 119 | 120 | @include medium { 121 | font-size: 2.25em; 122 | } 123 | 124 | @include small { 125 | font-size: 1.75em; 126 | } 127 | } 128 | 129 | .pageHeaderTagline { 130 | margin-bottom: 2em; 131 | font-weight: normal; 132 | opacity: 0.8; 133 | 134 | @include large { 135 | font-size: 1.25em; 136 | } 137 | 138 | @include medium { 139 | font-size: 1.15em; 140 | } 141 | 142 | @include small { 143 | font-size: 1em; 144 | } 145 | 146 | a { 147 | color: #fff; 148 | text-decoration: underline; 149 | } 150 | } 151 | 152 | .pageContent { 153 | font-size: 1.1em; 154 | } 155 | -------------------------------------------------------------------------------- /examples/gh-pages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-inline-grid-gh-pages", 3 | "version": "0.0.0", 4 | "description": "Github website for React Inline Grid", 5 | "scripts": { 6 | "start": "node webpack.server.js" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/broucz/react-inline-grid.git" 11 | }, 12 | "author": "Pierre Brouca (https://github.com/broucz)", 13 | "license": "MIT", 14 | "bugs": { 15 | "url": "https://github.com/broucz/react-inline-grid/issues" 16 | }, 17 | "homepage": "https://github.com/broucz/react-inline-grid", 18 | "dependencies": { 19 | "normalize.css": "^3.0.3", 20 | "react": "^0.14.0 || ^0.14.0-beta3 || ^0.14.0-rc1", 21 | "react-dom": "^0.14.0 || ^0.14.0-beta3 || ^0.14.0-rc1", 22 | "react-embed-code": "^0.1.0" 23 | }, 24 | "devDependencies": { 25 | "babel": "^5.8.21", 26 | "babel-core": "^5.8.22", 27 | "babel-loader": "^5.3.2", 28 | "css-loader": "^0.16.0", 29 | "node-sass": "^3.3.2", 30 | "react-hot-loader": "^1.2.8", 31 | "sass-loader": "^2.0.1", 32 | "style-loader": "^0.12.3", 33 | "webpack": "^1.11.0", 34 | "webpack-dev-server": "^1.10.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/gh-pages/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var webpack = require('webpack'); 5 | 6 | module.exports = { 7 | entry: [path.join(__dirname)], 8 | output: { 9 | path: path.join(__dirname, 'dist'), 10 | filename: 'bundle.js' 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.js$/, 16 | loaders: ['babel-loader'], 17 | exclude: /node_modules/ 18 | }, 19 | { 20 | test: /\.css$/, 21 | loaders: [ 'style-loader', 'css-loader' ] 22 | }, 23 | { 24 | test: /\.scss$/, 25 | loader: 'style!css!sass' 26 | } 27 | ] 28 | }, 29 | resolve: { 30 | alias: { 31 | 'react-inline-grid': path.join(__dirname, '..', '..', 'src'), 32 | 'react': path.resolve(__dirname, 'node_modules', 'react') 33 | } 34 | }, 35 | plugins: [ 36 | new webpack.optimize.OccurenceOrderPlugin() 37 | ] 38 | }; 39 | -------------------------------------------------------------------------------- /examples/gh-pages/webpack.config.development.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var path = require('path'); 5 | var webpack = require('webpack'); 6 | var baseConfig = require('./webpack.config.base'); 7 | 8 | module.exports = _.merge({}, baseConfig, { 9 | entry: _.union(baseConfig.entry, [ 10 | 'webpack-dev-server/client?http://localhost:3000', 11 | 'webpack/hot/only-dev-server' 12 | ]), 13 | plugins: _.union(baseConfig.plugins, [ 14 | new webpack.DefinePlugin({ 15 | '__DEV__': 'false', 16 | 'process.env.NODE_ENV': '"production"' 17 | }), 18 | new webpack.HotModuleReplacementPlugin(), 19 | new webpack.NoErrorsPlugin() 20 | ]) 21 | }); 22 | -------------------------------------------------------------------------------- /examples/gh-pages/webpack.config.production.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var webpack = require('webpack'); 5 | var baseConfig = require('./webpack.config.base'); 6 | 7 | module.exports = _.merge({}, baseConfig, { 8 | plugins: _.union(baseConfig.plugins, [ 9 | new webpack.DefinePlugin({ 10 | '__DEV__': 'false', 11 | 'process.env.NODE_ENV': '"production"' 12 | }), 13 | new webpack.optimize.UglifyJsPlugin({ 14 | compressor: { 15 | pure_getters: true, 16 | unsafe: true, 17 | unsafe_comps: true, 18 | screw_ie8: true, 19 | warnings: false 20 | } 21 | }) 22 | ]) 23 | }); 24 | -------------------------------------------------------------------------------- /examples/gh-pages/webpack.server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var WebpackDevServer = require('webpack-dev-server'); 3 | var config = require('./webpack.config.development'); 4 | 5 | new WebpackDevServer(webpack(config), { 6 | publicPath: config.output.publicPath, 7 | hot: true, 8 | historyApiFallback: true, 9 | stats: { 10 | colors: true 11 | } 12 | }).listen(3000, 'localhost', function (err) { 13 | if (err) { 14 | console.log(err); 15 | } 16 | 17 | console.log('Listening at localhost:3000'); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/react-transform-boilerplate/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "plugins": [ 4 | "react-transform" 5 | ], 6 | "extra": { 7 | "react-transform": [{ 8 | "target": "react-transform-webpack-hmr", 9 | "imports": ["react"], 10 | "locals": ["module"] 11 | }, { 12 | "target": "react-transform-catch-errors", 13 | "imports": ["react", "redbox-react"] 14 | }] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/react-transform-boilerplate/README.md: -------------------------------------------------------------------------------- 1 | react-inline-grid-transform-boilerplate 2 | -------------------------------------------------------------------------------- /examples/react-transform-boilerplate/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | DEV | react-inline-grid 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/react-transform-boilerplate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-inline-grid-transform-boilerplate", 3 | "version": "0.0.0", 4 | "description": "Workspace for react-inline-gridA using https://github.com/gaearon/react-transform-boilerplate.", 5 | "scripts": { 6 | "start": "node server.js" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/broucz/react-inline-grid.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/broucz/react-inline-grid/issues" 14 | }, 15 | "homepage": "https://github.com/broucz/react-inline-grid", 16 | "devDependencies": { 17 | "babel-core": "^5.4.7", 18 | "babel-eslint": "^3.1.9", 19 | "babel-loader": "^5.1.2", 20 | "babel-plugin-react-transform": "^1.0.1", 21 | "eslint": "^1.3.1", 22 | "eslint-plugin-react": "^2.3.0", 23 | "express": "^4.13.3", 24 | "react-transform-catch-errors": "^0.1.1", 25 | "react-transform-webpack-hmr": "^0.1.2", 26 | "redbox-react": "^1.0.1", 27 | "webpack": "^1.9.6", 28 | "webpack-dev-middleware": "^1.2.0", 29 | "webpack-hot-middleware": "^2.0.0" 30 | }, 31 | "dependencies": { 32 | "react": "^0.14.0 || ^0.14.0-beta3 || ^0.14.0-rc1", 33 | "react-dom": "^0.14.0 || ^0.14.0-beta3 || ^0.14.0-rc1", 34 | "react-redux": "^2.1.1", 35 | "redux": "^2.0.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/react-transform-boilerplate/server.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var webpack = require('webpack'); 4 | var config = require('./webpack.config'); 5 | 6 | var app = express(); 7 | var compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | noInfo: true, 11 | publicPath: config.output.publicPath 12 | })); 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)); 15 | 16 | app.get('*', function(req, res) { 17 | res.sendFile(path.join(__dirname, 'index.html')); 18 | }); 19 | 20 | app.listen(3000, 'localhost', function (err) { 21 | if (err) { 22 | console.log(err); 23 | return; 24 | } 25 | 26 | console.log('Listening at http://localhost:3000'); 27 | }); -------------------------------------------------------------------------------- /examples/react-transform-boilerplate/src/App.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-multi-comp */ 2 | 3 | import React, { Component } from 'react'; 4 | import { Grid } from 'react-inline-grid'; 5 | import ToolBar from './ToolBar'; 6 | import Content from './Content'; 7 | 8 | const options = { 9 | gutter: 16, 10 | margin: 16, 11 | list: [ 12 | { 13 | name: 'phone', 14 | query: '(max-width: 479px)' 15 | }, 16 | { 17 | name: 'tablet', 18 | query: '(min-width: 480px) and (max-width: 839px)' 19 | }, 20 | { 21 | name: 'desktop', 22 | query: '(min-width: 840px)' 23 | } 24 | ] 25 | }; 26 | 27 | class WorkSpace extends Component { 28 | render() { 29 | return ( 30 | 31 |
32 | 33 | 34 |
35 |
36 | ); 37 | } 38 | } 39 | 40 | export class App extends Component { 41 | render() { 42 | return ( 43 |
44 | 45 |
46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/react-transform-boilerplate/src/Box.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | const container = { 4 | box: { height: '30px' }, 5 | big: { height: '90px' }, 6 | huge: { height: '200px' } 7 | }; 8 | 9 | export default class Box extends Component { 10 | static propTypes = { 11 | color: PropTypes.string.isRequired, 12 | size: PropTypes.string 13 | }; 14 | 15 | render() { 16 | const { color, size } = this.props; 17 | const height = container[size] || container.box; 18 | const background = { background: color }; 19 | return ( 20 |
21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/react-transform-boilerplate/src/Content.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Row, Cell } from 'react-inline-grid'; 3 | import Box from './Box'; 4 | 5 | const gray = '#bdbdbd'; 6 | const primary = '#4A75BD'; 7 | 8 | export default class Content extends Component { 9 | render() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/react-transform-boilerplate/src/ToolBar.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { bindActionCreators } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import { Row, Cell } from 'react-inline-grid'; 5 | import { updateMediaName } from './actionCreators'; 6 | 7 | const mapStateToProps = state => ({ ...state }); 8 | const mapDispatchToProps = dispatch => { 9 | return bindActionCreators({ updateMediaName }, dispatch); 10 | }; 11 | 12 | const container = { 13 | color: '#fff', 14 | fontFamily: 'Helvetica Neue,Helvetica,Arial,sans-serif', 15 | fontSize: '.9rem', 16 | background: '#4A75BD' 17 | }; 18 | 19 | const btn = { 20 | width: '100%', 21 | boxSizing: 'border-box', 22 | padding: '.75em 1em', 23 | textAlign: 'center', 24 | color: 'rgba(255, 255, 255, 0.7)', 25 | backgroundColor: 'rgba(255, 255, 255, 0.08)', 26 | border: '1px solid rgba(255, 255, 255, 0.2)', 27 | borderRadius: '0.3em' 28 | }; 29 | 30 | class ToolBar extends Component { 31 | static propTypes = { 32 | updateMediaName: PropTypes.func.isRequired 33 | }; 34 | 35 | render() { 36 | return ( 37 |
38 | 39 | 40 |
phone
41 |
42 | 43 |
tablet
44 |
45 | 46 |
desktop
47 |
48 |
49 |
50 | ); 51 | } 52 | 53 | handleUpdateMediaName(name) { 54 | return this.props.updateMediaName(name); 55 | } 56 | } 57 | 58 | export default connect( 59 | mapStateToProps, 60 | mapDispatchToProps 61 | )(ToolBar); 62 | -------------------------------------------------------------------------------- /examples/react-transform-boilerplate/src/actionCreators.js: -------------------------------------------------------------------------------- 1 | export function updateMediaName(payload) { 2 | return { 3 | type: 'media/name/UPDATE', 4 | payload 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /examples/react-transform-boilerplate/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { App } from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /examples/react-transform-boilerplate/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | entry: [ 7 | 'webpack-hot-middleware/client', 8 | './src/index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/static/' 14 | }, 15 | plugins: [ 16 | new webpack.HotModuleReplacementPlugin(), 17 | new webpack.NoErrorsPlugin() 18 | ], 19 | resolve: { 20 | alias: { 21 | 'react-inline-grid': path.join(__dirname, '..', '..', 'src'), 22 | 'react': path.resolve('node_modules', 'react') 23 | }, 24 | extensions: ['', '.js'] 25 | }, 26 | module: { 27 | loaders: [{ 28 | test: /\.js$/, 29 | loaders: ['babel-loader'], 30 | include: [ 31 | path.join(__dirname, 'src'), 32 | path.join(__dirname, '..', '..', 'src') 33 | ] 34 | }] 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-inline-grid", 3 | "description": "A predictable gird layout based on flexbox for React.", 4 | "main": "./lib/index.js", 5 | "version": "0.5.3", 6 | "scripts": { 7 | "clean": "rimraf lib dist coverage", 8 | "lint": "eslint src test examples", 9 | "build:lib": "babel src --out-dir lib", 10 | "build:umd": "webpack src/index.js dist/react-inline-grid.js --config webpack.config.development.js", 11 | "build:umd:min": "webpack src/index.js dist/react-inline-grid.min.js --config webpack.config.production.js", 12 | "build": "npm run build:lib && npm run build:umd && npm run build:umd:min", 13 | "prepublish": "npm run clean && npm run build", 14 | "gh:clean": "rimraf examples/gh-pages/dist", 15 | "gh:build": "webpack -p --config examples/gh-pages/webpack.config.production.js && cp examples/gh-pages/index.html examples/gh-pages/dist/", 16 | "gh:publish": "npm run gh:clean && npm run gh:build && cd examples/gh-pages/dist && git init && git commit --allow-empty -m 'update gh-pages' && git checkout -b gh-pages && git add . && git commit -am 'update gh-pages' && git push git@github.com:broucz/react-inline-grid gh-pages --force", 17 | "test": "mocha --compilers js:babel/register --recursive", 18 | "test:watch": "npm test -- --watch", 19 | "test:cov": "babel-node $(npm bin)/isparta cover $(npm bin)/_mocha -- --recursive" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/broucz/react-inline-grid.git" 24 | }, 25 | "keywords": [ 26 | "react", 27 | "reactjs", 28 | "grid", 29 | "inline", 30 | "style", 31 | "flux", 32 | "redux", 33 | "predictable", 34 | "react-component" 35 | ], 36 | "author": "Pierre Brouca (https://github.com/broucz)", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/broucz/react-inline-grid/issues" 40 | }, 41 | "homepage": "https://github.com/broucz/react-inline-grid", 42 | "dependencies": { 43 | "lodash": "^3.10.1", 44 | "react-redux": "^2.1.1", 45 | "redux": "^2.0.0" 46 | }, 47 | "peerDependencies": { 48 | "react": "^0.14.0 || ^0.14.0-beta3 || ^0.14.0-rc1" 49 | }, 50 | "devDependencies": { 51 | "babel": "^5.8.21", 52 | "babel-core": "^5.8.22", 53 | "babel-eslint": "^4.0.10", 54 | "babel-loader": "^5.3.2", 55 | "eslint": "^1.2.0", 56 | "eslint-config-airbnb": "0.0.7", 57 | "eslint-plugin-react": "^3.2.3", 58 | "expect": "^1.9.0", 59 | "isparta": "^3.0.3", 60 | "mocha": "^2.2.5", 61 | "react-hot-loader": "^1.2.8", 62 | "rimraf": "^2.4.2", 63 | "webpack": "^1.11.0", 64 | "webpack-dev-server": "^1.10.1" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/components/Grid.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes, Children } from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import matchMedia from '../matchMedia'; 4 | import store from '../store'; 5 | import { updateMediaName } from '../reducers/media'; 6 | import { MEDIA_MODEL_HELPER } from '../constants'; 7 | import invariant from '../utils/invariant'; 8 | 9 | const optionsShape = PropTypes.shape({ 10 | columns: PropTypes.number, 11 | gutter: PropTypes.number, 12 | margin: PropTypes.number, 13 | deaf: PropTypes.bool, 14 | list: PropTypes.arrayOf( 15 | PropTypes.shape({ 16 | name: PropTypes.string.isRequired, 17 | query: PropTypes.string.isRequired, 18 | gutter: PropTypes.number, 19 | margin: PropTypes.number 20 | }) 21 | ) 22 | }); 23 | 24 | export function ensureValue(options, base, key, value) { 25 | if (process.env.NODE_ENV !== 'production') { 26 | invariant( 27 | key, 28 | ` -> ensureValue -> key must be defined.` 29 | ); 30 | 31 | invariant( 32 | base, 33 | ` -> ensureValue -> base must be defined.` 34 | ); 35 | 36 | invariant( 37 | (typeof base[key] !== 'undefined'), 38 | ` -> ensureValue -> base -> key must be defined.` 39 | ); 40 | } 41 | 42 | if (value >= 0) return value; 43 | const result = (options && options[key] >= 0) 44 | ? options[key] 45 | : base[key]; 46 | return result; 47 | } 48 | 49 | export function ensureListProperties(options, base, list) { 50 | return list.map(n => { 51 | const { name, query, gutter, margin } = n; 52 | return { 53 | name, 54 | query, 55 | gutter: ensureValue(options, base, 'gutter', gutter), 56 | margin: ensureValue(options, base, 'margin', margin) 57 | }; 58 | }); 59 | } 60 | 61 | export function build(options = {}, base = {}) { 62 | const { 63 | columns, 64 | deaf = false, 65 | list = base.list 66 | } = options; 67 | 68 | const size = list.length; 69 | 70 | invariant( 71 | !!size, 72 | ' -> options -> list can not be empty' 73 | ); 74 | 75 | if (columns) { 76 | invariant( 77 | !(columns % size) > 0, 78 | ' -> options -> columns must be a multiple of ' + 79 | ' -> options -> list -> length' 80 | ); 81 | } 82 | 83 | return { 84 | columns: columns || size * 4, 85 | deaf, 86 | list: ensureListProperties(options, base, list) 87 | }; 88 | } 89 | 90 | export function setMedia(name) { 91 | return { name }; 92 | } 93 | 94 | export function setReference(options) { 95 | return { options }; 96 | } 97 | 98 | export default class Grid extends Component { 99 | static propTypes = { 100 | options: optionsShape, 101 | children: PropTypes.element.isRequired 102 | }; 103 | 104 | constructor(props, context) { 105 | super(props, context); 106 | 107 | // Initialize a new Model: 108 | // If -> options is missing, it return a default Model. 109 | // if -> options is provided, it return a valid Model. 110 | const model = build(props.options, MEDIA_MODEL_HELPER); 111 | 112 | this.match = matchMedia(model.list); 113 | this.shouldSubscribe = model.deaf !== true; 114 | 115 | // Initialize Redux `store`. 116 | const media = setMedia(this.match.getCurrentName); 117 | const reference = setReference(model); 118 | this.store = store({ media, reference }); 119 | } 120 | 121 | componentDidMount() { 122 | this.trySubscribe(); 123 | } 124 | 125 | componentWillUnmount() { 126 | this.tryUnsubscribe(); 127 | } 128 | 129 | render() { 130 | return ( 131 | 132 | {Children.only(this.props.children)} 133 | 134 | ); 135 | } 136 | 137 | trySubscribe() { 138 | if (this.shouldSubscribe && !this.unsubscribe) { 139 | this.unsubscribe = 140 | this.match.subscribe(this.handleChange.bind(this)); 141 | } 142 | } 143 | 144 | tryUnsubscribe() { 145 | if (this.unsubscribe) { 146 | this.unsubscribe(); 147 | this.unsubscribe = null; 148 | } 149 | } 150 | 151 | handleChange(payload) { 152 | if (!this.unsubscribe) { 153 | return; 154 | } 155 | this.store.dispatch(updateMediaName(payload)); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/components/createComponent.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes, Children, cloneElement } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import pick from '../utils/pick'; 4 | 5 | const mapStateToProps = state => ({ ...state }); 6 | 7 | const mergeProps = 8 | (stateProps, dispatchProps, { is, ...clean }) => ({ 9 | ...clean, 10 | grid: { 11 | ...stateProps, 12 | is 13 | } 14 | }); 15 | 16 | const gridShape = 17 | PropTypes.shape({ 18 | media: PropTypes.object.isRequired, 19 | reference: PropTypes.object.isRequired, 20 | is: PropTypes.string 21 | }).isRequired; 22 | 23 | const elem = (tag) => { 24 | return class Elem extends Component { 25 | static propTypes = { 26 | grid: gridShape 27 | }; 28 | 29 | shouldComponentUpdate(nextProps) { 30 | if (process.env.NODE_ENV !== 'production') { 31 | return true; 32 | } 33 | 34 | if (process.env.NODE_ENV === 'production') { 35 | return (nextProps.grid.media.name !== this.props.grid.media.name) 36 | || (nextProps.grid.is !== this.props.grid.is); 37 | } 38 | } 39 | 40 | render() { 41 | const { grid, children, ...clean } = this.props; 42 | return ( 43 |
44 | {Children.map(children, child => { 45 | return cloneElement(child, {...clean}); 46 | })} 47 |
48 | ); 49 | } 50 | }; 51 | }; 52 | 53 | export default function createComponent(tag) { 54 | return connect( 55 | mapStateToProps, 56 | null, 57 | mergeProps)(elem(tag)); 58 | } 59 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const UPDATE_MEDIA_NAME = 'media/name/UPDATE'; 2 | export const ROW_ID = 'row'; 3 | export const CELL_ID = 'cell'; 4 | export const MEDIA_MODEL_HELPER = { 5 | gutter: 16, 6 | margin: 16, 7 | list: [ 8 | { 9 | name: 'phone', 10 | query: '(max-width: 479px)' 11 | }, 12 | { 13 | name: 'tablet', 14 | query: '(min-width: 480px) and (max-width: 839px)' 15 | }, 16 | { 17 | name: 'desktop', 18 | query: '(min-width: 840px)' 19 | } 20 | ] 21 | }; 22 | export const WHITE_LIST = { 23 | [ROW_ID]: [ 24 | 'row', 25 | 'start', 26 | 'center', 27 | 'end', 28 | 'around', 29 | 'between', 30 | 'nospace' 31 | ], 32 | [CELL_ID]: [ 33 | 'cell', 34 | '1', 35 | '2', 36 | '3', 37 | '4', 38 | '5', 39 | '6', 40 | '7', 41 | '8', 42 | '9', 43 | '10', 44 | '11', 45 | '12', 46 | 'top', 47 | 'middle', 48 | 'bottom', 49 | 'stretch', 50 | 'between', 51 | 'offset', 52 | 'nospace' 53 | ] 54 | }; 55 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { ROW_ID, CELL_ID } from './constants'; 2 | import createComponent from './components/createComponent'; 3 | 4 | export { default as Grid } from './components/Grid'; 5 | 6 | const Row = createComponent(ROW_ID); 7 | const Cell = createComponent(CELL_ID); 8 | 9 | export { 10 | Row, 11 | Cell 12 | }; 13 | -------------------------------------------------------------------------------- /src/matchMedia.js: -------------------------------------------------------------------------------- 1 | function setModel(options) { 2 | return options.map(n => { 3 | const { name, query } = n; 4 | return { 5 | name, 6 | query 7 | }; 8 | }); 9 | } 10 | 11 | function setState(model, handleChange) { 12 | return model.map(({ name, query }) => { 13 | const match = window.matchMedia(query); 14 | match.add = () => match.addListener(handleChange); 15 | match.add(); 16 | match.remove = () => match.removeListener(handleChange); 17 | 18 | return { 19 | name, 20 | match 21 | }; 22 | }); 23 | } 24 | 25 | class MatchMedia { 26 | constructor(list) { 27 | this.listeners = []; 28 | this.state = []; 29 | this.model = setModel(list.slice()); 30 | 31 | return this.updateState(); 32 | } 33 | 34 | handleChange() { 35 | this.state.slice().forEach(({ match }) => match.remove()); 36 | 37 | return this.updateState(); 38 | } 39 | 40 | updateState() { 41 | this.state = 42 | setState(this.model, this.handleChange.bind(this)); 43 | 44 | return this.dispatchUpdate(); 45 | } 46 | 47 | getCurrentName() { 48 | const current = 49 | this.state.filter(({ match }) => match.matches); 50 | 51 | const { name } = 52 | (current.length > 0) 53 | ? current[0] 54 | : this.model[0]; 55 | 56 | return name; 57 | } 58 | 59 | dispatchUpdate() { 60 | const current = this.getCurrentName(); 61 | return this.listeners 62 | .slice() 63 | .forEach(listener => listener(current)); 64 | } 65 | 66 | subscribe(listener) { 67 | this.listeners.push(listener); 68 | 69 | return function unsubscribe() { 70 | if (this.listeners != null) { 71 | const index = this.listeners.indexOf(listener); 72 | this.listeners = this.listeners.slice(index, 1); 73 | } 74 | }; 75 | } 76 | } 77 | 78 | export default function matchMedia(list) { 79 | const mM = new MatchMedia(list); 80 | 81 | return { 82 | subscribe: mM.subscribe.bind(mM), 83 | getCurrentName: mM.getCurrentName() 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /src/reducers/media.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_MEDIA_NAME } from '../constants'; 2 | 3 | export function hydrateMedia({ name }) { 4 | return { 5 | name 6 | }; 7 | } 8 | 9 | export function updateMediaName(payload) { 10 | return { 11 | type: UPDATE_MEDIA_NAME, 12 | payload 13 | }; 14 | } 15 | 16 | export default function media(state = {}, action = {}) { 17 | switch (action.type) { 18 | case UPDATE_MEDIA_NAME: 19 | return { 20 | ...state, 21 | name: action.payload 22 | }; 23 | default: 24 | return state; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/reducers/reference.js: -------------------------------------------------------------------------------- 1 | import isUAFixNeeded from '../utils/isUAFixNeeded'; 2 | import fixUserAgent from '../utils/fixUserAgent'; 3 | import calcPropWithGutter from '../utils/calcPropWithGutter'; 4 | import { ROW_ID, CELL_ID } from '../constants'; 5 | 6 | const ROW_ROOT = { 7 | display: 'flex', 8 | flexFlow: 'row wrap', 9 | alignItems: 'stretch' 10 | }; 11 | 12 | export const buildRow = (id, FIXED_ROW, gutter, margin) => { 13 | return { 14 | [id]: { 15 | ...FIXED_ROW, 16 | padding: `${margin - (gutter / 2)}px` 17 | } 18 | }; 19 | }; 20 | 21 | export const buildRowTypeProperties = (justifyContent) => { 22 | return { 23 | start: { [justifyContent]: 'flex-start' }, 24 | center: { [justifyContent]: 'center' }, 25 | end: { [justifyContent]: 'flex-end' }, 26 | around: { [justifyContent]: 'space-around' }, 27 | between: { [justifyContent]: 'space-between' } 28 | }; 29 | }; 30 | 31 | export const buildCell = (id, gutter) => { 32 | return { 33 | [id]: { 34 | boxSizing: 'border-box', 35 | margin: `${gutter / 2}px`, 36 | width: `calc(100% - ${gutter}px)` 37 | } 38 | }; 39 | }; 40 | 41 | export const buildCellTypeProperties = (alignSelf) => { 42 | return { 43 | top: { [alignSelf]: 'flex-start' }, 44 | middle: { [alignSelf]: 'center' }, 45 | bottom: { [alignSelf]: 'flex-end' }, 46 | stretch: { [alignSelf]: 'stretch' } 47 | }; 48 | }; 49 | 50 | export const buildSharedProperties = () => { 51 | return { 52 | nospace: { padding: 0, margin: 0 } 53 | }; 54 | }; 55 | 56 | export function hydrateReference({ options }) { 57 | const { columns, list } = options; 58 | const size = list.length; 59 | 60 | return list.reduce((acc, current, i) => { 61 | const { name, gutter, margin } = current; 62 | 63 | const { 64 | justifyContent, 65 | alignSelf, 66 | FIXED_ROW 67 | } = fixUserAgent(ROW_ROOT, isUAFixNeeded(navigator.userAgent)); 68 | 69 | // 4 70 | // 8 71 | // 12 72 | const localColumns = (columns / size) * (i + 1); 73 | 74 | // Define partial sizes for columnNumber < totalColumns. 75 | const partialWidth = 76 | calcPropWithGutter( 77 | [1, localColumns, gutter], 78 | 'width' 79 | ); 80 | 81 | // Define sizes = 100% for everything else. 82 | const fullWidth = 83 | calcPropWithGutter( 84 | [localColumns, columns + 1, gutter], 85 | 'width', 86 | true 87 | ); 88 | 89 | // Define offset sizes. 90 | const offset = 91 | calcPropWithGutter( 92 | [0, localColumns, gutter / 2], 93 | 'marginLeft' 94 | ); 95 | 96 | const row = buildRow(ROW_ID, FIXED_ROW, gutter, margin); 97 | const rowTypeProperties = buildRowTypeProperties(justifyContent); 98 | 99 | const cell = buildCell(CELL_ID, gutter); 100 | const cellTypeProperties = buildCellTypeProperties(alignSelf); 101 | 102 | const sharedProperties = buildSharedProperties(); 103 | 104 | return { 105 | ...acc, 106 | [name]: { 107 | ...row, 108 | ...rowTypeProperties, 109 | ...cell, 110 | ...cellTypeProperties, 111 | ...partialWidth, 112 | ...fullWidth, 113 | ...sharedProperties, 114 | offset: { ...offset } 115 | } 116 | }; 117 | }, {}); 118 | } 119 | 120 | export default function reference(state = {}) { 121 | return state; 122 | } 123 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers } from 'redux'; 2 | import media, { hydrateMedia } from './reducers/media'; 3 | import reference, { hydrateReference } from './reducers/reference'; 4 | 5 | export default function store(initialState) { 6 | return createStore( 7 | combineReducers({ media, reference }), 8 | { 9 | media: hydrateMedia(initialState.media), 10 | reference: hydrateReference(initialState.reference) 11 | } 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/cache.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /src/utils/calcPropWithGutter.js: -------------------------------------------------------------------------------- 1 | import range from 'lodash/utility/range'; 2 | 3 | export default function calcPropWithGutter([start, end, gutter], prop, isFull) { 4 | return range(start, end).reduce((acc, n) => { 5 | const width = (isFull) ? 100 : (n / end) * 100; 6 | acc[n] = { 7 | [prop]: `calc(${width}% - ${gutter}px)` 8 | }; 9 | return acc; 10 | }, {}); 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/capitalize.js: -------------------------------------------------------------------------------- 1 | export default function capitalize(string) { 2 | return string.charAt(0).toUpperCase() + string.slice(1); 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/fixUserAgent.js: -------------------------------------------------------------------------------- 1 | export default function fixUserAgent(rowRoot, needFix) { 2 | const justifyContent = 3 | needFix 4 | ? 'WebkitJustifyContent' 5 | : 'justifyContent'; 6 | 7 | const alignSelf = 8 | needFix 9 | ? 'WebkitAlignSelf' 10 | : 'alignSelf'; 11 | 12 | const FIXED_ROW = 13 | needFix 14 | ? { 15 | display: '-webkit-flex', 16 | WebkitFlexFlow: 'row wrap', 17 | WebkitAlignItems: 'stretch' 18 | } 19 | : rowRoot; 20 | 21 | return { 22 | justifyContent, 23 | alignSelf, 24 | FIXED_ROW 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/invariant.js: -------------------------------------------------------------------------------- 1 | export default function invariant(condition, error) { 2 | if (!condition) throw new Error(error); 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/isUAFixNeeded.js: -------------------------------------------------------------------------------- 1 | export default function isUAFixNeeded(userAgent) { 2 | return userAgent.indexOf('Chrome') < 0 3 | && userAgent.indexOf('Safari') > -1; 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/memoize.js: -------------------------------------------------------------------------------- 1 | import cache from './cache'; 2 | 3 | export default function memoize(callback) { 4 | return function getInMemory(key) { 5 | if (!cache.hasOwnProperty(key)) { 6 | cache[key] = callback.call(this, key); 7 | } 8 | return cache[key]; 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/pick.js: -------------------------------------------------------------------------------- 1 | import compact from 'lodash/array/compact'; 2 | import getIn from 'lodash/object/get'; 3 | import memoize from './memoize'; 4 | import invariant from './invariant'; 5 | import capitalize from './capitalize'; 6 | import { WHITE_LIST } from '../constants'; 7 | 8 | export const parser = (initial, input) => { 9 | if (input && input.trim()) { 10 | return [initial, ...input.trim().split(/\s+/)]; 11 | } 12 | return [initial]; 13 | }; 14 | 15 | export const listReducer = (name, list = []) => { 16 | return compact(list.map(n => { 17 | const [ entry, ...value ] = n.split('-'); 18 | 19 | switch (value.length) { 20 | case 0: 21 | return entry; 22 | case 1: 23 | if (entry === 'offset') { 24 | return [entry, ...value]; 25 | } 26 | if (entry !== name) return false; 27 | return value[0]; 28 | case 2: 29 | if (entry !== name) return false; 30 | if (value[0] === 'offset') { 31 | return value; 32 | } 33 | return false; 34 | default: 35 | return false; 36 | } 37 | })); 38 | }; 39 | 40 | export const generatePayload = ({ name }, list) => { 41 | return { 42 | name, 43 | list: listReducer(name, list) 44 | }; 45 | }; 46 | 47 | export const reducePayload = ({ name, list }, reference) => { 48 | return list.reduce((acc, current) => { 49 | const style = getIn(reference, [name, ...current]); 50 | return { 51 | ...acc, 52 | ...style 53 | }; 54 | }, {}); 55 | }; 56 | 57 | export const getInReference = (tag, { media, reference, is }) => { 58 | const list = parser(tag, is); 59 | const payload = generatePayload(media, list); 60 | 61 | if (process.env.NODE_ENV !== 'production') { 62 | payload.list.forEach(n => { 63 | const value = (Array.isArray(n)) ? n[0] : n; 64 | invariant( 65 | WHITE_LIST[tag].indexOf(value) > -1, 66 | `Property '${value}' is not allowed for <${capitalize(tag)}> component.` 67 | ); 68 | }); 69 | } 70 | 71 | return reducePayload(payload, reference); 72 | }; 73 | 74 | export const memoizeProcess = 75 | (...arg) => memoize(() => getInReference(...arg)); 76 | 77 | export const generateKey = 78 | (tag, { media: { name }, is }) => `${tag}${name}${is}`; 79 | 80 | export default function pick(...arg) { 81 | const key = generateKey(...arg); 82 | 83 | return memoizeProcess(...arg)(key); 84 | } 85 | -------------------------------------------------------------------------------- /test/components/Grid.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { 3 | ensureValue, 4 | ensureListProperties, 5 | build, 6 | setMedia, 7 | setReference 8 | } from '../../src/components/Grid'; 9 | import { MEDIA_MODEL_HELPER } from '../../src/constants'; 10 | 11 | function buildModelFrom(options) { 12 | const { list } = options; 13 | return { 14 | columns: list.length * 4, 15 | deaf: false, 16 | list: ensureListProperties(undefined, options, list) 17 | }; 18 | } 19 | 20 | describe('Components', () => { 21 | describe('Grid helpers', () => { 22 | describe('ensureValue(options, base, key, value)', () => { 23 | it('should return ensureValue -> value if valid and provided', () => { 24 | const v = ensureValue(undefined, { a: 12 }, 'a', 0); 25 | const expected = 0; 26 | expect(v).toEqual(expected); 27 | }); 28 | 29 | it('should return ensureValue -> options -> key if valid and provided and if ensureValue -> value is invalid or missing', () => { 30 | const v = ensureValue({ a: 0 }, { a: 12 }, 'a', undefined); 31 | const expected = 0; 32 | expect(v).toEqual(expected); 33 | }); 34 | 35 | it('should return ensureValue -> base -> key if ensureValue -> value and ensureValue -> options -> key are invalid or missing', () => { 36 | const v = ensureValue(undefined, { a: 12 }, 'a', undefined); 37 | const expected = 12; 38 | expect(v).toEqual(expected); 39 | }); 40 | 41 | it('should throw if ensureValue -> key is missing', () => { 42 | expect(() => { 43 | ensureValue(undefined, undefined, undefined); 44 | }).toThrow( 45 | `Property 'key' of ensureValue() must be defined.` 46 | ); 47 | }); 48 | 49 | it('should throw if ensureValue -> base is missing', () => { 50 | expect(() => { 51 | ensureValue(undefined, undefined); 52 | }).toThrow( 53 | `Property 'base' of ensureValue() must be defined.` 54 | ); 55 | }); 56 | 57 | it('should throw if ensureValue -> key is not defined in ensureValue -> base -> key', () => { 58 | const key = 'a'; 59 | expect(() => { 60 | ensureValue(undefined, { [key]: undefined }, key); 61 | }).toThrow( 62 | `Property '${key}' of ensureValue() -> base must be defined.` 63 | ); 64 | }); 65 | }); 66 | 67 | describe('ensureListProperties(options, base, list)', () => { 68 | it('should return a new Model -> list from ensureListProperties -> list', () => { 69 | const list = [ { name: 'a', query: 'b' } ]; 70 | const v = ensureListProperties(undefined, MEDIA_MODEL_HELPER, list); 71 | const expected = [{ 72 | name: 'a', 73 | query: 'b', 74 | gutter: MEDIA_MODEL_HELPER.gutter, 75 | margin: MEDIA_MODEL_HELPER.margin 76 | }]; 77 | expect(v).toEqual(expected); 78 | }); 79 | }); 80 | 81 | describe('build(options, base)', () => { 82 | it('should handle missing build -> options properties', () => { 83 | const v = build({}, MEDIA_MODEL_HELPER); 84 | const expected = buildModelFrom(MEDIA_MODEL_HELPER); 85 | expect(v).toEqual(expected); 86 | }); 87 | 88 | it('should throw if build -> options -> list is defined and empty', () => { 89 | expect(() => { 90 | build({ list: [] }, MEDIA_MODEL_HELPER); 91 | }).toThrow( 92 | `Property 'list' of -> 'options' can not be empty` 93 | ); 94 | }); 95 | 96 | it('should throw if build -> options -> columns is defined and wrong', () => { 97 | expect(() => { 98 | build({ columns: '11', list: ['a', 'b', 'c'] }, MEDIA_MODEL_HELPER); 99 | }).toThrow( 100 | `Property 'columns' of -> 'options' must be a multiple of ` + 101 | ` -> 'options' -> 'list' -> length.` 102 | ); 103 | }); 104 | }); 105 | 106 | describe('setMedia(name)', () => { 107 | it('should return a valid object', () => { 108 | const v = setMedia('a'); 109 | const expected = { name: 'a' }; 110 | expect(v).toEqual(expected); 111 | }); 112 | }); 113 | 114 | describe('setReference(options)', () => { 115 | it('should return a valid object', () => { 116 | const v = setReference('a'); 117 | const expected = { options: 'a' }; 118 | expect(v).toEqual(expected); 119 | }); 120 | }); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /test/reducers/media.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import media, { updateMediaName, hydrateMedia } from '../../src/reducers/media'; 3 | import { UPDATE_MEDIA_NAME } from '../../src/constants'; 4 | 5 | describe('Reducers', () => { 6 | describe('media helpers', () => { 7 | it('should hydrateMedia', () => { 8 | const v = { name: 'a' }; 9 | const expected = { name: 'a' }; 10 | expect(hydrateMedia(v)).toEqual(expected); 11 | }); 12 | 13 | it('should generate an action to update media name', () => { 14 | const v = 'a'; 15 | const expected = { 16 | type: UPDATE_MEDIA_NAME, 17 | payload: v 18 | }; 19 | expect(updateMediaName(v)).toEqual(expected); 20 | }); 21 | }); 22 | 23 | describe('media', () => { 24 | it('should return the initial state', () => { 25 | expect( 26 | media(undefined, undefined) 27 | ).toEqual({}); 28 | }); 29 | 30 | it('should handle UPDATE_MEDIA_NAME', () => { 31 | expect( 32 | media({ name: 'a' }, { 33 | type: UPDATE_MEDIA_NAME, 34 | payload: 'b' 35 | }) 36 | ).toEqual({ name: 'b' }); 37 | }); 38 | 39 | it('should handle wrong action.type', () => { 40 | expect( 41 | media({ name: 'a' }, { 42 | type: 'fail', 43 | payload: 'b' 44 | }) 45 | ).toEqual({ name: 'a' }); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/reducers/reference.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import reference, { 3 | buildRow, 4 | buildRowTypeProperties, 5 | buildCell, 6 | buildCellTypeProperties, 7 | sharedProperties 8 | } from '../../src/reducers/reference'; 9 | 10 | describe('Reducers', () => { 11 | describe('reference helpers', () => { 12 | describe('buildRow(ROW_ID, FIXED_ROW, gutter, margin)', () => { 13 | it('should build a new row properties object', () => { 14 | const v = ['a', { b: 'B' }, 8, 16]; 15 | const expected = { 16 | a: { 17 | b: 'B', 18 | padding: `${16 - (8 / 2)}px` 19 | } 20 | }; 21 | expect(buildRow(...v)).toEqual(expected); 22 | }); 23 | }); 24 | 25 | describe('buildRowTypeProperties(justifyContent)', () => { 26 | it('should build new row type properties objects', () => { 27 | const v = ['a']; 28 | const expected = { 29 | start: { a: 'flex-start' }, 30 | center: { a: 'center' }, 31 | end: { a: 'flex-end' }, 32 | around: { a: 'space-around' }, 33 | between: { a: 'space-between' } 34 | }; 35 | expect(buildRowTypeProperties(...v)).toEqual(expected); 36 | }); 37 | }); 38 | 39 | describe('buildCell(id, gutter)', () => { 40 | it('should build a new cell properties object', () => { 41 | const v = ['a', 8]; 42 | const expected = { 43 | a: { 44 | boxSizing: 'border-box', 45 | margin: `${8 / 2}px`, 46 | width: `calc(100% - ${8}px)` 47 | } 48 | }; 49 | expect(buildCell(...v)).toEqual(expected); 50 | }); 51 | }); 52 | 53 | describe('buildCellTypeProperties(alignSelf)', () => { 54 | it('should build new cell type properties objects', () => { 55 | const v = ['a']; 56 | const expected = { 57 | top: { a: 'flex-start' }, 58 | middle: { a: 'center' }, 59 | bottom: { a: 'flex-end' }, 60 | stretch: { a: 'stretch' } 61 | }; 62 | expect(buildCellTypeProperties(...v)).toEqual(expected); 63 | }); 64 | }); 65 | 66 | describe('sharedProperties()', () => { 67 | it('should build a new shared properties object', () => { 68 | const expected = { 69 | nospace: { padding: 0, margin: 0 } 70 | }; 71 | expect(sharedProperties()).toEqual(expected); 72 | }); 73 | }); 74 | }); 75 | 76 | describe('reference', () => { 77 | it('should return the initial state', () => { 78 | expect( 79 | reference(undefined, undefined) 80 | ).toEqual({}); 81 | }); 82 | 83 | it('should handle wrong action.type', () => { 84 | expect( 85 | reference({ name: 'a' }, { 86 | type: 'fail', 87 | payload: 'b' 88 | }) 89 | ).toEqual({ name: 'a' }); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /test/utils/cache.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import cache from '../../src/utils/cache'; 3 | 4 | describe('Utils', () => { 5 | describe('cache', () => { 6 | it(`should be an empty object`, () => { 7 | expect(cache).toEqual({}); 8 | }); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/utils/calcPropWithGutter.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import calcPropWithGutter from '../../src/utils/calcPropWithGutter'; 3 | 4 | describe('Utils', () => { 5 | describe('calcPropWithGutter([start, end, gutter], prop, isFull)', () => { 6 | it(`should generate partial style property if calcPropWithGutter -> isFull is missing or not true`, () => { 7 | const v = calcPropWithGutter([1, 2, 20], 'a'); 8 | expect(v).toEqual({ '1': { a: 'calc(50% - 20px)' } }); 9 | }); 10 | 11 | it(`should generate full style property if calcPropWithGutter -> isFull is true`, () => { 12 | const v = calcPropWithGutter([1, 2, 20], 'a', true); 13 | expect(v).toEqual({ '1': { a: 'calc(100% - 20px)' } }); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/utils/capitalize.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import capitalize from '../../src/utils/capitalize'; 3 | 4 | describe('Utils', () => { 5 | describe('capitalize(string)', () => { 6 | it(`should capitalize capitalize -> string`, () => { 7 | const v = capitalize('abc def'); 8 | expect(v).toEqual('Abc def'); 9 | }); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/utils/fixUserAgent.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import fixUserAgent from '../../src/utils/fixUserAgent'; 3 | 4 | const base = { 5 | alignSelf: 'alignSelf', 6 | justifyContent: 'justifyContent', 7 | row: { 8 | display: 'flex', 9 | flexFlow: 'row wrap', 10 | alignItems: 'stretch' 11 | } 12 | }; 13 | 14 | const resultNotFixed = { 15 | alignSelf: 'alignSelf', 16 | justifyContent: 'justifyContent', 17 | FIXED_ROW: { 18 | display: 'flex', 19 | flexFlow: 'row wrap', 20 | alignItems: 'stretch' 21 | } 22 | }; 23 | 24 | const resultFixed = { 25 | alignSelf: 'WebkitAlignSelf', 26 | justifyContent: 'WebkitJustifyContent', 27 | FIXED_ROW: { 28 | display: '-webkit-flex', 29 | WebkitFlexFlow: 'row wrap', 30 | WebkitAlignItems: 'stretch' 31 | } 32 | }; 33 | 34 | describe('Utils', () => { 35 | describe('fixUserAgent(rowRoot, needFix)', () => { 36 | it(`should not fix if fixUserAgent -> needFix is missing or not true`, () => { 37 | const v = fixUserAgent(base.row, false); 38 | expect(v).toEqual(resultNotFixed); 39 | }); 40 | 41 | it(`should fix if fixUserAgent -> needFix is true`, () => { 42 | const v = fixUserAgent(base.row, true); 43 | expect(v).toEqual(resultFixed); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/utils/invariant.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import invariant from '../../src/utils/invariant'; 3 | 4 | describe('Utils', () => { 5 | describe('invariant(condition, error)', () => { 6 | it(`should throw invariant -> error if !invariant -> condition`, () => { 7 | expect(() => { 8 | invariant(false, 'error text'); 9 | }).toThrow('error text'); 10 | }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/utils/isUAFixNeeded.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import isUAFixNeeded from '../../src/utils/isUAFixNeeded'; 3 | 4 | describe('Utils', () => { 5 | describe('isUAFixNeeded(userAgent)', () => { 6 | it(`not needed`, () => { 7 | const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36'; 8 | const v = isUAFixNeeded(UA); 9 | expect(v).toBe(false); 10 | }); 11 | 12 | it(`needed`, () => { 13 | const UA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4'; 14 | const v = isUAFixNeeded(UA); 15 | expect(v).toBe(true); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/utils/pick.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { 3 | parser, 4 | listReducer, 5 | generatePayload, 6 | reducePayload, 7 | generateKey 8 | } from '../../src/utils/pick'; 9 | 10 | describe('Utils', () => { 11 | describe('pick helpers', () => { 12 | describe('parser(initial, input)', () => { 13 | it('should return an array of parser -> initial if parser -> input is missing or invalid', () => { 14 | const v = parser('a'); 15 | expect(v).toEqual(['a']); 16 | 17 | const v2 = parser('a', ' '); 18 | expect(v2).toEqual(['a']); 19 | }); 20 | 21 | it('should handle parser -> input patterns', () => { 22 | const v1 = parser('a', 'prop-1'); 23 | expect(v1).toEqual(['a', 'prop-1']); 24 | 25 | const v2 = parser('a', 'prop-1 prop-2'); 26 | expect(v2).toEqual(['a', 'prop-1', 'prop-2']); 27 | 28 | const v3 = parser('a', ' prop-1 prop-2 '); 29 | expect(v3).toEqual(['a', 'prop-1', 'prop-2']); 30 | }); 31 | }); 32 | 33 | describe('listReducer(name, list = [])', () => { 34 | it('should handle empty listReducer -> list', () => { 35 | const v = listReducer(); 36 | const expected = []; 37 | expect(v).toEqual(expected); 38 | }); 39 | 40 | it('should handle global listReducer -> list -> value', () => { 41 | const v = listReducer(undefined, ['a', 'b', 'c']); 42 | const expected = ['a', 'b', 'c']; 43 | expect(v).toEqual(expected); 44 | }); 45 | 46 | it('should handle global named listReducer -> list -> value', () => { 47 | const v = listReducer(undefined, ['a', 'b', 'offset-c']); 48 | const expected = ['a', 'b', ['offset', 'c']]; 49 | expect(v).toEqual(expected); 50 | }); 51 | 52 | it('should ignore non matching listReducer -> list -> value', () => { 53 | const v = listReducer('phone', ['a', 'b', 'fail-c']); 54 | const expected = ['a', 'b']; 55 | expect(v).toEqual(expected); 56 | }); 57 | 58 | it('should keep matching listReducer -> list -> value', () => { 59 | const v = listReducer('phone', ['a', 'b', 'phone-c']); 60 | const expected = ['a', 'b', 'c']; 61 | expect(v).toEqual(expected); 62 | }); 63 | 64 | it('should ignore non matching named listReducer -> list -> value', () => { 65 | const v = listReducer('phone', ['a', 'b', 'fail-offset-c']); 66 | const expected = ['a', 'b']; 67 | expect(v).toEqual(expected); 68 | }); 69 | 70 | it('should keep matching named listReducer -> list -> value', () => { 71 | const v = listReducer('phone', ['a', 'b', 'phone-offset-c']); 72 | const expected = ['a', 'b', ['offset', 'c']]; 73 | expect(v).toEqual(expected); 74 | }); 75 | 76 | it('should ignore matching invalid named listReducer -> list -> value', () => { 77 | const v = listReducer('phone', ['a', 'b', 'phone-fail-c']); 78 | const expected = ['a', 'b']; 79 | expect(v).toEqual(expected); 80 | }); 81 | 82 | it('should ignore other invalid listReducer -> list -> value', () => { 83 | const v = listReducer('phone', ['a', 'b', 'phone-offset-c-fail']); 84 | const expected = ['a', 'b']; 85 | expect(v).toEqual(expected); 86 | }); 87 | }); 88 | 89 | describe('generatePayload({ name }, list)', () => { 90 | it('should keep generatePayload -> name and parse generatePayload -> list', () => { 91 | const v = generatePayload({ name: 'a' }); 92 | const expected = { 93 | name: 'a', 94 | list: [] 95 | }; 96 | expect(v).toEqual(expected); 97 | }); 98 | }); 99 | 100 | describe('reducePayload({ name, list }, reference)', () => { 101 | it('should keep generatePayload -> name and parse generatePayload -> list', () => { 102 | const reference = { 103 | phone: { 104 | a: { propA: 'A' }, 105 | b: { propB: 'B' }, 106 | c: { inner: { propC: 'C' } } 107 | } 108 | }; 109 | const payload = { 110 | name: 'phone', 111 | list: ['a', 'b', ['c', 'inner']] 112 | }; 113 | const v = reducePayload(payload, reference); 114 | const expected = { 115 | propA: 'A', 116 | propB: 'B', 117 | propC: 'C' 118 | }; 119 | expect(v).toEqual(expected); 120 | }); 121 | }); 122 | 123 | describe('generateKey(tag, { media: { name }, is })', () => { 124 | it('should generate a key', () => { 125 | const v = generateKey('a', { media: { name: 'b' }, is: 'c' }); 126 | const expected = 'abc'; 127 | expect(v).toEqual(expected); 128 | }); 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /webpack.config.base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var path = require('path'); 5 | var webpack = require('webpack'); 6 | 7 | var reactExternal = { 8 | root: 'React', 9 | commonjs2: 'react', 10 | commonjs: 'react', 11 | amd: 'react' 12 | }; 13 | 14 | var reduxExternal = { 15 | root: 'Redux', 16 | commonjs2: 'redux', 17 | commonjs: 'redux', 18 | amd: 'redux' 19 | }; 20 | 21 | module.exports = { 22 | externals: { 23 | 'react': reactExternal, 24 | 'redux': reduxExternal 25 | }, 26 | module: { 27 | loaders: [ 28 | { 29 | test: /\.js$/, 30 | loaders: ['babel-loader'], 31 | include: [ 32 | path.resolve(__dirname, 'src'), 33 | path.resolve(__dirname, 'node_modules', 'lodash') 34 | ] 35 | } 36 | ] 37 | }, 38 | node: { 39 | process: false 40 | }, 41 | output: { 42 | library: 'ReactInlineGrid', 43 | libraryTarget: 'umd' 44 | }, 45 | plugins: [ 46 | new webpack.optimize.OccurenceOrderPlugin() 47 | ] 48 | }; 49 | -------------------------------------------------------------------------------- /webpack.config.development.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var webpack = require('webpack'); 5 | var baseConfig = require('./webpack.config.base'); 6 | 7 | module.exports = _.merge({}, baseConfig, { 8 | plugins: _.union(baseConfig.plugins, [ 9 | new webpack.DefinePlugin({ 10 | '__DEV__': 'true', 11 | 'process.env.NODE_ENV': '"development"' 12 | }) 13 | ]) 14 | }); 15 | -------------------------------------------------------------------------------- /webpack.config.production.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var webpack = require('webpack'); 5 | var baseConfig = require('./webpack.config.base'); 6 | 7 | module.exports = _.merge({}, baseConfig, { 8 | plugins: _.union(baseConfig.plugins, [ 9 | new webpack.DefinePlugin({ 10 | '__DEV__': 'false', 11 | 'process.env.NODE_ENV': '"production"' 12 | }), 13 | new webpack.optimize.UglifyJsPlugin({ 14 | compressor: { 15 | pure_getters: true, 16 | unsafe: true, 17 | unsafe_comps: true, 18 | screw_ie8: true, 19 | warnings: false 20 | } 21 | }) 22 | ]) 23 | }); 24 | --------------------------------------------------------------------------------