├── .gitignore ├── .npmignore ├── History.md ├── Readme.md ├── circle.yml ├── example.js ├── examples ├── client.js ├── server.js └── ssr.js ├── index.js ├── package.json ├── rollup.config.js └── test └── vcom.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /npm-debug.log 3 | /.DS_Store 4 | /dist 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | support 2 | test 3 | examples 4 | *.sock 5 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 6.0.13 / 2016-10-23 3 | ================== 4 | 5 | * bump sun and add examples 6 | 7 | 6.0.12 / 2016-10-19 8 | ================== 9 | 10 | * update sun 11 | 12 | 6.0.11 / 2016-10-19 13 | ================== 14 | 15 | * bump deps to add key attribute 16 | 17 | 6.0.10 / 2016-10-18 18 | ================== 19 | 20 | * bump deps 21 | 22 | 6.0.9 / 2016-10-16 23 | ================== 24 | 25 | * bump deps 26 | 27 | 6.0.8 / 2016-10-10 28 | ================== 29 | 30 | * bump deps 31 | 32 | 6.0.7 / 2016-10-06 33 | ================== 34 | 35 | * fix onMount and onUnmount to only fire when 36 | the element is added and removed 37 | 38 | 6.0.6 / 2016-10-03 39 | ================== 40 | 41 | * add mount and unmount hooks 42 | 43 | 6.0.5 / 2016-10-03 44 | ================== 45 | 46 | * bump deps 47 | 48 | 6.0.4 / 2016-09-28 49 | ================== 50 | 51 | * support server-side rendering and update examples 52 | 53 | 6.0.3 / 2016-09-26 54 | ================== 55 | 56 | * bump sun 57 | * use a peer dependency for preact 58 | * update readme 59 | 60 | 6.0.2 / 2016-09-23 61 | ================== 62 | 63 | * support re-writing pure vnodes 64 | 65 | 6.0.1 / 2016-09-23 66 | ================== 67 | 68 | * fix package.json 69 | 70 | 6.0.0 / 2016-09-23 71 | ================== 72 | 73 | * Switch main to the entry instead of the distribution 74 | 75 | 5.0.3 / 2016-09-22 76 | ================== 77 | 78 | * ensure that latest sun is present in the build 79 | * bump dep, update example, fix circle 80 | * bump package 81 | 82 | 5.0.2 / 2016-09-21 83 | ================== 84 | 85 | * bump sun to remove 2nd preact 86 | 87 | 5.0.1 / 2016-09-21 88 | ================== 89 | 90 | * fresh node_modules install 91 | 92 | 5.0.0 / 2016-09-21 93 | ================== 94 | 95 | * vcom.CSS(...) used to inject views with css, with vcom.Stylesheet is an alias for Afro 96 | 97 | 4.0.1 / 2016-09-21 98 | ================== 99 | 100 | * bump afro and fix internal class to hash renaming 101 | 102 | 4.0.0 / 2016-09-19 103 | ================== 104 | 105 | * update deps 106 | * BREAKING: socrates key change. see matthewmueller/socrates 107 | 108 | 3.0.3 / 2016-09-16 109 | ================== 110 | 111 | * BREAKING: Stylesheet => CSS 112 | 113 | 3.0.2 / 2016-09-16 114 | ================== 115 | 116 | * BREAKING: bump alley 117 | 118 | 3.0.1 / 2016-09-16 119 | ================== 120 | 121 | * BREAKING: pass in effects directly instead of send 122 | * BREAKING: rename Render to render 123 | 124 | 3.0.0 / 2016-09-16 125 | ================== 126 | 127 | * BREAKING: Capitalize all the exports 128 | * BREAKING: New API for alley 129 | 130 | 2.0.9 / 2016-09-16 131 | ================== 132 | 133 | * bump deps 134 | 135 | 2.0.8 / 2016-09-14 136 | ================== 137 | 138 | * update deps, skip walk if no vnode 139 | 140 | 2.0.7 / 2016-09-01 141 | ================== 142 | 143 | * bump socrates 144 | 145 | 2.0.6 / 2016-09-01 146 | ================== 147 | 148 | * fix: sun and add a test 149 | 150 | 2.0.5 / 2016-09-01 151 | ================== 152 | 153 | * support passing objects in as styles 154 | 155 | 2.0.4 / 2016-08-30 156 | ================== 157 | 158 | * bump sun 159 | 160 | 2.0.3 / 2016-08-29 161 | ================== 162 | 163 | * bump afro 164 | 165 | 2.0.2 / 2016-08-29 166 | ================== 167 | 168 | * bump afro 169 | 170 | 171 | 2.0.1 / 2016-08-29 172 | ================== 173 | 174 | * bump afro 175 | 176 | 2.0.0 / 2016-08-29 177 | ================== 178 | 179 | * complete solution for virtual components 180 | 181 | 1.0.5 / 2016-08-24 182 | ================== 183 | 184 | * bump deps, support styles(...) wrapping 185 | 186 | 1.0.4 / 2016-08-12 187 | ================== 188 | 189 | * bump afro 190 | 191 | 1.0.3 / 2016-08-12 192 | ================== 193 | 194 | * bump afro 195 | 196 | 1.0.2 / 2016-08-12 197 | ================== 198 | 199 | * actually update dist 200 | 201 | 1.0.1 / 2016-08-12 202 | ================== 203 | 204 | * bump afro 205 | 206 | 1.0.0 / 2010-01-03 207 | ================== 208 | 209 | * Initial release 210 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # vcom 3 | 4 | Everything you need to create functional, virtual [Preact](https://github.com/developit/preact) Components with CSS, HTML, and JS. 5 | 6 | > Note: if you want to use this standalone, please use dist/vcom.js. Otherwise you'll want to use babel with es2015 or buble to transpile this in your application, either as a global transform or after the build step. 7 | 8 | ## Example 9 | 10 | ```js 11 | /** 12 | * Modules 13 | */ 14 | 15 | const { HTML, CSS, render } = require('./dist/vcom.js') 16 | 17 | /** 18 | * Styles 19 | */ 20 | 21 | const css = CSS(` 22 | .box { 23 | text-align: center; 24 | font-size: 10rem; 25 | background: blue; 26 | padding: 50px; 27 | color: white; 28 | } 29 | `) 30 | 31 | /** 32 | * HTML 33 | */ 34 | 35 | const { div } = HTML 36 | 37 | /** 38 | * Render 39 | */ 40 | 41 | const App = (props) => ( 42 | div.class('box')('welcome') 43 | ) 44 | 45 | /** 46 | * Render to DOM 47 | */ 48 | 49 | render(css(App), document.body) 50 | ``` 51 | 52 | ## Installation 53 | 54 | ```bash 55 | npm install vcom 56 | ``` 57 | 58 | ## License 59 | 60 | MIT 61 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | post: 3 | - wget https://saucelabs.com/downloads/sc-latest-linux.tar.gz 4 | - tar -xzf sc-latest-linux.tar.gz 5 | 6 | test: 7 | override: 8 | - cd sc-*-linux && ./bin/sc --user $SAUCE_USERNAME --api-key $SAUCE_ACCESS_KEY --readyfile ~/sauce_is_ready: 9 | background: true 10 | # Wait for tunnel to be ready 11 | - while [ ! -e ~/sauce_is_ready ]; do sleep 1; done 12 | - python -m hello.hello_app: 13 | background: true 14 | # Wait for app to be ready 15 | - curl --retry 10 --retry-delay 2 -v http://localhost:5000 16 | # Run selenium tests 17 | - nosetests 18 | post: 19 | - killall --wait sc # wait for Sauce Connect to close the tunnel 20 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Modules 3 | */ 4 | 5 | const { HTML, CSS, render } = require('./dist/vcom.js') 6 | 7 | /** 8 | * Styles 9 | */ 10 | 11 | const css = CSS(` 12 | .box { 13 | text-align: center; 14 | font-size: 10rem; 15 | background: blue; 16 | padding: 50px; 17 | color: white; 18 | } 19 | `) 20 | 21 | /** 22 | * HTML 23 | */ 24 | 25 | const { div } = HTML 26 | 27 | /** 28 | * Render 29 | */ 30 | 31 | const App = (props) => ( 32 | div.class('box')('welcome') 33 | ) 34 | 35 | /** 36 | * Render to DOM 37 | */ 38 | 39 | render(css(App), document.body) 40 | -------------------------------------------------------------------------------- /examples/client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | let { HTML, render, Store } = require('..') 6 | let { div, h1 } = HTML 7 | let store = Store() 8 | 9 | const App = ({ url }) => ( 10 | div.class('app').onMount(load).onUnmount(unload).onClick(route)( 11 | url === '/' && h1.key('/').onMount(root).onUnmount(unroot)(`${url}`), 12 | url === '/name' && h1.key('name').onMount(name).onUnmount(unname)(`${url}`), 13 | h1.onMount(consistent).onUnmount(unconsistent)('consistent') 14 | ) 15 | ) 16 | 17 | function load (el, send) { 18 | console.log('LOAD') 19 | } 20 | function unload (el, send) { 21 | console.log('UNLOAD') 22 | } 23 | 24 | function root (el, send) { 25 | console.log('ROOT') 26 | } 27 | 28 | function unroot (el, send) { 29 | console.log('UNROOT') 30 | } 31 | 32 | function name (el, send) { 33 | console.log('NAME') 34 | } 35 | 36 | function unname (el, send) { 37 | console.log('UNNAME') 38 | } 39 | 40 | function consistent (el, send) { 41 | console.log('CONSISTENT') 42 | } 43 | 44 | function unconsistent (el, send) { 45 | console.log('UNCONSISTENT') 46 | } 47 | 48 | function route () { 49 | store().url === '/' 50 | ? store('set:url', '/name') 51 | : store('set:url', '/') 52 | } 53 | 54 | store('set:url', '/') 55 | 56 | render(App, document.body, { 57 | store 58 | }) 59 | 60 | // setInterval(function () { 61 | // i-- 62 | // render(App({ name: 'Matt' }), document.body, { 63 | // root: document.body.lastChild, 64 | // effects, 65 | // store 66 | // }) 67 | // }, 500) 68 | 69 | // function mount (el, send) { 70 | // console.log('mounting!') 71 | // } 72 | 73 | // function unmount (el, send) { 74 | // console.log('unmounting!') 75 | // } 76 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | let render = require('preact-render-to-string') 6 | let { CSS, HTML } = require('..') 7 | let { div, h1, style } = HTML 8 | 9 | let css = CSS(` 10 | header { 11 | background: skyblue; 12 | } 13 | 14 | header:hover { 15 | background: yellow 16 | } 17 | `) 18 | 19 | const App = ({ name }) => ( 20 | div.class('app')( 21 | style.type('text/css')(css()), 22 | h1.class(css.header)(`hi ${name}!`) 23 | ) 24 | ) 25 | 26 | let html = render(App({ name: 'Matt' })) 27 | console.log(html) 28 | -------------------------------------------------------------------------------- /examples/ssr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | let string = require('preact-render-to-string') 6 | let { CSS, HTML, render } = require('..') 7 | 8 | let css = CSS(` 9 | .header { 10 | background: skyblue; 11 | } 12 | 13 | .header:hover { 14 | background: yellow 15 | } 16 | `) 17 | 18 | const { div, style, h1 } = HTML 19 | 20 | const App = ({ name }) => ( 21 | div.class('app')( 22 | style.type('text/css')(css()), 23 | h1.class('header').onClick(hi)(`hi ${name}!`) 24 | ) 25 | ) 26 | 27 | function hi () { 28 | console.log('hi!') 29 | } 30 | 31 | console.log(string(css(App({ name: 'Matt' })))) 32 | document.body.innerHTML = string(css(App({ name: 'matt' }))) 33 | 34 | render(css(App({ name: 'Matt' })), document.body, { 35 | root: document.body.lastChild 36 | }) 37 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Module Dependencies 5 | */ 6 | 7 | const Socrates = require('socrates') 8 | const Stylesheet = require('afro') 9 | const Effects = require('alley') 10 | const preact = require('preact') 11 | const rsplit = /\.?([^\s]+)/g 12 | const sun = require('sun') 13 | 14 | /** 15 | * Defer with fallback 16 | */ 17 | 18 | let resolved = typeof Promise !== 'undefined' && Promise.resolve() 19 | const defer = resolved ? f => { resolved.then(f) } : setTimeout 20 | 21 | /** 22 | * Initialize the vcom object 23 | */ 24 | 25 | const vcom = module.exports = {} 26 | 27 | /** 28 | * DOM 29 | */ 30 | 31 | vcom.HTML = sun 32 | 33 | /** 34 | * Attach CSS 35 | */ 36 | 37 | vcom.CSS = CSS 38 | 39 | /** 40 | * Store 41 | */ 42 | 43 | vcom.Store = Socrates 44 | 45 | /** 46 | * Effects 47 | */ 48 | 49 | vcom.Effects = Effects 50 | 51 | /** 52 | * Stylesheet 53 | */ 54 | 55 | vcom.Stylesheet = Stylesheet 56 | 57 | /** 58 | * Render 59 | */ 60 | 61 | vcom.render = Render 62 | 63 | /** 64 | * Render 65 | */ 66 | 67 | function Render (renderable, parent, { effects, store, css, root } = {}) { 68 | let styles = typeof css === 'object' ? (key) => css[key] : css 69 | let transform = Transform({ css: styles, effects, rehydrating: !!root }) 70 | 71 | root = root || parent.lastChild 72 | 73 | function render () { 74 | let state = typeof store === 'function' ? store() : store 75 | let vdom = typeof renderable === 'function' 76 | ? transform(renderable(state)) 77 | : transform(renderable) 78 | 79 | root = preact.render(vdom, parent, root) 80 | return root 81 | } 82 | 83 | // subscribe to updates 84 | if (store && typeof store === 'function') { 85 | store.subscribe(() => defer(render)) 86 | } 87 | 88 | return render() 89 | } 90 | 91 | /** 92 | * CSS transform 93 | */ 94 | 95 | function CSS () { 96 | let sheet = Stylesheet.apply(null, arguments) 97 | return function css (render) { 98 | if (!arguments.length) { 99 | return sheet.apply(null, arguments) 100 | } else if (typeof render === 'function') { 101 | return Stylize(render, sheet) 102 | } else if (render.nodeName) { 103 | const styles = Styles(sheet) 104 | return walk(render, node => styles(node)) 105 | } else { 106 | sheet = sheet.apply(null, arguments) 107 | return css 108 | } 109 | } 110 | } 111 | 112 | /** 113 | * Stylize the render 114 | */ 115 | 116 | function Stylize (Render, css) { 117 | const styles = Styles(css) 118 | return function render () { 119 | const vnodes = Render.apply(Render, arguments) 120 | if (!vnodes) return vnodes 121 | return walk(vnodes, node => styles(node)) 122 | } 123 | } 124 | 125 | /** 126 | * Transform vnodes 127 | */ 128 | 129 | function Transform ({ css, effects, rehydrating }) { 130 | let actions = Actions(effects) 131 | let styles = Styles(css) 132 | let mounts = Mounts({ rehydrating }) 133 | return function transform (vnode) { 134 | return walk(vnode, node => { 135 | mounts(node) 136 | if (css) styles(node) 137 | if (effects) actions(node) 138 | return node 139 | }) 140 | } 141 | } 142 | 143 | /** 144 | * Render the classnames 145 | */ 146 | 147 | function Styles (css) { 148 | return function styles (node) { 149 | let attrs = node.attributes 150 | if (!attrs || !attrs.class) return node 151 | attrs.class = selectors(attrs.class) 152 | .map(cls => css[cls] || cls) 153 | .join(' ') 154 | return node 155 | } 156 | } 157 | 158 | /** 159 | * Actions 160 | */ 161 | 162 | function Actions (effects) { 163 | return function actions (node) { 164 | let attrs = node.attributes 165 | if (!attrs) return node 166 | for (let attr in attrs) { 167 | if (typeof attrs[attr] !== 'function') continue 168 | let fn = attrs[attr] 169 | attrs[attr] = (e) => fn(e, effects.send) 170 | } 171 | return node 172 | } 173 | } 174 | 175 | /** 176 | * Mounts 177 | */ 178 | 179 | function Mounts ({ rehydrating }) { 180 | // if we're rehydrating from the 181 | // server, we'll want to run hooks 182 | // at least once on the client 183 | let firstMount = rehydrating 184 | 185 | return function mounts (node) { 186 | let attrs = node.attributes 187 | if (!attrs) return 188 | else if (!attrs.onMount && !attrs.onUnmount) return 189 | 190 | // create a mount using the ref 191 | let ref = node.attributes && node.attributes.ref 192 | node.attributes.ref = (el, send) => { 193 | const parent = el && el.parentNode 194 | if (attrs.onMount && el && (!parent || firstMount)) { 195 | firstMount = false 196 | // when initially mounted, el will 197 | // exist but there won't be a parent 198 | // we want to defer to ensure that 199 | // the dimensions have been calculated 200 | defer(function () { attrs.onMount(el, send) }) 201 | } else if (attrs.onUnmount && !el && !parent) { 202 | // when unmounting, both el and 203 | // the parent will be null. 204 | defer(function () { attrs.onUnmount(null, send) }) 205 | } 206 | 207 | // call original ref, if there is one 208 | ref && ref(el, send) 209 | } 210 | } 211 | } 212 | 213 | /** 214 | * Walk the vnode tree 215 | */ 216 | 217 | function walk (vnode, fn) { 218 | if (!vnode) return vnode 219 | fn(vnode) 220 | if (!vnode.children) return vnode 221 | vnode.children.map(child => walk(child, fn)) 222 | return vnode 223 | } 224 | 225 | /** 226 | * Split into separate selectors 227 | */ 228 | 229 | function selectors (selectors) { 230 | const arr = [] 231 | selectors.replace(rsplit, (m, s) => arr.push(s)) 232 | return arr.filter(selector => !!selector) 233 | } 234 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vcom", 3 | "version": "6.0.13", 4 | "description": "Everything you need to create virtual components with CSS, HTML, and JS", 5 | "keywords": [ 6 | "preact", 7 | "virtual", 8 | "dom", 9 | "css", 10 | "js" 11 | ], 12 | "author": "Matthew Mueller ", 13 | "repository": "MatthewMueller/vcom", 14 | "dependencies": { 15 | "afro": "3.0.1", 16 | "alley": "2.0.2", 17 | "socrates": "4.0.1", 18 | "sun": "1.1.4" 19 | }, 20 | "devDependencies": { 21 | "babel-eslint": "7.0.0", 22 | "beefy": "2.1.8", 23 | "browserify": "13.1.1", 24 | "bubleify": "0.6.0", 25 | "devtool": "2.3.0", 26 | "disc": "1.3.2", 27 | "lru-fast": "0.1.0", 28 | "mocha": "3.1.2", 29 | "murmur.js": "1.0.0", 30 | "popsicle": "^8.2.0", 31 | "preact-render-to-string": "3.2.1", 32 | "rollup": "^0.36.0", 33 | "rollup-plugin-buble": "^0.14.0", 34 | "rollup-plugin-commonjs": "^5.0.4", 35 | "rollup-plugin-json": "2.0.2", 36 | "rollup-plugin-node-resolve": "^2.0.0", 37 | "snazzy": "5.0.0", 38 | "uglifyjs": "2.4.10" 39 | }, 40 | "peerDependencies": { 41 | "preact": "*" 42 | }, 43 | "main": "index.js", 44 | "minified:main": "dist/vcom.min.js", 45 | "scripts": { 46 | "build": "npm run -s transpile && npm run -s minify", 47 | "transpile": "rollup -c rollup.config.js -f umd -n $npm_package_name -m -o dist/vcom.js index.js", 48 | "minify": "uglifyjs dist/vcom.js -cm -o $npm_package_minified_main -p relative --in-source-map dist/vcom.js.map --source-map ${npm_package_minified_main}.map", 49 | "example:client": "budo examples/client.js --open --live -- -t [ bubleify ]", 50 | "example:data": "budo examples/data.js --open --live -- -t [ bubleify ]", 51 | "example:scroll": "budo examples/scroll.js --open --live -- -t [ bubleify ]", 52 | "example:server": "budo examples/server.js --open --live -- -t [ bubleify ]", 53 | "example:ssr-html": "budo examples/ssr-html.js --open --live -- -t [ bubleify ]", 54 | "example:ssr": "budo examples/ssr.js --open --live -- -t [ bubleify ]", 55 | "example:zilch": "budo examples/zilch.js --open --live -- -t [ bubleify ]", 56 | "disc": "browserify -t [ bubleify ] --full-paths index.js | discify --open", 57 | "example": "budo example.js --open --live -- -t [ bubleify ]", 58 | "test": "devtool node_modules/mocha/bin/_mocha -qc -- test/vcom.js", 59 | "prepublish": "npm run build", 60 | "lint": "snazzy", 61 | "pretest": "npm run lint" 62 | }, 63 | "standard": { 64 | "parser": "babel-eslint", 65 | "globals": [ 66 | "describe", 67 | "it", 68 | "before", 69 | "beforeEach", 70 | "after", 71 | "afterEach" 72 | ] 73 | } 74 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import buble from 'rollup-plugin-buble' 4 | import json from 'rollup-plugin-json' 5 | 6 | export default { 7 | exports: 'default', 8 | useStrict: false, 9 | external: [], 10 | plugins: [ 11 | json(), 12 | nodeResolve({ 13 | main: true 14 | }), 15 | commonjs({ 16 | include: '**/*' 17 | }), 18 | buble({ 19 | exclude: 'node_modules' 20 | }) 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /test/vcom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | const vcom = require('../index.js') 6 | const assert = require('assert') 7 | const { h } = require('preact') 8 | 9 | /** 10 | * Tests 11 | * 12 | * - See sun and afro's tests for a more comprehensive suite 13 | */ 14 | 15 | describe('vcom', function () { 16 | it('should ensure that stylesheet is preset', function () { 17 | assert.ok(vcom.CSS) 18 | }) 19 | 20 | it('should check stylesheet is afro', function () { 21 | let css = vcom.Stylesheet(` 22 | .landing { background: blue; } 23 | `) 24 | 25 | assert.equal(css(), '._1nxhvta { background: blue; }') 26 | }) 27 | 28 | describe('HTML', () => { 29 | it('should ensure that the HTML attributes are attached', function () { 30 | assert.ok(vcom.HTML.div) 31 | assert.ok(vcom.HTML.span) 32 | assert.ok(vcom.HTML.h1) 33 | assert.ok(vcom.HTML.a) 34 | }) 35 | 36 | it('should work vnode children', () => { 37 | let d = vcom.HTML.div([ 38 | h('h2', { class: 'blue' }, [ 39 | h('strong', {}, [ 40 | 'hi there!' 41 | ]) 42 | ]) 43 | ]) 44 | assert.equal(r(d), '

hi there!

') 45 | }) 46 | }) 47 | 48 | describe('CSS', function () { 49 | it('should support passing a function in', function () { 50 | let css = vcom.Stylesheet(` 51 | .theme { color: red; } 52 | .landing { background: blue; } 53 | `) 54 | 55 | let a = vcom.HTML.a.class('theme landing')('hi') 56 | vcom.render(a, document.body, { css }) 57 | let el = document.querySelector('a') 58 | assert.equal(el.className, '_im3wl1 _1nxhvta') 59 | }) 60 | 61 | it('should support rewriting render functions', function () { 62 | let css = vcom.CSS(` 63 | .theme { color: red; } 64 | .landing { background: blue; } 65 | `) 66 | 67 | function render (props) { 68 | return vcom.HTML.a.class('theme landing')('hi') 69 | } 70 | 71 | vcom.render(css(render), document.body) 72 | let el = document.querySelector('a') 73 | assert.equal(el.className, '_im3wl1 _1nxhvta') 74 | }) 75 | 76 | it('should support passing direct vnodes in', () => { 77 | let css = vcom.CSS(` 78 | .theme { color: red; } 79 | .landing { background: blue; } 80 | `) 81 | 82 | function render (props) { 83 | return vcom.HTML.a.class('theme landing')('hi') 84 | } 85 | 86 | vcom.render(css(render({})), document.body) 87 | let el = document.querySelector('a') 88 | assert.equal(el.className, '_im3wl1 _1nxhvta') 89 | }) 90 | 91 | it('should write out CSS when there are no arguments', () => { 92 | let css = vcom.CSS(` 93 | .theme { color: red; } 94 | .landing { background: blue; } 95 | `) 96 | 97 | assert.equal(css(), `._im3wl1 { color: red; } 98 | ._1nxhvta { background: blue; }`) 99 | }) 100 | }) 101 | 102 | describe('send', function () { 103 | it('should pass send in', function (done) { 104 | let { send, on } = vcom.Effects() 105 | 106 | on(function (payload, next) { 107 | assert.deepEqual(payload, { 108 | type: 'hi', 109 | payload: null 110 | }) 111 | return done() 112 | }) 113 | 114 | let button = vcom.HTML.button.onClick(onClick)('hi') 115 | vcom.render(button, document.body, { effects: { send } }) 116 | function onClick (e, send) { 117 | assert.equal(e.type, 'click') 118 | send('hi') 119 | } 120 | document.querySelector('button').click() 121 | }) 122 | }) 123 | 124 | describe('server-side rendering', () => { 125 | it('should render both as as string and to dom', (done) => { 126 | const string = require('preact-render-to-string') 127 | const { CSS, HTML, render } = require('..') 128 | const { div, style, button } = HTML 129 | 130 | const css = CSS(` 131 | .header { 132 | background: skyblue; 133 | } 134 | 135 | .header:hover { 136 | background: yellow 137 | } 138 | `) 139 | 140 | const App = ({ name }) => ( 141 | div.class('app')( 142 | style.type('text/css')(css()), 143 | button.class('header').onClick(e => done())(`hi ${name}!`) 144 | ) 145 | ) 146 | 147 | const html = string(css(App({ name: 'Matt' }))) 148 | document.body.innerHTML = html 149 | 150 | assert.equal(document.body.innerHTML, `
`) 152 | 153 | render(css(App({ name: 'Mark' })), document.body, { 154 | root: document.body.lastChild 155 | }) 156 | 157 | assert.equal(document.body.innerHTML, `
`) 159 | 160 | document.querySelector('button').click() 161 | }) 162 | }) 163 | 164 | describe('mounts', () => { 165 | it('should mount and unmount', (done) => { 166 | document.body.innerHTML = '' 167 | 168 | let mounted = 0 169 | let unmounted = 0 170 | let i = 1 171 | 172 | const App = ({ name } = {}) => ( 173 | vcom.HTML.div( 174 | (i > 0 || i <= -1) && vcom.HTML.h1({ onMount: mount, onUnmount: unmount }).class('header')(`hi ${name}!`) 175 | ) 176 | ) 177 | 178 | function mount (el, send) { 179 | mounted++ 180 | assert.equal(el.nodeName, 'H1') 181 | } 182 | 183 | function unmount (el, send) { 184 | unmounted++ 185 | assert.equal(el.nodeName, 'H1') 186 | } 187 | 188 | render(App()) 189 | render(App()) 190 | render(App()) 191 | render(App()) 192 | 193 | // since we're deferring to allow 194 | // the dimensions to exist we need 195 | // to wait to check 196 | setTimeout(function () { 197 | assert.equal(mounted, 2) 198 | assert.equal(unmounted, 1) 199 | done() 200 | }, 30) 201 | 202 | function render (component) { 203 | i-- 204 | return vcom.render(component, document.body) 205 | } 206 | }) 207 | 208 | it('should pass send through when effects are present', (done) => { 209 | document.body.innerHTML = '' 210 | 211 | let effects = vcom.Effects() 212 | let mounted = 0 213 | let unmounted = 0 214 | let i = 1 215 | 216 | const App = ({ name } = {}) => ( 217 | vcom.HTML.div( 218 | (i > 0 || i <= -1) && vcom.HTML.h1({ onMount: mount, onUnmount: unmount }).class('header')(`hi ${name}!`) 219 | ) 220 | ) 221 | 222 | function mount (el, send) { 223 | mounted++ 224 | assert.equal(el.nodeName, 'H1') 225 | assert.equal(typeof send, 'function') 226 | } 227 | 228 | function unmount (el, send) { 229 | unmounted++ 230 | assert.equal(el.nodeName, 'H1') 231 | assert.equal(typeof send, 'function') 232 | } 233 | 234 | render(App()) 235 | render(App()) 236 | render(App()) 237 | render(App()) 238 | 239 | // since we're deferring to allow 240 | // the dimensions to exist we need 241 | // to wait to check 242 | setTimeout(function () { 243 | assert.equal(mounted, 2) 244 | assert.equal(unmounted, 1) 245 | done() 246 | }, 100) 247 | 248 | function render (component) { 249 | vcom.render(component, document.body, { effects }) 250 | i-- 251 | } 252 | }) 253 | 254 | it('should be consistent even if there was already HTML there (from server)', (done) => { 255 | document.body.innerHTML = '

hi undefined!

' 256 | 257 | let mounted = 0 258 | let unmounted = 0 259 | let i = 1 260 | 261 | const App = ({ name } = {}) => ( 262 | vcom.HTML.div( 263 | (i > 0 || i <= -1) && vcom.HTML.h1({ onMount: mount, onUnmount: unmount }).class('header')(`hi ${name}!`) 264 | ) 265 | ) 266 | 267 | function mount (el, send) { 268 | mounted++ 269 | assert.equal(el.nodeName, 'H1') 270 | } 271 | 272 | function unmount (el, send) { 273 | unmounted++ 274 | assert.equal(el.nodeName, 'H1') 275 | } 276 | 277 | // first one apply a root since it's 278 | // overwriting existing HTML 279 | render(App(), document.body.lastChild) 280 | render(App()) 281 | render(App()) 282 | render(App()) 283 | 284 | // since we're deferring to allow 285 | // the dimensions to exist we need 286 | // to wait to check 287 | setTimeout(function () { 288 | assert.equal(mounted, 2) 289 | assert.equal(unmounted, 1) 290 | done() 291 | }, 30) 292 | 293 | function render (component, root) { 294 | i-- 295 | return vcom.render(component, document.body, { root }) 296 | } 297 | }) 298 | }) 299 | }) 300 | 301 | function r (v) { 302 | document.body.innerHTML = '' 303 | vcom.render(v, document.body, { root: document.body.lastChild }) 304 | return document.body.innerHTML 305 | } 306 | --------------------------------------------------------------------------------