├── .babelrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── lib └── index.js ├── package.json ├── src └── index.js └── test └── index.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules 4 | bower_components 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .babelrc 3 | src 4 | test 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Maximiliano Guzenski 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 Easy Style 2 | 3 | A tiny library to easy apply css/styles to react components with **zero references** to your css classes or to inline-styles. 4 | 5 | ## Install 6 | 7 | `npm install react-easy-style` 8 | 9 | you'll need also: 10 | `webpack`, `css-loader`, `react` 11 | 12 | ## Why? 13 | The React community is highly fragmented when it comes to styling. Right now, there is more then 16 project for inline css on the github, all of they are trying to fix some "issue" that css has, like global scope... but, I think, they all are creating a lot of new one as well. 14 | 15 | **React Easy Style** "borrow" ideas from one of those projects: react-css (1). And join it with webpack css-loader (2) to make a very light and useful library to easy use classes and styles on react components. 16 | 17 | 1. [react-css](http://reactcss.com/): seems to be the only one to have noticed that style and react props/state are strong linked to each other. 18 | 19 | 2. [webpack css-loader](https://github.com/webpack/css-loader): with support to [CSS Module spec](https://github.com/css-modules/css-modules) that fix css global scope issue. 20 | 21 | 22 | ## How? 23 | 24 | Take a look! 25 | 26 | ```javascript 27 | // button.jsx 28 | 29 | import React from 'react' 30 | import EasyStyle from 'react-easy-style' 31 | 32 | // import css/scss/less with webpack css-loader 33 | // webpack.config sample loaders: [{test: /\.scss$/, loader: "style!css!sass" }] 34 | import css from './button.scss' 35 | 36 | @EasyStyle( css ) 37 | export default class Button extends React.Component { 38 | static defaultProps = { 39 | kind: 'default', circle: false 40 | } 41 | 42 | render() { 43 | return 44 | } 45 | } 46 | 47 | // if you don't use decorators: 48 | // class Button extends React.component { ... } 49 | // export default EasyStyle(css)(Button) 50 | ``` 51 | ```sass 52 | // button.scss 53 | 54 | :local .Button { 55 | border: 1px solid transparent; 56 | // a lot of other styles 57 | 58 | // browser state rules 59 | &:focus { outline: none; } 60 | 61 | // React props/state rules 62 | // pattern: className--propsKey-propsValue 63 | 64 | &--circle-true { border-radius: 50% } 65 | 66 | &--kind-default { /*...*/ } 67 | &--kind-primary { /*...*/ } 68 | &--kind-success { /*...*/ } 69 | } 70 | ``` 71 | And that is it. Seriously! For small/medium components you will end up with 0 (zero) css references on you code. 72 | 73 | Here a example of how to call it and its output: 74 | 75 | ```javascript 76 | // when you call 77 | 81 | 82 | // ps.: On real world, css-modules will change classes names 83 | // to make they unique (no more global namespace!), something like: 84 | 85 | 86 | ``` 87 | For more complex components, please, keep reading ;-) 88 | 89 | 90 | ## Other examples 91 | 92 | #### When you need pass internal and external classNames... 93 | ```javascript 94 | class Button extends React.Component { 95 | render() { 96 | return ( 97 | 98 | ) 99 | } 100 | } 101 | 102 | // call 103 | 107 | ``` 108 | 109 | #### You can make references to a nested class (using 'is' attribute) 110 | ```javascript 111 | class Button extends React.Component { 112 | render() { 113 | return ( 114 | 118 | ) 119 | } 120 | } 121 | 122 | // call 123 | 132 | 133 | ``` 134 | ```scss 135 | // button.scss 136 | 137 | :local .Button { 138 | .label { color: #000 } 139 | .desc { font-size: 85% } 140 | 141 | // and into your states... 142 | .&--kind-primary .label {} 143 | .&--kind-primary .desc {} 144 | } 145 | 146 | ``` 147 | 148 | #### Classes and styles defined on nested element will be merged as well 149 | ```javascript 150 | class Button extends React.Component { 151 | render() { 152 | return ( 153 | 157 | ) 158 | } 159 | } 160 | 161 | // call 162 | 169 | ``` 170 | 171 | 172 | #### Your root and nested elements can receive classes and styles from outside 173 | 174 | To nested elements that use, for example, is='label' you'll have labelClasses='class1' and labelStyle={{...}}. 175 | for top-level element (root) you have rootClasses, rootStyle, className and style. 176 | In another word: Themeable for free. 177 | 178 | ```javascript 179 | class Button extends React.Component { 180 | render() { 181 | return ( 182 | 186 | ) 187 | } 188 | } 189 | 190 | // call 191 | // ps.: for root you can use className/style or rootClasses/rootStyle or both ;) 192 | // all styles will be merged 193 | 206 | ``` 207 | 208 | #### If your top-level element is not your root element, use is='root' 209 | ```javascript 210 | class Button extends React.Component { 211 | render() { 212 | return ( 213 | 214 | ) 215 | } 216 | } 217 | 218 | // call 219 | 224 | 225 | ``` 226 | 227 | #### If you want/have to change top-level class name 228 | 229 | By default Easy Style will try to find a class with same name of component, or one called 'root'. 230 | But you can pass a new one. 231 | 232 | ```javascript 233 | // grid.jsx 234 | @EasyStyle(css, 'myContainer') 235 | class Container extends React.Component {} 236 | ``` 237 | ```scss 238 | // grid.scss 239 | // easy style will find classes in this order (and use the first found): 240 | 241 | :local .myContainer { /** ... **/ } 242 | :local .Container { /** ... **/ } 243 | :local .root { /** ... **/ } 244 | ``` 245 | 246 | 247 | ## All this is pretty cool... but I want to use inline styles. 248 | 249 | Ok, React Easy Style has support to inline styles BUT without fancy feature like browser state or media queries. 250 | Let's see: 251 | 252 | ```javascript 253 | 254 | const style = { 255 | base: { 256 | root: { padding: 2, /**...**/ }, 257 | label: { /**...**/ }, 258 | desc: { /**...**/ } 259 | }, 260 | 'kind-primary': { 261 | root: { /**...**/ }, 262 | label: { /**...**/ }, 263 | desc: { /**...**/ } 264 | }, 265 | 'circle-true': { 266 | root: { /**...**/ }, 267 | label: { /**...**/ }, 268 | desc: { /**...**/ } 269 | } 270 | } 271 | 272 | @EasyStyle( style ) 273 | export default class Button extends React.Component { 274 | static defaultProps = { 275 | kind: 'primary' 276 | } 277 | 278 | render() { 279 | return ( 280 | 284 | ) 285 | } 286 | } 287 | 288 | // and it just works. 289 | // and you can use rootClasses, rootStyle, labelStyle, labelClasses, etc... 290 | ``` 291 | 292 | ## What next? 293 | * ~~Implement tests ;)~~ 294 | * Benchmark performance (it's very fast, but maybe it can be more) 295 | * Support themes, maybe using react context 296 | 297 | ## Finally 298 | 299 | **If you like it, please help!!** 300 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | 7 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 8 | 9 | var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; 10 | 11 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 12 | 13 | exports['default'] = EasyStyle; 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 16 | 17 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 18 | 19 | function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 20 | 21 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 22 | 23 | var _react = require('react'); 24 | 25 | var _react2 = _interopRequireDefault(_react); 26 | 27 | var _reactAddonsCloneWithProps = require('react-addons-clone-with-props'); 28 | 29 | var _reactAddonsCloneWithProps2 = _interopRequireDefault(_reactAddonsCloneWithProps); 30 | 31 | var _classnames = require('classnames'); 32 | 33 | var _classnames2 = _interopRequireDefault(_classnames); 34 | 35 | // 36 | // 37 | function isPlain(value) { 38 | var type = typeof value; 39 | return type === 'number' || type === 'string' || type === 'boolean'; 40 | } 41 | 42 | // 43 | // 44 | function isClassNames(styleOrClass) { 45 | var firstKey = Object.keys(styleOrClass)[0]; 46 | return typeof styleOrClass[firstKey] !== 'object'; 47 | } 48 | 49 | // 50 | // 51 | function getDisplayName(Component) { 52 | return Component.displayName || Component.name; 53 | } 54 | 55 | // 56 | // 57 | function tranverse(node, level, getClasesAndStyles) { 58 | if (level === undefined) level = 0; 59 | 60 | if (!node || !node.props || !_react2['default'].isValidElement(node)) { 61 | return [false, node]; 62 | } 63 | 64 | var children = node.props.children; 65 | 66 | var newChildren = null; 67 | var anyChildChanged = false; 68 | var rootChildAppear = false; 69 | 70 | if (children) { 71 | newChildren = _react2['default'].Children.map(children, function (child) { 72 | var tChild = tranverse(child, level + 1, getClasesAndStyles); 73 | anyChildChanged = anyChildChanged || tChild[1] !== child; 74 | rootChildAppear = rootChildAppear || tChild[0]; 75 | return tChild[1]; 76 | }); 77 | } 78 | 79 | if (rootChildAppear) { 80 | return [true, _react2['default'].cloneElement(node, {}, newChildren)]; 81 | } 82 | 83 | if (!level || node.props.is) { 84 | var isNodeRoot = !level || node.props.is === 'root'; 85 | var _p = getClasesAndStyles(node, isNodeRoot); 86 | 87 | return [isNodeRoot, _react2['default'].cloneElement(node, _extends({ is: null }, _p), newChildren)]; 88 | } 89 | 90 | if (anyChildChanged) { 91 | return [false, _react2['default'].cloneElement(node, {}, newChildren)]; 92 | } 93 | 94 | return [false, node]; 95 | } 96 | 97 | function EasyStyle(styleOrClass, _rootName) { 98 | if (styleOrClass === undefined) styleOrClass = {}; 99 | 100 | var isClass = isClassNames(styleOrClass); 101 | 102 | return function (DecoredComponent) { 103 | var dispName = getDisplayName(DecoredComponent); 104 | 105 | var rootName = !isClass && 'root' || styleOrClass && styleOrClass[_rootName] && _rootName || styleOrClass && styleOrClass[dispName] && dispName || 'root'; 106 | 107 | var getClassesAndStyles = function getClassesAndStyles(node, isRoot) { 108 | var is = node.props.is; 109 | var isName = isRoot ? rootName : is; 110 | var propsKlzz = []; 111 | 112 | var getStyleProp = function getStyleProp(k, v) { 113 | if (isClass) { 114 | return styleOrClass[v && isPlain(v) ? rootName + '--' + k + '-' + v : rootName + '--' + k + '-false']; 115 | } else { 116 | return styleOrClass[k + '-' + v] ? styleOrClass[k + '-' + v][isName] : null; 117 | } 118 | }; 119 | 120 | if (isRoot || !isClass) { 121 | var allProps = _extends({}, this.state, this.props); 122 | 123 | for (var k in allProps) { 124 | var sp = getStyleProp(k, allProps[k]); 125 | sp && propsKlzz.push(sp); 126 | } 127 | } 128 | 129 | var className = (0, _classnames2['default'])(isClass ? styleOrClass[isName] : null, isClass ? propsKlzz : null, node.props.className, this.props[(isRoot ? 'root' : is) + 'Classes'], _defineProperty({}, this.props.className, isRoot && this.props.className)); 130 | 131 | if (className.trim) className = className.trim(); 132 | if (className === '') className = null; 133 | 134 | // 135 | // ok, let see styles now 136 | // 137 | 138 | var propsIfStyle = isClass ? {} : _extends({}, styleOrClass['base'] && styleOrClass['base'][isName], propsKlzz.reduce(function (m, i) { 139 | return _extends({}, m, i); 140 | }, {})); 141 | 142 | var style = _extends({}, propsIfStyle, node.props.style, this.props[(isRoot ? 'root' : is) + 'Style'], isRoot && this.props.style); 143 | 144 | return { className: className, style: style }; 145 | }; 146 | 147 | return (function (_DecoredComponent) { 148 | _inherits(Component, _DecoredComponent); 149 | 150 | function Component() { 151 | _classCallCheck(this, Component); 152 | 153 | _get(Object.getPrototypeOf(Component.prototype), 'constructor', this).apply(this, arguments); 154 | } 155 | 156 | _createClass(Component, [{ 157 | key: 'render', 158 | value: function render() { 159 | var elem = _get(Object.getPrototypeOf(Component.prototype), 'render', this).call(this); 160 | return tranverse(elem, 0, getClassesAndStyles.bind(this))[1]; 161 | } 162 | }]); 163 | 164 | return Component; 165 | })(DecoredComponent); 166 | }; 167 | } 168 | 169 | module.exports = exports['default']; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-easy-style", 3 | "version": "2.0.0", 4 | "description": "A tiny library to easy apply css/styles to react components", 5 | "main": "lib/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/maxguzenski/react-easy-style.git" 9 | }, 10 | "keywords": [ 11 | "react", 12 | "reactjs", 13 | "css", 14 | "inline", 15 | "style", 16 | "styles" 17 | ], 18 | "author": "Maximiliano Guzenski (http://github.com/maxguzenski)", 19 | "homepage": "https://github.com/maxguzenski/react-easy-style", 20 | "bugs": "https://github.com/maxguzenski/react-easy-style/issues", 21 | "scripts": { 22 | "build": "babel src/ -d lib/", 23 | "test": "mocha --compilers js:babel/register --recursive", 24 | "test:watch": "mocha --compilers js:babel/register --recursive --watch", 25 | "prepublish": "rm -Rf lib/ && npm run build" 26 | }, 27 | "license": "MIT", 28 | "dependencies": { 29 | "classnames": "~2.1.3", 30 | "react-addons-clone-with-props": "^0.14.3" 31 | }, 32 | "devDependencies": { 33 | "babel": "^5.5.8", 34 | "babel-core": "5.8.22", 35 | "babel-loader": "^5.1.4", 36 | "expect": "latest", 37 | "jsdom": "^3.1.2", 38 | "mocha": "latest", 39 | "mocha-jsdom": "^1.0.0", 40 | "react": "^0.14.0", 41 | "react-addons-test-utils": "^0.14.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import cloneWithProps from 'react-addons-clone-with-props' 4 | import cx from 'classnames' 5 | 6 | 7 | // 8 | // 9 | function isPlain(value) { 10 | const type = typeof value 11 | return type === 'number' || type === 'string' || type === 'boolean' 12 | } 13 | 14 | // 15 | // 16 | function isClassNames(styleOrClass) { 17 | const firstKey = Object.keys(styleOrClass)[0] 18 | return typeof styleOrClass[firstKey] !== 'object' 19 | } 20 | 21 | // 22 | // 23 | function getDisplayName(Component) { 24 | return Component.displayName || Component.name 25 | } 26 | 27 | // 28 | // 29 | function tranverse(node, level=0, getClasesAndStyles) { 30 | if (!node || !node.props || !React.isValidElement(node)) { 31 | return [false, node] 32 | } 33 | 34 | const { children } = node.props 35 | 36 | let newChildren = null 37 | let anyChildChanged = false 38 | let rootChildAppear = false 39 | 40 | if (children) { 41 | newChildren = React.Children.map(children, child => { 42 | const tChild = tranverse(child, level + 1, getClasesAndStyles) 43 | anyChildChanged = anyChildChanged || tChild[1] !== child 44 | rootChildAppear = rootChildAppear || tChild[0] 45 | return tChild[1] 46 | }) 47 | } 48 | 49 | if (rootChildAppear) { 50 | return [ 51 | true, 52 | React.cloneElement(node, {}, newChildren) 53 | ] 54 | } 55 | 56 | if (!level || node.props.is) { 57 | const isNodeRoot = !level || node.props.is === 'root' 58 | const _p = getClasesAndStyles(node, isNodeRoot) 59 | 60 | return [ 61 | isNodeRoot, 62 | React.cloneElement(node, {is: null, ..._p}, newChildren) 63 | ] 64 | } 65 | 66 | if (anyChildChanged) { 67 | return [ 68 | false, 69 | React.cloneElement(node, {}, newChildren) 70 | ] 71 | } 72 | 73 | return [ false, node ] 74 | } 75 | 76 | 77 | export default function EasyStyle(styleOrClass={}, _rootName) { 78 | const isClass = isClassNames(styleOrClass) 79 | 80 | return DecoredComponent => { 81 | const dispName = getDisplayName(DecoredComponent) 82 | 83 | const rootName = (!isClass && 'root') || 84 | (styleOrClass && styleOrClass[_rootName] && _rootName) || 85 | (styleOrClass && styleOrClass[dispName] && dispName) || 86 | 'root' 87 | 88 | const getClassesAndStyles = function(node, isRoot) { 89 | const is = node.props.is 90 | const isName = isRoot ? rootName : is 91 | const propsKlzz = [] 92 | 93 | const getStyleProp = (k, v) => { 94 | if (isClass) { 95 | return styleOrClass[v && isPlain(v) ? `${rootName}--${k}-${v}` : `${rootName}--${k}-false`] 96 | } else { 97 | return styleOrClass[`${k}-${v}`] ? styleOrClass[`${k}-${v}`][isName] : null 98 | } 99 | } 100 | 101 | if (isRoot || !isClass) { 102 | const allProps = {...this.state, ...this.props} 103 | 104 | for (let k in allProps) { 105 | const sp = getStyleProp(k, allProps[k]) 106 | sp && propsKlzz.push(sp) 107 | } 108 | } 109 | 110 | let className = cx( 111 | isClass ? styleOrClass[isName] : null, 112 | isClass ? propsKlzz : null, 113 | node.props.className, 114 | this.props[(isRoot ? 'root' : is)+'Classes'], 115 | {[this.props.className]: isRoot && this.props.className} 116 | ) 117 | 118 | if (className.trim) className = className.trim() 119 | if (className === '') className = null 120 | 121 | // 122 | // ok, let see styles now 123 | // 124 | 125 | const propsIfStyle = isClass ? {} : { 126 | ...( styleOrClass['base'] && styleOrClass['base'][isName] ), 127 | ...( propsKlzz.reduce((m, i) => { return {...m, ...i}}, {}) ) 128 | } 129 | 130 | const style = { 131 | ...propsIfStyle, 132 | ...( node.props.style ), 133 | ...( this.props[(isRoot ? 'root' : is)+'Style'] ), 134 | ...( isRoot && this.props.style ) 135 | } 136 | 137 | return { className, style } 138 | } 139 | 140 | return class Component extends DecoredComponent { 141 | render() { 142 | const elem = super.render() 143 | return tranverse(elem, 0, getClassesAndStyles.bind(this))[1] 144 | } 145 | } 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | 2 | import jsdom from 'mocha-jsdom' 3 | import expect from 'expect' 4 | import EasyStyle from '../src/index' 5 | import React, { Component } from 'react' 6 | import TestUtils from 'react-addons-test-utils' 7 | jsdom() 8 | 9 | describe('index', () => { 10 | 11 | describe('support to null styles', () => { 12 | const Button = EasyStyle()( 13 | class Comp extends Component { 14 | render() { return } 15 | } 16 | ) 17 | 18 | it('should work', () => { 19 | const { btn } = TestUtils.renderIntoDocument(} 73 | } 74 | ) 75 | 76 | it('should has root class', () => { 77 | const node = TestUtils.renderIntoDocument(} 116 | } 117 | ) 118 | 119 | it('should has label class', () => { 120 | const node = TestUtils.renderIntoDocument( } 149 | }) 150 | 151 | it('should has default style', () => { 152 | const node = TestUtils.renderIntoDocument(