├── .gitignore ├── README.md ├── StyleBuilder.js ├── __tests__ └── StyleBuilder.js ├── dist └── StyleBuilder.js ├── gulpfile.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Style Builder 2 | 3 | A JavaScript utility to break apart shorthand CSS components in objects. 4 | 5 | When using inline styles with React components, it can be dangerous to use shorthand CSS properties. As a work around, use StyleBuilder to break apart any shorthand properties that exist in your style objects. 6 | 7 | Read our blog post [here](http://www.actioniq.co/blog/reactjs-cramping-my-style/) 8 | 9 | ## Example 10 | 11 | ``` 12 | const StyleBuilder = require("style-builder"); 13 | 14 | const _styles = StyleBuilder.build({ 15 | margin: "5px 10px" 16 | }); 17 | 18 | console.log(_styles); 19 | { 20 | marginTop: "5px", 21 | marginRight: "10px", 22 | marginBottom: "5px", 23 | marginLeft: "10px" 24 | } 25 | ``` 26 | ## Options 27 | Style builder can also take options when building styles. Currently only one option is available. 28 | 29 | `cache` (default: `true`): If true, style builder will store a cache of the results from style functions, key'd off the arguments. This is very useful in React if you use the pure render mixin. Each time you pass a computed style to a child component, it will receive the exact (===) same style object. 30 | 31 | For example: 32 | ``` 33 | const _styles = StyleBuilder.build({ 34 | awesomeStyle: (iLikeGreen) => ({ 35 | background: iLikeGreen ? "green" : "blue", 36 | }), 37 | }, { 38 | cache: true, 39 | }); 40 | console.log(_styles.awesomeStyle(true) === _styles.awesomeStyle(true)); // true 41 | 42 | const _styles = StyleBuilder.build({ 43 | awesomeStyle: (iLikeGreen) => ({ 44 | background: iLikeGreen ? "green" : "blue", 45 | }), 46 | }, { 47 | cache: false, 48 | }); 49 | console.log(_styles.awesomeStyle(true) === _styles.awesomeStyle(true)); // false 50 | ``` 51 | ## Build 52 | ``` 53 | npm run-script build 54 | ``` 55 | 56 | ## Test 57 | ``` 58 | npm test 59 | ``` 60 | 61 | ## TODO 62 | * background 63 | * font 64 | * transition 65 | * transform 66 | * list-style 67 | 68 | -------------------------------------------------------------------------------- /StyleBuilder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * StyleBuilder. Breaks apart short hand properties (padding, margin, border, etc) into their 3 | * sub components. 4 | * 5 | * Currently react doesn't properly handle short hand properties: http://jsfiddle.net/ox3Lcmuy/ 6 | * 7 | * If you start with 8 | * { border: 1px solid red } 9 | * then add an active state with 10 | * { borderColor: green } 11 | * you'll correctly see 12 | * { border: 1px solid green } 13 | * but as soon as you remove that active border state and only define { border: 1px soild red} again 14 | * you'll be left with a black border because of the way React handles its style diffing. 15 | * See a more detailed explanation here: 16 | * 17 | * By always breaking up short hand components into their smaller parts we should be able to sidestep 18 | * this problem. 19 | * 20 | * TODO: 21 | * background 22 | * font 23 | * transition 24 | * transform 25 | * list-style 26 | */ 27 | 28 | const objectAssign = require('object-assign'); 29 | "use strict"; 30 | 31 | const _borderStyles = ["none", "hidden", "dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset"]; 32 | const _units = ["px", "em", "pt", "%"]; 33 | 34 | class StyleBuilder { 35 | 36 | /* 37 | * Properly converts 4 part values (like padding and margin) into an 38 | * array, each index matching one of the 4 sub components. 39 | */ 40 | _explodeToFour(value) { 41 | let fourPartArray = [0, 1, 2, 3]; 42 | let valueArray = value.split(" "); 43 | if (valueArray.length == 1) { 44 | fourPartArray = fourPartArray.map(() => valueArray[0]); 45 | } else if (valueArray.length == 2) { 46 | fourPartArray = fourPartArray.map((v, i) => valueArray[i % 2]); 47 | } else if (valueArray.length == 3) { 48 | fourPartArray = fourPartArray.map((v, i) => valueArray[i % 3 + Math.floor(i / 3)]); 49 | } else { 50 | fourPartArray = valueArray; 51 | } 52 | return fourPartArray; 53 | } 54 | 55 | /* 56 | * Takes in an array of length 4 with a prefix and suffix and converts 57 | * it to an object with Top, Right, Bottom, Left 58 | */ 59 | _applyFour(valueArr, prefix, suffix) { 60 | prefix = prefix || ""; 61 | suffix = suffix || ""; 62 | const styles = {}; 63 | styles[`${prefix}Top${suffix}`] = valueArr[0]; 64 | styles[`${prefix}Right${suffix}`] = valueArr[1]; 65 | styles[`${prefix}Bottom${suffix}`] = valueArr[2]; 66 | styles[`${prefix}Left${suffix}`] = valueArr[3]; 67 | return styles; 68 | } 69 | 70 | /* 71 | * Takes in an array of length 4 with a prefix and suffix and converts 72 | * it to an object with TopLeft, TopRight, BottomRight, BottomLeft 73 | */ 74 | _applyFourCorner(valueArr, prefix, suffix) { 75 | prefix = prefix || ""; 76 | suffix = suffix || ""; 77 | const styles = {}; 78 | styles[`${prefix}TopLeft${suffix}`] = valueArr[0]; 79 | styles[`${prefix}TopRight${suffix}`] = valueArr[1]; 80 | styles[`${prefix}BottomRight${suffix}`] = valueArr[2]; 81 | styles[`${prefix}BottomLeft${suffix}`] = valueArr[3]; 82 | return styles; 83 | } 84 | 85 | /* 86 | * Adds px to any numbers that do not have an extension 87 | */ 88 | applyPX(values) { 89 | return values.map(v => { 90 | if (_units.filter(u => v.indexOf(u) > -1).length === 0) { 91 | return v + "px"; 92 | } 93 | return v; 94 | }); 95 | } 96 | 97 | margin(value) { 98 | return this._applyFour(this._explodeToFour(value), "margin"); 99 | } 100 | 101 | padding(value) { 102 | return this._applyFour(this._explodeToFour(value), "padding"); 103 | } 104 | 105 | borderRadius(value) { 106 | let partOne = this._explodeToFour(value.split("/")[0]); 107 | if (value.indexOf("/") > -1) { 108 | const partTwo = this._explodeToFour(value.split("/")[1]); 109 | partOne = partOne.map((v, index) => v + " " + partTwo[index]); 110 | } 111 | return this._applyFourCorner(partOne, "border", "Radius"); 112 | } 113 | 114 | borderStyle(value) { 115 | return this._applyFour(this._explodeToFour(value), "border", "Style"); 116 | } 117 | 118 | borderColor(value) { 119 | return this._applyFour(this._explodeToFour(value), "border", "Color"); 120 | } 121 | 122 | borderWidth(value) { 123 | return this._applyFour(this._explodeToFour(value), "border", "Width"); 124 | } 125 | 126 | borderSide(value, side) { 127 | const styles = { 128 | [`border${side}Width`]: "initial", 129 | [`border${side}Style`]: "initial", 130 | [`border${side}Color`]: "initial", 131 | }; 132 | 133 | const values = value.split(" "); 134 | if (values.length > 3) { 135 | console.warn(`More than 3 properties found in border: ${value}. Only using first 3`); 136 | } 137 | 138 | let widthIndex, styleIndex, colorIndex = -1; 139 | [0, 1, 2].forEach(index => { 140 | if (values[index] === undefined) { return; } 141 | 142 | const v = values[index].trim(); 143 | 144 | if (_borderStyles.indexOf(v) > -1) { 145 | if (styleIndex > -1) { console.warn(`Found more than one 'style' border prop: ${value}`); } 146 | styleIndex = index; 147 | } else if (!isNaN(parseFloat(v))) { 148 | if (widthIndex > -1) { console.warn(`Found more than one 'width' border prop: ${value}`); } 149 | widthIndex = index; 150 | } else { 151 | if (colorIndex > -1) { console.warn(`Found more than one 'color' border prop: ${value}`); } 152 | colorIndex = index; 153 | } 154 | }); 155 | 156 | if (widthIndex > -1) { styles[`border${side}Width`] = values[widthIndex]; } 157 | if (styleIndex > -1) { styles[`border${side}Style`] = values[styleIndex]; } 158 | if (colorIndex > -1) { styles[`border${side}Color`] = values[colorIndex]; } 159 | 160 | return styles; 161 | } 162 | 163 | borderLeft(value) { 164 | return this.borderSide(value, "Left"); 165 | } 166 | 167 | borderRight(value) { 168 | return this.borderSide(value, "Right"); 169 | } 170 | 171 | borderTop(value) { 172 | return this.borderSide(value, "Top"); 173 | } 174 | 175 | borderBottom(value) { 176 | return this.borderSide(value, "Bottom"); 177 | } 178 | 179 | border(value) { 180 | // TODO: Properly handle { border: