├── .gitignore ├── src ├── preact-create-element.js ├── inferno-create-element.js ├── react-create-element.js ├── preact.js ├── hyperscript.js ├── inferno.js ├── react.js ├── deku-create-element.js ├── bel.js ├── store.js ├── deku.js ├── hyperscript-create-element.js └── bel-create-element.js ├── .babelrc ├── webpack.bel.config.js ├── webpack.deku.config.js ├── webpack.inferno.config.js ├── webpack.preact.config.js ├── webpack.react.config.js ├── webpack.hyperscript.config.js ├── components ├── Button.js └── Hello.js ├── webpack.config.js ├── dist └── index.html ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/*.js 3 | -------------------------------------------------------------------------------- /src/preact-create-element.js: -------------------------------------------------------------------------------- 1 | 2 | const h = require('preact').h 3 | module.exports = h 4 | -------------------------------------------------------------------------------- /src/inferno-create-element.js: -------------------------------------------------------------------------------- 1 | const h = require('inferno-create-element') 2 | module.exports = h 3 | -------------------------------------------------------------------------------- /src/react-create-element.js: -------------------------------------------------------------------------------- 1 | 2 | const h = require('react').createElement 3 | module.exports = h 4 | -------------------------------------------------------------------------------- /src/preact.js: -------------------------------------------------------------------------------- 1 | 2 | import { render } from 'preact' 3 | import Hello from '../components/Hello' 4 | 5 | const div = document.getElementById('preact') 6 | render(, div) 7 | 8 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0" 5 | ], 6 | "plugins": [ 7 | [ 8 | "transform-react-jsx", 9 | { "pragma": "h" } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/hyperscript.js: -------------------------------------------------------------------------------- 1 | 2 | import Hello from '../components/Hello' 3 | 4 | const div = document.getElementById('hyperscript') 5 | const tree = 6 | 7 | div.appendChild(tree) 8 | 9 | -------------------------------------------------------------------------------- /src/inferno.js: -------------------------------------------------------------------------------- 1 | import InfernoDOM from 'inferno-dom' 2 | import Hello from '../components/Hello' 3 | 4 | const div = document.getElementById('inferno') 5 | InfernoDOM.render(, div) 6 | -------------------------------------------------------------------------------- /src/react.js: -------------------------------------------------------------------------------- 1 | 2 | // import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import Hello from '../components/Hello' 5 | 6 | const div = document.getElementById('react') 7 | ReactDOM.render(, div) 8 | 9 | -------------------------------------------------------------------------------- /webpack.bel.config.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | const config = require('./webpack.config') 5 | 6 | config.entry.bel = './src/bel.js' 7 | 8 | config.plugins = [ 9 | new webpack.ProvidePlugin({ 10 | h: path.resolve('./src/bel-create-element') 11 | }) 12 | ] 13 | 14 | module.exports = config 15 | 16 | -------------------------------------------------------------------------------- /webpack.deku.config.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | const config = require('./webpack.config') 5 | 6 | config.entry.deku = './src/deku.js' 7 | 8 | config.plugins = [ 9 | new webpack.ProvidePlugin({ 10 | h: path.resolve('./src/deku-create-element') 11 | }) 12 | ] 13 | 14 | module.exports = config 15 | 16 | -------------------------------------------------------------------------------- /webpack.inferno.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const config = require('./webpack.config') 4 | 5 | config.entry.inferno = './src/inferno.js' 6 | 7 | config.plugins = [ 8 | new webpack.ProvidePlugin({ 9 | h: path.resolve('./src/inferno-create-element') 10 | }) 11 | ] 12 | 13 | module.exports = config 14 | 15 | -------------------------------------------------------------------------------- /webpack.preact.config.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | const config = require('./webpack.config') 5 | 6 | config.entry.preact = './src/preact.js' 7 | 8 | config.plugins = [ 9 | new webpack.ProvidePlugin({ 10 | h: path.resolve('./src/preact-create-element') 11 | }) 12 | ] 13 | 14 | module.exports = config 15 | 16 | -------------------------------------------------------------------------------- /webpack.react.config.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | const config = require('./webpack.config') 5 | 6 | config.entry.react = './src/react.js' 7 | 8 | config.plugins = [ 9 | new webpack.ProvidePlugin({ 10 | h: path.resolve('./src/react-create-element') 11 | }) 12 | ] 13 | 14 | module.exports = config 15 | 16 | -------------------------------------------------------------------------------- /src/deku-create-element.js: -------------------------------------------------------------------------------- 1 | 2 | // Not working 3 | 4 | const h = require('deku').element 5 | 6 | module.exports = (tag, props, ...children) => { 7 | if (!children) { 8 | console.log('children', children) 9 | } 10 | if (typeof tag === 'function') { 11 | return { 12 | render: tag // ({ ...props, ...children }) 13 | } 14 | } 15 | 16 | return h(tag, props, ...children) 17 | } 18 | 19 | -------------------------------------------------------------------------------- /webpack.hyperscript.config.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | const config = require('./webpack.config') 5 | 6 | config.entry.hyperscript = './src/hyperscript.js' 7 | 8 | config.plugins = [ 9 | new webpack.ProvidePlugin({ 10 | h: path.resolve('./src/hyperscript-create-element') 11 | // h: 'hyperscript' 12 | }) 13 | ] 14 | 15 | module.exports = config 16 | 17 | -------------------------------------------------------------------------------- /components/Button.js: -------------------------------------------------------------------------------- 1 | 2 | const sx = { 3 | fontFamily: 'inherit', 4 | fontSize: 14, 5 | display: 'inline-block', 6 | padding: 8, 7 | margin: 0, 8 | color: 'white', 9 | backgroundColor: 'black', 10 | border: 0, 11 | borderRadius: 3, 12 | WebkitAppearance: 'none', 13 | MozAppearance: 'none', 14 | cursor: 'pointer' 15 | } 16 | 17 | const Button = ({ children, ...props }) => ( 18 | 19 | ) 20 | 21 | export default Button 22 | 23 | -------------------------------------------------------------------------------- /src/bel.js: -------------------------------------------------------------------------------- 1 | 2 | // import { update } from 'yo' 3 | import Hello from '../components/Hello' 4 | import Button from '../components/Button' 5 | import readme from '../README.md' 6 | 7 | const div = document.getElementById('bel') 8 | 9 | const tree = 10 | const btn = tree.querySelector('button') 11 | div.appendChild(tree) 12 | 13 | const sx = { 14 | padding: 32, 15 | maxWidth: 640 16 | } 17 | const content =
18 | content.innerHTML = readme 19 | document.body.appendChild(content) 20 | 21 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path') 3 | 4 | const config = { 5 | entry: {}, 6 | output: { 7 | path: path.join(__dirname, 'dist'), 8 | filename: '[name].js' 9 | }, 10 | module: { 11 | resolve: { 12 | root: [ 13 | path.resolve('./src'), 14 | path.resolve('./components'), 15 | ] 16 | }, 17 | loaders: [ 18 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel' }, 19 | { test: /\.md/, loader: 'html!highlight!markdown' } 20 | ] 21 | }, 22 | devServer: { 23 | contentBase: 'dist' 24 | } 25 | } 26 | 27 | module.exports = config 28 | 29 | -------------------------------------------------------------------------------- /components/Hello.js: -------------------------------------------------------------------------------- 1 | 2 | import Button from './Button' 3 | 4 | const handleClick = lib => e => { 5 | console.log('Hello', lib) 6 | alert(`Hello ${lib}`) 7 | } 8 | 9 | const Hello = ({ lib }) => { 10 | const sx = { 11 | root: { 12 | padding: 16, 13 | backgroundColor: '#f6f6f6' 14 | }, 15 | heading: { 16 | marginTop: 0 17 | } 18 | } 19 | return ( 20 |
22 |

Hello {lib}

23 | 27 |
28 | ) 29 | } 30 | 31 | export default Hello 32 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 |

Universal Components

8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | 2 | let _state = {} 3 | let _reducer = () => _state 4 | const store = { 5 | get state () { 6 | return _state 7 | }, 8 | set reducer (f) { 9 | _state = f(undefined, {}) 10 | _reducer = f 11 | }, 12 | dispatch: (action) => { 13 | _state = { 14 | ..._state, 15 | ..._reducer(_state, action) 16 | } 17 | store.listener(_state) 18 | }, 19 | listener: () => {} 20 | } 21 | 22 | export const INC = 'INC' 23 | export const DEC = 'DEC' 24 | 25 | store.reducer = (state = { 26 | count: 0 27 | }, action) => { 28 | switch (action.type) { 29 | case INC: 30 | return { 31 | ...state, 32 | count: state.count + 1 33 | } 34 | case DEC: 35 | return { 36 | ...state, 37 | count: state.count - 1 38 | } 39 | default: 40 | return state 41 | } 42 | } 43 | 44 | export default store 45 | 46 | -------------------------------------------------------------------------------- /src/deku.js: -------------------------------------------------------------------------------- 1 | 2 | import { createApp } from 'deku' 3 | import Hello from '../components/Hello' 4 | import Button from '../components/Button' 5 | 6 | let _state = {} 7 | let _reducer = () => _state 8 | const store = { 9 | get state () { 10 | return _state 11 | }, 12 | set reducer (f) { 13 | _state = f(undefined, {}) 14 | _reducer = f 15 | }, 16 | dispatch: (action) => { 17 | _state = { 18 | ..._state, 19 | ..._reducer(_state, action) 20 | } 21 | store.listener(_state) 22 | }, 23 | listener: () => {} 24 | } 25 | 26 | store.reducer = (state = { 27 | lib: 'Deku' 28 | }, action) => { 29 | switch (action.type) { 30 | default: 31 | return state 32 | } 33 | } 34 | 35 | const div = document.getElementById('deku') 36 | const render = createApp(div, store.dispatch) 37 | 38 | console.log('jsx test', render( 39 |
40 |

Hello

41 | 42 |
43 | )) 44 | // console.log('jsx test', render()) 45 | 46 | // render(, store.state) 47 | 48 | -------------------------------------------------------------------------------- /src/hyperscript-create-element.js: -------------------------------------------------------------------------------- 1 | 2 | const h = require('hyperscript').context() 3 | const addPx = require('add-px-to-style') 4 | 5 | const parseValue = (prop, val) => typeof val === 'number' ? addPx(prop, val) : val 6 | const kebab = (str) => str.replace(/([A-Z])/g, g => '-' + g.toLowerCase()) 7 | 8 | const transformProps = (props) => { 9 | if (props.style && typeof props.style === 'object') { 10 | Object.keys(props.style) 11 | .forEach(key => { 12 | const kebabkey = kebab(key) 13 | props.style[kebabkey] = parseValue(key, props.style[key]) 14 | if (kebabkey !== key) { 15 | delete props.style[key] 16 | } 17 | }) 18 | } 19 | 20 | for (let key in props) { 21 | if (/^on/.test(key)) { 22 | const lowerkey = key.toLowerCase() 23 | if (lowerkey !== key) { 24 | props[lowerkey] = props[key] 25 | delete props[key] 26 | } 27 | } 28 | } 29 | return props 30 | } 31 | 32 | module.exports = (tag, props, ...children) => { 33 | if (props && props.children) { 34 | children = props.children 35 | delete props.children 36 | } 37 | 38 | props = transformProps(props || {}) 39 | 40 | if (typeof tag === 'function') { 41 | props.children = children 42 | return tag(props, ...children) 43 | } 44 | return h(tag, props, ...children) 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/bel-create-element.js: -------------------------------------------------------------------------------- 1 | 2 | const createElement = require('bel').createElement 3 | const addPx = require('add-px-to-style') 4 | 5 | const parseValue = (prop, val) => typeof val === 'number' ? addPx(prop, val) : val 6 | const kebab = (str) => str.replace(/([A-Z])/g, g => '-' + g.toLowerCase()) 7 | const styleToString = (style) => { 8 | return Object.keys(style) 9 | .filter(key => style[key] !== null) 10 | .map(key => `${kebab(key)}:${parseValue(key, style[key])}`) 11 | .join(';') 12 | } 13 | 14 | const transformProps = (props) => { 15 | if (props.style && typeof props.style === 'object') { 16 | props.style = styleToString(props.style) 17 | } 18 | 19 | for (let key in props) { 20 | if (/^on/.test(key)) { 21 | const lowerkey = key.toLowerCase() 22 | if (lowerkey !== key) { 23 | props[lowerkey] = props[key] 24 | delete props[key] 25 | } 26 | } 27 | } 28 | return props 29 | } 30 | 31 | const h = (tag, props = {}, ...children) => { 32 | if (props && props.children) { 33 | children = props.children 34 | delete props.children 35 | } 36 | 37 | props = transformProps(props || {}) 38 | 39 | if (typeof tag === 'function') { 40 | props = props || {} 41 | props.children = children 42 | const root = tag(props, ...children) 43 | return root 44 | } 45 | 46 | return createElement(tag, props || {}, children) 47 | } 48 | module.exports = h 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Universal Components 3 | 4 | An experiment in creating library-agnostic universal functional UI components 5 | 6 | http://jxnblk.com/universal-components 7 | 8 | ```sh 9 | npm i 10 | npm run build 11 | ``` 12 | 13 | This repo has two generic functional UI components in the [`/components`](/components) folder. 14 | 15 | ```js 16 | // Button.js 17 | const sx = { 18 | fontFamily: 'inherit', 19 | fontSize: 14, 20 | display: 'inline-block', 21 | padding: 8, 22 | margin: 0, 23 | color: 'white', 24 | backgroundColor: 'black', 25 | border: 0, 26 | borderRadius: 3, 27 | WebkitAppearance: 'none', 28 | MozAppearance: 'none', 29 | cursor: 'pointer' 30 | } 31 | 32 | const Button = ({ ...props }) => ( 33 | 66 |
67 | ) 68 | } 69 | 70 | export default Hello 71 | ``` 72 | 73 | These components are rendered by five different libraries: 74 | 75 | - React 76 | - Preact 77 | - Hyperscript 78 | - Bel 79 | - Inferno 80 | 81 | --- 82 | 83 | ### To do: 84 | 85 | - [ ] Hook up a generic store 86 | 87 | MIT License 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "universal-components", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "react": "webpack -p --config webpack.react.config.js", 8 | "preact": "webpack -p --config webpack.preact.config.js", 9 | "hyperscript": "webpack -p --config webpack.hyperscript.config.js", 10 | "bel": "webpack -p --config webpack.bel.config.js", 11 | "inferno": "webpack -p --config webpack.inferno.config.js", 12 | "react-dev": "webpack-dev-server --config webpack.react.config.js", 13 | "preact-dev": "webpack-dev-server --config webpack.preact.config.js", 14 | "hyperscript-dev": "webpack-dev-server --config webpack.hyperscript.config.js", 15 | "bel-dev": "webpack-dev-server --config webpack.bel.config.js", 16 | "deku-dev": "webpack-dev-server --config webpack.deku.config.js", 17 | "inferno-dev": "webpack-dev-server --config webpack.inferno.config.js", 18 | "build": "mkdir -p dist && npm run react && npm run preact && npm run hyperscript && npm run bel && npm run inferno", 19 | "gh-pages": "gh-pages -d dist" 20 | }, 21 | "keywords": [], 22 | "author": "", 23 | "license": "MIT", 24 | "devDependencies": { 25 | "add-px-to-style": "^1.0.0", 26 | "babel-loader": "^6.2.4", 27 | "babel-plugin-transform-react-jsx": "^6.8.0", 28 | "babel-preset-es2015": "^6.9.0", 29 | "babel-preset-stage-0": "^6.5.0", 30 | "babel-register": "^6.9.0", 31 | "bel": "^4.4.2", 32 | "deku": "^2.0.0-rc16", 33 | "gh-pages": "^0.11.0", 34 | "highlight-loader": "^0.7.2", 35 | "html-loader": "^0.4.3", 36 | "hyperscript": "^1.4.7", 37 | "inferno-create-element": "^0.7.24", 38 | "inferno-dom": "^0.7.24", 39 | "markdown-loader": "^0.1.7", 40 | "preact": "^4.8.0", 41 | "react": "^15.2.0", 42 | "react-dom": "^15.2.1", 43 | "webpack": "^1.13.1", 44 | "webpack-dev-server": "^1.14.1", 45 | "yo-yo": "^1.2.2" 46 | } 47 | } 48 | --------------------------------------------------------------------------------