├── .gitignore ├── README.md ├── angular2.template ├── bin.js ├── examples ├── component-viewports.css ├── nested-component.css └── simple-component.css ├── index.js ├── loader └── index.js ├── package.json ├── react.template └── webpack-example ├── .babelrc ├── UserComponent.css ├── advanced-component.css ├── component.css ├── entry.js ├── index.html └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSS Components 2 | 3 | ![UserComponent](http://f.cl.ly/items/0s2x2x362p0O0K2L1828/Screen%20Shot%202016-06-17%20at%204.03.34%20PM.png) 4 | 5 | This component is authored completely in the CSS you see below. 6 | 7 | ```css 8 | .UserComponent { 9 | display: flex; 10 | font: 12px Helvetica; 11 | color: #333; 12 | max-width: 680px; 13 | margin: auto; 14 | 15 | .Avatar { 16 | @component avatar; 17 | padding-right: 16px; 18 | } 19 | 20 | .Details { 21 | .Name { 22 | font-size: 36px; 23 | padding-bottom: 8px; 24 | @component name; 25 | } 26 | 27 | .Extra { 28 | font-size: 12px; 29 | color: #999; 30 | padding-bottom: 8px; 31 | @component extra; 32 | } 33 | 34 | .Biography { 35 | font-size: 14px; 36 | @component biography; 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | However in your React codebase, it feels as if it is implemented first class. 43 | 44 | ```javascript 45 | } 47 | name='Merrick Christensen' 48 | biography='I am a kind chap but I get distracted easily. This entire component is compiled at build time. It can also compile Angular 2 components. I am not sure but I think I took this idea too far.' 49 | extra='Age 26 & Location Utah' 50 | /> 51 | ``` 52 | 53 | ## Explain yourself! 54 | 55 | CSS Components allows you to describe components completely in CSS. It uses nesting to to deterime DOM structure & automatically flattens and namespaces your CSS. It uses @rules to decide where certain properties should render to. It supports rendering to multiple frameworks, current React and Angular 2. 56 | 57 | What if you could author shared component libraries and target multiple frameworks for first class APIs? 58 | 59 | Currently there is a shoddy CLI and a shoddy webpack loader for this proof of concept. This leverages PostCSS so adding additional post-processing plugins for the CSS would be fairly trivial. 60 | 61 | 62 | ## The Concept 63 | 64 | ```css 65 | .User { 66 | background-color: #000; 67 | color: #FFF; 68 | @component children; 69 | } 70 | ``` 71 | 72 | This can then be used in your codebase: 73 | 74 | ```javascript 75 | import User from 'css-component!User.css'; 76 | 77 | ... 78 | Hello World! 79 | ... 80 | 81 | ``` 82 | 83 | Compiles a component that will effectively render: 84 | 85 | ```html 86 | 92 |
93 | Hello World! 94 |
95 | ``` 96 | 97 | It supports nesting, sibling elements and custom properties as well so you aren't stuck to just children. 98 | 99 | 100 | 101 | 102 | ## Supports Multiple Rendering Targets 103 | 104 | 105 | ### React 106 | Compiling the above example for React would return something like this: 107 | 108 | ```javascript 109 | import React from 'react'; 110 | 111 | const UserComponent = ({avatar,name,extra,biography}) => ( 112 |
113 | 114 |
115 |
116 | {avatar} 117 |
118 |
119 |
120 | {name} 121 |
122 |
123 | {extra} 124 |
125 |
126 | {biography} 127 |
128 |
129 |
130 |
131 | ); 132 | 133 | export default UserComponent; 134 | ``` 135 | 136 | ### Angular 2 137 | 138 | And compiling the same file for Angular 2, would return something like this: 139 | 140 | ```javascript 141 | import { Component } from '@angular/core'; 142 | 143 | @Component({ 144 | selector: 'UserComponent', 145 | template: ` 146 |
147 | 172 |
173 |
174 | 175 |
176 |
177 |
178 | 179 |
180 |
181 | 182 |
183 |
184 | 185 |
186 |
187 |
188 |
189 | ` 190 | }) 191 | export class UserComponent{ } 192 | ``` 193 | 194 | 195 | Prior Art: https://github.com/andreypopp/react-css-components 196 | -------------------------------------------------------------------------------- /angular2.template: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: '{{name}}', 5 | template: ` 6 |
7 | 10 | {{{template}}} 11 |
12 | ` 13 | }) 14 | export class {{name}}{ } 15 | -------------------------------------------------------------------------------- /bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var component = require('./index.js'); 4 | var program = require('commander'); 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | 8 | program 9 | .version('0.0.1') 10 | .option('-t, --target', 'Select a framework') 11 | .arguments(' [options]') 12 | .action(function (file, framework) { 13 | console.log(component.parse(fs.readFileSync(path.join(process.cwd(), file), 'utf8'), framework)); 14 | }) 15 | .parse(process.argv) 16 | 17 | -------------------------------------------------------------------------------- /examples/component-viewports.css: -------------------------------------------------------------------------------- 1 | .MyComponent { 2 | background-color: #FF0000; 3 | @component children; 4 | 5 | .Child { 6 | @component icon; 7 | font: 12px Helvetica; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/nested-component.css: -------------------------------------------------------------------------------- 1 | .MyComponent { 2 | background-color: #FF0000; 3 | 4 | .Child { 5 | font: 12px Helvetica; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/simple-component.css: -------------------------------------------------------------------------------- 1 | .MyComponent { 2 | background-color: #FF0000; 3 | } 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const postcss = require('postcss'); 2 | const Mustache = require('mustache'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const genericNames = require('generic-names'); 6 | 7 | const strategies = { 8 | react(name, template, css, args) { 9 | return Mustache.render(fs.readFileSync(path.join(__dirname, './react.template'), 'utf8'), { 10 | template: template.join('\n'), 11 | css: css.toResult().toString().replace(/\n/g, ''), 12 | name: name.replace('.', ''), 13 | args: args.join(',') 14 | }); 15 | }, 16 | angular2(name, template, css, args) { 17 | return Mustache.render(fs.readFileSync(path.join(__dirname, './angular2.template'), 'utf8'), { 18 | template: template.join('\n'), 19 | css: css.toResult().toString(), 20 | name: name.replace('.', '') 21 | }); 22 | } 23 | }; 24 | 25 | const generate = genericNames('[name]__[local]__[hash:base64:5]', { 26 | context: process.cwd() 27 | }); 28 | 29 | const parse = (source, type = 'react') => { 30 | const tree = postcss.parse(source); 31 | const output = []; 32 | const css = postcss.root(); 33 | const args = []; 34 | 35 | const getStyles = (rule) => { 36 | const selector = generate(rule.selector, 'Source'); 37 | const nextRule = postcss.rule({ 38 | selector: `.${selector}` 39 | }); 40 | 41 | rule.each((node) => { 42 | if (node.type !== 'rule') { 43 | if (node.type === 'atrule' && node.name === 'component') return; 44 | nextRule.append(node); 45 | } 46 | }); 47 | return { 48 | selector, 49 | rule: nextRule 50 | }; 51 | }; 52 | 53 | const walk = (node) => { 54 | if (node.type === 'rule') { 55 | const styles = getStyles(node); 56 | css.append(styles.rule); 57 | if (type === 'react') { 58 | output.push(`
`) 59 | } 60 | 61 | if (type === 'angular2') { 62 | output.push(`
`) 63 | } 64 | node.each(walk); 65 | output.push('
'); 66 | } 67 | if (node.type === 'atrule' && node.name === 'component') { 68 | args.push(node.params); 69 | if (type === 'react') { 70 | output.push(`{${node.params}}`); 71 | } 72 | if (type === 'angular2') { 73 | if (node.params === 'children') { 74 | output.push(``); 75 | } else { 76 | output.push(``); 77 | } 78 | } 79 | } 80 | }; 81 | 82 | tree.each(walk); 83 | 84 | return strategies[type](tree.nodes[0].selector, output, css, args); 85 | }; 86 | 87 | module.exports = { 88 | parse 89 | }; 90 | -------------------------------------------------------------------------------- /loader/index.js: -------------------------------------------------------------------------------- 1 | var component = require('../index'); 2 | module.exports = function(source) { 3 | return component.parse(source, 'react'); 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-components", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "escodegen": "^1.8.0", 13 | "mustache": "^2.2.1", 14 | "postcss": "^5.0.21" 15 | }, 16 | "devDependencies": { 17 | "babel-core": "^6.9.1", 18 | "babel-loader": "^6.2.4", 19 | "babel-preset-es2015": "^6.9.0", 20 | "expect": "^1.20.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /react.template: -------------------------------------------------------------------------------- 1 | {{=<% %>=}} 2 | import React from 'react'; 3 | 4 | const <%name%> = ({<%args%>}) => ( 5 |
6 | 7 | <%={{ }}=%> 8 | {{{template}}} 9 |
10 | ); 11 | 12 | export default {{name}}; 13 | -------------------------------------------------------------------------------- /webpack-example/.babelrc: -------------------------------------------------------------------------------- 1 | { "presets": ["es2015", "react"] } 2 | -------------------------------------------------------------------------------- /webpack-example/UserComponent.css: -------------------------------------------------------------------------------- 1 | .UserComponent { 2 | display: flex; 3 | font: 12px Helvetica; 4 | color: #333; 5 | max-width: 680px; 6 | margin: auto; 7 | 8 | .Avatar { 9 | @component avatar; 10 | padding-right: 16px; 11 | } 12 | 13 | .Details { 14 | .Name { 15 | font-size: 36px; 16 | padding-bottom: 8px; 17 | @component name; 18 | } 19 | 20 | .Extra { 21 | font-size: 12px; 22 | color: #999; 23 | padding-bottom: 8px; 24 | @component extra; 25 | } 26 | 27 | .Biography { 28 | font-size: 14px; 29 | @component biography; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /webpack-example/advanced-component.css: -------------------------------------------------------------------------------- 1 | .AdvancedComponent { 2 | background-color: #000; 3 | color: #FFF; 4 | display: flex; 5 | justify-content: space-between; 6 | 7 | .Children { 8 | @component children; 9 | } 10 | 11 | .Icon { 12 | @component icon; 13 | color: green; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /webpack-example/component.css: -------------------------------------------------------------------------------- 1 | .MyComponent { 2 | background-color: #FF0; 3 | @component children; 4 | } 5 | -------------------------------------------------------------------------------- /webpack-example/entry.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import CSSComponent from 'babel!../loader/index.js!./component.css'; 4 | import AdvancedCSSComponent from 'babel!../loader/index.js!./advanced-component.css'; 5 | import UserComponent from 'babel!../loader/index.js!./UserComponent.css'; 6 | 7 | const Icon = () => ( 8 | Icon 9 | ); 10 | 11 | class App extends React.Component { 12 | render() { 13 | return ( 14 |
15 | } 17 | name='Merrick Christensen' 18 | biography='I am a kind chap but I get distracted easily. This entire component is compiled at build time. It can also compile Angular 2 components. I am not sure but I think I took this idea too far.' 19 | extra='Age 26 & Location Utah' 20 | /> 21 |
22 | ); 23 | } 24 | } 25 | 26 | ReactDOM.render(, document.getElementById('app')); 27 | -------------------------------------------------------------------------------- /webpack-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /webpack-example/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './entry.js', 3 | output: { 4 | path: __dirname, 5 | filename: 'bundle.js' 6 | }, 7 | resolve: { 8 | alias: { 9 | present: 'babel?presets=es2015,react!../loader/index.js' 10 | } 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.js$/, 16 | exclude: /(node_modules)/, 17 | loader: 'babel', // 'babel-loader' is also a legal name to reference 18 | query: { 19 | presets: ['es2015', 'react'] 20 | } 21 | } 22 | ] 23 | } 24 | }; 25 | --------------------------------------------------------------------------------