├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json └── src ├── hash.js ├── index.js └── test.index.js /.gitignore: -------------------------------------------------------------------------------- 1 | ./node_modules 2 | node_modules 3 | 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.0 -- hyperapp-styled-components 2 | 3 | 1. Basic testing 4 | 2. Basic docs 5 | 3. License 6 | 4. linting 7 | 5. basic exports 8 | 9 | # 0.1.2 -- hyperapp v2 support 10 | 11 | 1. support hyperapp v2 12 | 2. add trimming for css injection of unused brackets 13 | 3. fix object assign issue 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This Code of Conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting the project maintainer at nick.dodson@consensys.net. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | 45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 46 | version 1.3.0, available at 47 | [http://contributor-covenant.org/version/1/3/0/][version] 48 | 49 | [homepage]: http://contributor-covenant.org 50 | [version]: http://contributor-covenant.org/version/1/3/0/ 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2020 Nick Dodson. nickdodson.com 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## hyperapp-styled-components 2 | 3 |
4 | 5 | 6 | NPM version 8 | 9 |
10 | 11 |
12 | 13 | A super tiny Hyperapp equivalent of [`styled-components`](https://github.com/styled-components/styled-components). 14 | 15 | ## Install 16 | 17 | ``` 18 | npm install --save hyperapp-styled-components 19 | ``` 20 | 21 | ## Usage 22 | 23 | First add this to your HTML file in the tag: `` than.. 24 | 25 | ```js 26 | import { h, app } from "hyperapp"; 27 | import styled from 'hyperapp-styled-components'; 28 | 29 | const Header = styled.h2` 30 | color: #333; 31 | `; 32 | 33 | const MyButton = styled.button` 34 | padding: 10px; 35 | border-radius: ${0}px; 36 | background: #F1F1F1; 37 | 38 | &:hover { 39 | background: ${props => props.color}; 40 | } 41 | `; 42 | 43 | const Wrapper = styled.div` 44 | width: 500px; 45 | border: 1px solid #aaa; 46 | 47 | @media (min-width: 400px) { 48 | width: 100%; 49 | background: #F1F1F1; 50 | } 51 | `; 52 | 53 | app({ 54 | init: 0, 55 | view: state => ( 56 | 57 |
Welcome to Styled Hyperapp {state ? 'Yay!' : ''}
58 | 59 | 1}>Go 60 |
61 | ), 62 | node: document.getElementById("app") 63 | }); 64 | ``` 65 | 66 | ## Features 67 | 68 | - Super tiny **2.1kb** gzipped 69 | - Completely DOM based 70 | - Supports `@media` 71 | - Supports `@keyframes` (via `keyframes` method) 72 | - Supports pseudo CSS (i.e. `&:hover`) 73 | - State can be fed in through primitive methods (i.e `props => ...`) 74 | - No dependencies 75 | - Uses template literals and real CSS 76 | - Supports all DOM elements 77 | - Auto CSS class injection/management 78 | - Works with `hyperapp` 79 | 80 | ## About 81 | 82 | I love `styled-components` and needed a HyperApp equivalent for a project. This is the result. It functions almost the same with pseudo and media queries supported. Dynamic props can be fed in through primitive functions; the output of each is a function where props can be fed in, which then returns another function where child elements and strings can be fed in. 83 | 84 | A special thanks to [Max Stoiber](https://twitter.com/mxstbr) and the `styled-components` team for coming up with a great component API for mixing CSS and JS. 85 | 86 | ## Usage with Props 87 | 88 | ```js 89 | import styled from 'hyperapp-styled-components'; 90 | 91 | const Header = styled.h2` 92 | color: #${props => props.status === 'success' ? '000' : '333'}; 93 | `; 94 | 95 | const View = () => () => ( 96 |
97 |
98 |
99 | ); 100 | ``` 101 | 102 | ## With Keyframes 103 | 104 | ```js 105 | import styled, { keyframes } from 'hyperapp-styled-components'; 106 | 107 | const boxmove = keyframes` 108 | 0% {top: 0px;} 109 | 25% {top: 200px;} 110 | 75% {top: 50px} 111 | 100% {top: 100px;} 112 | `; 113 | 114 | const Box = styled.div` 115 | background-color: lightcoral; 116 | width: 100px; 117 | height: 100px; 118 | display: block; 119 | position :relative; 120 | animation: ${boxmove} 5s infinite; 121 | `; 122 | 123 | ``` 124 | 125 | ## Important documents 126 | 127 | - [Changelog](CHANGELOG.md) 128 | - [Code of Conduct](CODE_OF_CONDUCT.md) 129 | - [License](https://raw.githubusercontent.com/SilentCicero/hyperapp-styled-components/master/LICENSE) 130 | 131 | ## Todo 132 | 133 | - Testing 134 | - Coverage 135 | - Documentation 136 | 137 | ## Licence 138 | 139 | This project is licensed under the MIT license, Copyright (c) 2016 Nick Dodson. For more information see LICENSE.md. 140 | 141 | ``` 142 | The MIT License 143 | 144 | Copyright (c) 2016 Nick Dodson. nickdodson.com 145 | 146 | Permission is hereby granted, free of charge, to any person obtaining a copy 147 | of this software and associated documentation files (the "Software"), to deal 148 | in the Software without restriction, including without limitation the rights 149 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 150 | copies of the Software, and to permit persons to whom the Software is 151 | furnished to do so, subject to the following conditions: 152 | 153 | The above copyright notice and this permission notice shall be included in 154 | all copies or substantial portions of the Software. 155 | 156 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 157 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 158 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 159 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 160 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 161 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 162 | THE SOFTWARE. 163 | ``` 164 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperapp-styled-components", 3 | "version": "0.1.2", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "hyperapp": { 8 | "version": "2.0.4", 9 | "resolved": "https://registry.npmjs.org/hyperapp/-/hyperapp-2.0.4.tgz", 10 | "integrity": "sha512-1S0KIsyB97S/hH84GkuYjH/hmpnNQ546x16o+W7g/PnszxRPudJlwwGBKKnaRVL7x+dyevvuX4XLH67FooEt4w==" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperapp-styled-components", 3 | "version": "0.1.2", 4 | "description": "A styled-components implementation for hyperapp (<3kb in size)", 5 | "main": "./src/index.js", 6 | "scripts": { 7 | "test": "./src/test.index.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/SilentCicero/hyperapp-styled-components.git" 12 | }, 13 | "keywords": [ 14 | "styled", 15 | "components", 16 | "hyperapp", 17 | "css", 18 | "frontend", 19 | "framework" 20 | ], 21 | "author": "Nick Dodson ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/SilentCicero/hyperapp-styled-components/issues" 25 | }, 26 | "homepage": "https://github.com/SilentCicero/hyperapp-styled-components#readme", 27 | "dependencies": { 28 | "hyperapp": "^2.0.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/hash.js: -------------------------------------------------------------------------------- 1 | function doHash(str, seed) { 2 | let m = 0x5bd1e995; 3 | let r = 24; 4 | let h = seed ^ str.length; 5 | let length = str.length; 6 | let currentIndex = 0; 7 | 8 | while (length >= 4) { 9 | let k = UInt32(str, currentIndex); 10 | 11 | k = Umul32(k, m); 12 | k ^= k >>> r; 13 | k = Umul32(k, m); 14 | 15 | h = Umul32(h, m); 16 | h ^= k; 17 | 18 | currentIndex += 4; 19 | length -= 4; 20 | } 21 | 22 | switch (length) { 23 | case 3: 24 | h ^= UInt16(str, currentIndex); 25 | h ^= str.charCodeAt(currentIndex + 2) << 16; 26 | h = Umul32(h, m); 27 | break 28 | 29 | case 2: 30 | h ^= UInt16(str, currentIndex); 31 | h = Umul32(h, m); 32 | break 33 | 34 | case 1: 35 | h ^= str.charCodeAt(currentIndex); 36 | h = Umul32(h, m); 37 | break 38 | } 39 | 40 | h ^= h >>> 13; 41 | h = Umul32(h, m); 42 | h ^= h >>> 15; 43 | 44 | return h >>> 0; 45 | } 46 | 47 | function UInt32(str, pos) { 48 | return (str.charCodeAt(pos++)) + 49 | (str.charCodeAt(pos++) << 8) + 50 | (str.charCodeAt(pos++) << 16) + 51 | (str.charCodeAt(pos) << 24); 52 | } 53 | 54 | function UInt16(str, pos) { 55 | return (str.charCodeAt(pos++)) + 56 | (str.charCodeAt(pos++) << 8); 57 | } 58 | 59 | function Umul32(n, m) { 60 | n = n | 0; 61 | m = m | 0; 62 | let nlo = n & 0xffff; 63 | let nhi = n >>> 16; 64 | let res = ((nlo * m) + (((nhi * m) & 0xffff) << 16)) | 0; 65 | return res; 66 | } 67 | 68 | module.exports = doHash; 69 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const doHash = require('./hash'); 2 | const { h } = require('hyperapp'); 3 | 4 | let theme = {}; 5 | let globalCSS = ''; 6 | export const docCSS = {}; 7 | export const dangerChars = [ 8 | /&/g, 9 | //g, 11 | /"/g, 12 | /'/g, 13 | // /\//g, 14 | ]; 15 | 16 | export function escapeChars(str) { 17 | let output = String(str); 18 | dangerChars.forEach(char => (output = output.replace(char, ''))); 19 | return output; 20 | } 21 | 22 | export function joinTemplate(strings, keys, state) { 23 | let output = ''; 24 | 25 | strings.forEach((str, index) => { 26 | if (keys.length >= index) { 27 | let keyValue = keys[index]; 28 | 29 | if (typeof keyValue === 'function') { 30 | keyValue = escapeChars(keyValue(state || {}) || ''); 31 | } 32 | 33 | if (typeof keyValue === 'string' && docCSS[keyValue.replace('class-', '')]) { 34 | const hash = keyValue.replace('class-', ''); 35 | keyValue = joinTemplate(docCSS[hash].strings, docCSS[hash].keys, state); 36 | } 37 | 38 | output += str + (keyValue || ''); 39 | } else { 40 | output += str; 41 | } 42 | }); 43 | 44 | return output; 45 | } 46 | 47 | export function buildName(hash, isKeyframes) { 48 | return isKeyframes ? `animation-${hash}` : `class-${hash}`; 49 | } 50 | 51 | function buildClass(className, rawCSS) { 52 | return rawCSS !== '' ? ` 53 | .${buildName(className)} { 54 | ${rawCSS.replace('}', '').trim()} 55 | }` : ''; 56 | } 57 | 58 | function buildPseudo(className, rawCSS) { 59 | let output = ` 60 | ${rawCSS.trim()}`; 61 | output = output.replace('&', `.${buildName(className)}`); 62 | return output; 63 | } 64 | 65 | function buildQuery(className, rawCSS) { 66 | let output = rawCSS; 67 | 68 | const innerContent = output.substring(output.indexOf('{') + 1, output.lastIndexOf('}')); 69 | output = ` 70 | ${output.substring(0, output.indexOf('{') + 1)} 71 | ${buildCSS(className, innerContent).trim()} 72 | }`; 73 | 74 | return output; 75 | } 76 | 77 | function findRightEndBracketPosition(rawCSS, start, lastClose) { 78 | const openPos = rawCSS.indexOf('{', start); 79 | const closePos = rawCSS.indexOf('}', (lastClose || openPos) + 1); 80 | const secondOpenPos = rawCSS.indexOf('{', openPos + 1); 81 | 82 | if (secondOpenPos > closePos || secondOpenPos === -1) { 83 | return closePos; 84 | } 85 | 86 | return findRightEndBracketPosition(rawCSS, secondOpenPos, closePos); 87 | } 88 | 89 | function buildCSS(className, rawCSS) { 90 | let output = rawCSS; 91 | const rawPseudos = []; 92 | const rawQueries = []; 93 | 94 | const parsePseudos = () => { 95 | const start = output.indexOf('&'); 96 | const pseudo = output.substring(start, findRightEndBracketPosition(output, start) + 1); 97 | if (String(pseudo).indexOf('&') !== -1) { 98 | rawPseudos.push(pseudo); 99 | output = output.replace(pseudo, ''); 100 | parsePseudos(); 101 | } 102 | }; 103 | 104 | const parseQueries = () => { 105 | const start = output.indexOf('@media'); 106 | const query = output.substring(start, findRightEndBracketPosition(output, start) + 1); 107 | if (String(query).indexOf('@media') !== -1) { 108 | rawQueries.push(query); 109 | output = output.replace(query, ''); 110 | parseQueries(); 111 | } 112 | }; 113 | 114 | parseQueries(); 115 | parsePseudos(); 116 | 117 | output = buildClass(className, output.trim()); 118 | rawPseudos.forEach(pseudo => (output += buildPseudo(className, pseudo))); 119 | rawQueries.forEach(query => (output += buildQuery(className, query))); 120 | 121 | return output; 122 | } 123 | 124 | function buildKeyframes(hash, rawCSS) { 125 | return ` 126 | @-webkit-keyframes ${buildName(hash, true)} { 127 | ${rawCSS.trim()} 128 | } 129 | @keyframes ${buildName(hash, true)} { 130 | ${rawCSS.trim()} 131 | }`; 132 | } 133 | 134 | export function renderCSS() { 135 | let renderedCSS = ''; 136 | Object.keys(docCSS).forEach(classHash => (renderedCSS += docCSS[classHash].rendered)); 137 | return `${globalCSS}${renderedCSS}`; 138 | } 139 | 140 | function buildAndRenderCSS(strings, keys, state, isKeyframes) { 141 | const rawCSS = joinTemplate(strings, keys, state); 142 | const hash = doHash(rawCSS).toString(36); 143 | 144 | if (document.querySelector('#styles') === null) { 145 | const styleEl = document.createElement('style'); // eslint-disable-line 146 | styleEl.type = 'text/css'; 147 | styleEl.id = 'styles'; 148 | 149 | document.head.appendChild(styleEl); 150 | } 151 | 152 | if (!docCSS[hash]) { 153 | if (isKeyframes) { 154 | docCSS[hash] = { rendered: buildKeyframes(hash, rawCSS), strings, keys }; 155 | } else { 156 | docCSS[hash] = { rendered: buildCSS(hash, rawCSS), strings, keys }; 157 | } 158 | 159 | document.querySelector('#styles').innerHTML = renderCSS(); 160 | } 161 | 162 | return buildName(hash, isKeyframes); 163 | } 164 | 165 | function makeKeyframes(strings, ...keys) { 166 | return buildAndRenderCSS(strings, keys, { theme }, true); 167 | } 168 | 169 | function appendChildren(children, el) { 170 | children.forEach((child) => { 171 | if (typeof child === 'string' || typeof child === 'number') { 172 | el.appendChild(document.createTextNode(String(child))); // eslint-disable-line 173 | } else if (Array.isArray(child)) { 174 | appendChildren(child, el); 175 | } else { 176 | el.appendChild(child); 177 | } 178 | }); 179 | 180 | return el; 181 | } 182 | 183 | function makeElement(tag) { 184 | return (strings, ...keys) => (...inputChildren) => { 185 | const inputProps = inputChildren[0]; 186 | const notProps = (typeof inputProps !== 'object' 187 | || Array.isArray(inputProps) 188 | || inputProps.length === 0 189 | || (inputProps.tagName && true || false) 190 | || (inputProps.nodeName && true || false)); 191 | const elProps = notProps ? {} : inputProps; 192 | const specifiedProps = elProps.props || {}; 193 | let children = (notProps ? inputChildren : inputChildren.slice(1)) || []; 194 | 195 | if (Array.isArray(children[0])) { 196 | children = children[0]; 197 | } 198 | 199 | const className = buildAndRenderCSS(strings, keys, Object.assign({}, { 200 | theme 201 | }, elProps, specifiedProps)); 202 | const newProps = Object.assign({}, elProps, { class: className }); 203 | delete newProps.children; 204 | 205 | return h(tag, newProps, children); 206 | }; 207 | } 208 | 209 | export default function styled(el) { 210 | return (strings, ...keys) => { 211 | const className = buildAndRenderCSS(strings, keys, { theme }); 212 | 213 | if (el.classList) { 214 | el.classList.add(className); 215 | } else { 216 | el.className += ` ${className}`; // eslint-disable-line 217 | } 218 | 219 | return el; 220 | }; 221 | } 222 | 223 | export const setTheme = styled.setTheme = selectedTheme => (theme = Object.assign({}, selectedTheme)); 224 | export const css = styled.css = (strings, ...keys) => buildAndRenderCSS(strings, keys, { theme }); 225 | export const injectGlobal = styled.injectGlobal = (strings, ...keys) => { 226 | globalCSS += joinTemplate(strings, keys, { theme }); 227 | }; 228 | 229 | export const tags = styled.tags = ['a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 230 | 'base', 'bdi', 'bdo', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 231 | 'cite', 'code', 'col', 'colgroup', 'command', 'datalist', 'dd', 'del', 'details', 232 | 'dfn', 'div', 'dl', 'doctype', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 233 | 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 234 | 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen', 'label', 235 | 'legend', 'li', 'link', 'main', 'map', 'mark', 'menu', 'meta', 'meter', 'nav', 236 | 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'pre', 237 | 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select', 238 | 'small', 'source', 'span', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 239 | 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 240 | 'u', 'ul', 'var', 'video', 'wbr']; 241 | 242 | export const presets = styled.presets = { 243 | mobile: '(min-width: 400px)', 244 | Mobile: '@media (min-width: 400px)', 245 | phablet: '(min-width: 550px)', 246 | Phablet: '@media (min-width: 550px)', 247 | tablet: '(min-width: 750px)', 248 | Tablet: '@media (min-width: 750px)', 249 | desktop: '(min-width: 1000px)', 250 | Desktop: '@media (min-width: 1000px)', 251 | hd: '(min-width: 1200px)', 252 | Hd: '@media (min-width: 1200px)', 253 | }; 254 | 255 | styled.tags.forEach(tag => (styled[tag] = makeElement(tag))); 256 | export const keyframes = styled.keyframes = makeKeyframes; 257 | -------------------------------------------------------------------------------- /src/test.index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilentCicero/hyperapp-styled-components/3df4677243e07b4cc90e0a18bfd34678e865f873/src/test.index.js --------------------------------------------------------------------------------