├── docs ├── .nojekyll ├── CNAME ├── codesandbox │ ├── get-started-ajax-ts │ │ ├── global.d.ts │ │ ├── sandbox.config.json │ │ ├── browser │ │ │ ├── index.ts │ │ │ ├── styles.css │ │ │ └── app.tsx │ │ ├── tsconfig.json │ │ ├── package.json │ │ └── webpack.config.js │ ├── get-started-compose-ts │ │ ├── global.d.ts │ │ ├── sandbox.config.json │ │ ├── browser │ │ │ ├── index.ts │ │ │ ├── styles.css │ │ │ ├── app.tsx │ │ │ └── BeerList.tsx │ │ ├── tsconfig.json │ │ ├── package.json │ │ └── webpack.config.js │ ├── get-started-events-ts │ │ ├── global.d.ts │ │ ├── sandbox.config.json │ │ ├── browser │ │ │ ├── index.ts │ │ │ ├── styles.css │ │ │ └── app.tsx │ │ ├── tsconfig.json │ │ ├── package.json │ │ └── webpack.config.js │ ├── get-started-init-ts │ │ ├── global.d.ts │ │ ├── sandbox.config.json │ │ ├── browser │ │ │ ├── index.ts │ │ │ ├── styles.css │ │ │ └── app.tsx │ │ ├── tsconfig.json │ │ ├── package.json │ │ └── webpack.config.js │ ├── get-started-routing-ts │ │ ├── global.d.ts │ │ ├── sandbox.config.json │ │ ├── browser │ │ │ ├── index.ts │ │ │ ├── routes.ts │ │ │ ├── styles.css │ │ │ ├── Beer.tsx │ │ │ ├── app.tsx │ │ │ └── BeerList.tsx │ │ ├── tsconfig.json │ │ ├── package.json │ │ └── webpack.config.js │ ├── demo-js │ │ ├── sandbox.config.json │ │ ├── .babelrc │ │ ├── browser │ │ │ └── index.jsx │ │ ├── package.json │ │ └── webpack.config.js │ ├── demo-ts │ │ ├── sandbox.config.json │ │ ├── tsconfig.json │ │ ├── browser │ │ │ └── index.tsx │ │ ├── package.json │ │ └── webpack.config.js │ ├── get-started-bindings-ts │ │ ├── global.d.ts │ │ ├── sandbox.config.json │ │ ├── browser │ │ │ ├── index.ts │ │ │ ├── styles.css │ │ │ └── app.tsx │ │ ├── tsconfig.json │ │ ├── package.json │ │ └── webpack.config.js │ ├── demo-no-jsx │ │ ├── sandbox.config.json │ │ ├── browser │ │ │ └── index.js │ │ ├── package.json │ │ └── webpack.config.js │ ├── demo-ts-routing │ │ ├── sandbox.config.json │ │ ├── tsconfig.json │ │ ├── browser │ │ │ └── index.tsx │ │ ├── package.json │ │ └── webpack.config.js │ ├── get-started-ajax │ │ ├── sandbox.config.json │ │ ├── .babelrc │ │ ├── browser │ │ │ ├── index.js │ │ │ ├── styles.css │ │ │ └── app.jsx │ │ ├── package.json │ │ └── webpack.config.js │ ├── get-started-compose │ │ ├── sandbox.config.json │ │ ├── .babelrc │ │ ├── browser │ │ │ ├── index.js │ │ │ ├── styles.css │ │ │ ├── app.jsx │ │ │ └── BeerList.jsx │ │ ├── package.json │ │ └── webpack.config.js │ ├── get-started-events │ │ ├── sandbox.config.json │ │ ├── .babelrc │ │ ├── browser │ │ │ ├── index.js │ │ │ ├── styles.css │ │ │ └── app.jsx │ │ ├── package.json │ │ └── webpack.config.js │ ├── get-started-init │ │ ├── sandbox.config.json │ │ ├── .babelrc │ │ ├── browser │ │ │ ├── index.js │ │ │ ├── styles.css │ │ │ └── app.jsx │ │ ├── package.json │ │ └── webpack.config.js │ ├── get-started-routing │ │ ├── sandbox.config.json │ │ ├── .babelrc │ │ ├── browser │ │ │ ├── index.js │ │ │ ├── routes.js │ │ │ ├── styles.css │ │ │ ├── Beer.jsx │ │ │ ├── app.jsx │ │ │ └── BeerList.jsx │ │ ├── package.json │ │ └── webpack.config.js │ └── get-started-bindings │ │ ├── sandbox.config.json │ │ ├── .babelrc │ │ ├── browser │ │ ├── index.js │ │ ├── styles.css │ │ └── app.jsx │ │ ├── package.json │ │ └── webpack.config.js ├── favicon.ico ├── .eslintrc ├── hyperdom-isometric-raster.png ├── coverpage.md ├── _sidebar.md ├── introduction.md ├── readme.markdown ├── index.html ├── getting-started.md └── api.md ├── browser.js ├── .travis.yml ├── .eslintignore ├── .exrc ├── merge.d.ts ├── windowEvents.d.ts ├── mount.d.ts ├── storeCache.d.ts ├── render.d.ts ├── serverRenderCache.d.ts ├── test ├── browser │ ├── mountHyperdom.d.ts │ ├── detect.d.ts │ ├── detect.js │ ├── karma.index.ts │ ├── mountHyperdom.js │ └── tsxSpec.tsx ├── server │ ├── hyperxSpec.ts │ ├── joinSpec.ts │ ├── jsdomSpec.ts │ ├── detachedRenderingSpec.ts │ ├── toHtmlSpec.ts │ └── storeCacheSpec.ts └── dummy-defs.d.ts ├── toHtml.d.ts ├── hyperx.d.ts ├── join.d.ts ├── viewComponent.js ├── .gitignore ├── toHtml.js ├── refreshAfter.js ├── join.js ├── hyperx.js ├── mapBinding.d.ts ├── isVdom.js ├── componentWidget.d.ts ├── propertyHook.js ├── .eslintrc ├── serverRenderCache.js ├── meta.js ├── listener.js ├── debuggingProperties.js ├── simplePromise.js ├── set.js ├── tslint.json ├── tsconfig.json ├── deprecations.js ├── binding.js ├── merge.js ├── LICENSE ├── index.js ├── vhtml.js ├── toVdom.js ├── storeCache.js ├── readme.md ├── domComponent.js ├── render.js ├── router.d.ts ├── sync.js ├── serverRender.js ├── refreshEventResult.js ├── windowEvents.js ├── prepareAttributes.js ├── mapBinding.js ├── xml.js ├── CODE_OF_CONDUCT.md ├── package.json ├── componentWidget.js ├── mount.js ├── karma.conf.js ├── component.js ├── bindModel.js ├── rendering.js └── gh-md-toc /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | hyperdom.org -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | window.hyperdom = require('.') 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | docs/codesandbox 2 | website-dist 3 | dist 4 | -------------------------------------------------------------------------------- /.exrc: -------------------------------------------------------------------------------- 1 | let g:ale_linters_ignore = {'typescript': ['eslint', 'tsserver']} 2 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-ajax-ts/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.css" 2 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-compose-ts/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.css" 2 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-events-ts/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.css" 2 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-init-ts/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.css" 2 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-routing-ts/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.css" 2 | -------------------------------------------------------------------------------- /merge.d.ts: -------------------------------------------------------------------------------- 1 | declare function merge (...args: any[]): any 2 | export = merge 3 | -------------------------------------------------------------------------------- /docs/codesandbox/demo-js/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node" 3 | } 4 | -------------------------------------------------------------------------------- /docs/codesandbox/demo-ts/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node" 3 | } 4 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-bindings-ts/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.css" 2 | -------------------------------------------------------------------------------- /docs/codesandbox/demo-no-jsx/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node" 3 | } 4 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/featurist/hyperdom/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /docs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "standard" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /docs/codesandbox/demo-js/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "hyperdom" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /docs/codesandbox/demo-ts-routing/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node" 3 | } 4 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-ajax-ts/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node" 3 | } 4 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-ajax/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node" 3 | } 4 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-compose/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node" 3 | } 4 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-events/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node" 3 | } 4 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-init-ts/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node" 3 | } 4 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-init/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node" 3 | } 4 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-routing/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node" 3 | } 4 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-bindings-ts/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node" 3 | } 4 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-bindings/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node" 3 | } 4 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-compose-ts/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node" 3 | } 4 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-events-ts/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node" 3 | } 4 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-routing-ts/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node" 3 | } 4 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-ajax/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "hyperdom" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-compose/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "hyperdom" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-events/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "hyperdom" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-init/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "hyperdom" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-routing/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "hyperdom" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /windowEvents.d.ts: -------------------------------------------------------------------------------- 1 | declare function windowEVents (...args: any[]): any 2 | export = windowEVents 3 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-bindings/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "hyperdom" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /mount.d.ts: -------------------------------------------------------------------------------- 1 | declare class Mount { 2 | public refreshify (...args: any[]): void 3 | } 4 | export = Mount 5 | -------------------------------------------------------------------------------- /storeCache.d.ts: -------------------------------------------------------------------------------- 1 | declare class StoreCache { 2 | public refreshify (): void 3 | } 4 | export = StoreCache 5 | -------------------------------------------------------------------------------- /render.d.ts: -------------------------------------------------------------------------------- 1 | declare const render: { 2 | _currentRender: any, 3 | refreshify: any, 4 | } 5 | export = render 6 | -------------------------------------------------------------------------------- /serverRenderCache.d.ts: -------------------------------------------------------------------------------- 1 | declare function serverRenderCache (...args: any[]): any 2 | export = serverRenderCache 3 | -------------------------------------------------------------------------------- /test/browser/mountHyperdom.d.ts: -------------------------------------------------------------------------------- 1 | declare function mountHyperdom (...args: any[]): any 2 | export = mountHyperdom 3 | -------------------------------------------------------------------------------- /docs/hyperdom-isometric-raster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/featurist/hyperdom/HEAD/docs/hyperdom-isometric-raster.png -------------------------------------------------------------------------------- /toHtml.d.ts: -------------------------------------------------------------------------------- 1 | import {Component, VdomFragment} from "./index" 2 | 3 | declare function toHtml (vdom: Component | VdomFragment): string 4 | export = toHtml 5 | -------------------------------------------------------------------------------- /hyperx.d.ts: -------------------------------------------------------------------------------- 1 | import {VdomFragment} from "./index" 2 | 3 | declare function hx (strings: TemplateStringsArray, ...keys: any[]): VdomFragment 4 | export = hx 5 | -------------------------------------------------------------------------------- /join.d.ts: -------------------------------------------------------------------------------- 1 | import {VdomFragment} from "./index" 2 | 3 | declare function join (fragments: VdomFragment[], separator: string): VdomFragment 4 | export = join 5 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-ajax/browser/index.js: -------------------------------------------------------------------------------- 1 | import hyperdom from 'hyperdom'; 2 | import App from './app'; 3 | 4 | hyperdom.append(document.body, new App()); 5 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-init/browser/index.js: -------------------------------------------------------------------------------- 1 | import hyperdom from 'hyperdom' 2 | import App from './app' 3 | 4 | hyperdom.append(document.body, new App()) 5 | -------------------------------------------------------------------------------- /viewComponent.js: -------------------------------------------------------------------------------- 1 | var Component = require('./component') 2 | 3 | module.exports = function (model) { 4 | return new Component(model, {viewComponent: true}) 5 | } 6 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-bindings/browser/index.js: -------------------------------------------------------------------------------- 1 | import hyperdom from 'hyperdom'; 2 | import App from './app'; 3 | 4 | hyperdom.append(document.body, new App()); 5 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-compose/browser/index.js: -------------------------------------------------------------------------------- 1 | import hyperdom from 'hyperdom'; 2 | import App from './app'; 3 | 4 | hyperdom.append(document.body, new App()); 5 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-events/browser/index.js: -------------------------------------------------------------------------------- 1 | import hyperdom from 'hyperdom'; 2 | import App from './app' 3 | 4 | hyperdom.append(document.body, new App()); 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.sw? 2 | .sw? 3 | /node_modules/ 4 | /npm-debug.log 5 | website-dist 6 | tags 7 | package-lock.json 8 | .DS_Store 9 | .idea 10 | .vscode 11 | /dist 12 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-ajax-ts/browser/index.ts: -------------------------------------------------------------------------------- 1 | import * as hyperdom from "hyperdom"; 2 | import App from "./app"; 3 | 4 | hyperdom.append(document.body, new App()); 5 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-events-ts/browser/index.ts: -------------------------------------------------------------------------------- 1 | import * as hyperdom from "hyperdom"; 2 | import App from "./app"; 3 | 4 | hyperdom.append(document.body, new App()); 5 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-init-ts/browser/index.ts: -------------------------------------------------------------------------------- 1 | import * as hyperdom from "hyperdom"; 2 | import App from "./app"; 3 | 4 | hyperdom.append(document.body, new App()); 5 | -------------------------------------------------------------------------------- /test/browser/detect.d.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line 2 | declare namespace detect { 3 | const pushState: boolean 4 | const historyBack: boolean 5 | } 6 | export = detect 7 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-bindings-ts/browser/index.ts: -------------------------------------------------------------------------------- 1 | import * as hyperdom from "hyperdom"; 2 | import App from "./app"; 3 | 4 | hyperdom.append(document.body, new App()); 5 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-compose-ts/browser/index.ts: -------------------------------------------------------------------------------- 1 | import * as hyperdom from "hyperdom"; 2 | import App from "./app"; 3 | 4 | hyperdom.append(document.body, new App()); 5 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-events/browser/styles.css: -------------------------------------------------------------------------------- 1 | :global(body) { 2 | font-family: "Helvetica Neue", Arial, sans-serif; 3 | } 4 | 5 | .hello { 6 | text-transform: uppercase; 7 | } 8 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-init/browser/styles.css: -------------------------------------------------------------------------------- 1 | :global(body) { 2 | font-family: "Helvetica Neue", Arial, sans-serif; 3 | } 4 | 5 | .hello { 6 | text-transform: uppercase; 7 | } 8 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-bindings-ts/browser/styles.css: -------------------------------------------------------------------------------- 1 | :global(body) { 2 | font-family: "Helvetica Neue", Arial, sans-serif; 3 | } 4 | 5 | .hello { 6 | text-transform: uppercase; 7 | } 8 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-events-ts/browser/styles.css: -------------------------------------------------------------------------------- 1 | :global(body) { 2 | font-family: "Helvetica Neue", Arial, sans-serif; 3 | } 4 | 5 | .hello { 6 | text-transform: uppercase; 7 | } 8 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-init-ts/browser/styles.css: -------------------------------------------------------------------------------- 1 | :global(body) { 2 | font-family: "Helvetica Neue", Arial, sans-serif; 3 | } 4 | 5 | .hello { 6 | text-transform: uppercase; 7 | } 8 | -------------------------------------------------------------------------------- /toHtml.js: -------------------------------------------------------------------------------- 1 | var vdomToHtml = require('vdom-to-html') 2 | var toVdom = require('./toVdom') 3 | 4 | module.exports = function (_vdom) { 5 | var vdom = toVdom(_vdom) 6 | return vdomToHtml(vdom) 7 | } 8 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-routing/browser/index.js: -------------------------------------------------------------------------------- 1 | import hyperdom from 'hyperdom'; 2 | import router from 'hyperdom/router'; 3 | import App from './app'; 4 | 5 | hyperdom.append(document.body, new App(), { router }); 6 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-routing-ts/browser/index.ts: -------------------------------------------------------------------------------- 1 | import * as hyperdom from "hyperdom"; 2 | import * as router from "hyperdom/router"; 3 | import App from "./app"; 4 | 5 | hyperdom.append(document.body, new App(), { router }); 6 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-routing/browser/routes.js: -------------------------------------------------------------------------------- 1 | import router from 'hyperdom/router'; 2 | 3 | export default { 4 | home: router.route('/'), 5 | beers: router.route('/beers'), 6 | beer: router.route('/beers/:id') 7 | }; 8 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-routing-ts/browser/routes.ts: -------------------------------------------------------------------------------- 1 | import * as router from "hyperdom/router"; 2 | 3 | export default { 4 | home: router.route("/"), 5 | beers: router.route("/beers"), 6 | beer: router.route("/beers/:id") 7 | }; 8 | -------------------------------------------------------------------------------- /refreshAfter.js: -------------------------------------------------------------------------------- 1 | var render = require('./render') 2 | var refreshEventResult = require('./refreshEventResult') 3 | 4 | module.exports = function (promise) { 5 | refreshEventResult(promise, render.currentRender().mount, {refresh: 'promise'}) 6 | } 7 | -------------------------------------------------------------------------------- /docs/coverpage.md: -------------------------------------------------------------------------------- 1 | # Hyperdom 2 | 3 | ![logo](hyperdom-isometric-raster.png ':size=120') 4 | 5 | A simple, fast, feature rich framework for building dynamic browser applications. 6 | 7 | [Get Started](getting-started) 8 | [GitHub](https://github.com/featurist/hyperdom) 9 | -------------------------------------------------------------------------------- /test/browser/detect.js: -------------------------------------------------------------------------------- 1 | var browser = require('detect-browser') 2 | 3 | var version = Number(/^(\d+)/.exec(browser.version)[1]) 4 | 5 | module.exports = { 6 | pushState: !!window.history && !!window.history.pushState, 7 | historyBack: !(browser.name === 'ie' && version <= 9) 8 | } 9 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-init-ts/browser/app.tsx: -------------------------------------------------------------------------------- 1 | import * as hyperdom from "hyperdom"; 2 | import { hello } from "./styles.css"; 3 | 4 | export default class App extends hyperdom.RenderComponent { 5 | render() { 6 | return

Hello from Hyperdom!

; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /join.js: -------------------------------------------------------------------------------- 1 | module.exports = function join (array, separator) { 2 | var output = [] 3 | for (var i = 0, l = array.length; i < l; i++) { 4 | var item = array[i] 5 | if (i > 0) { 6 | output.push(separator) 7 | } 8 | output.push(item) 9 | } 10 | return output 11 | } 12 | -------------------------------------------------------------------------------- /docs/codesandbox/demo-no-jsx/browser/index.js: -------------------------------------------------------------------------------- 1 | import hyperdom from 'hyperdom'; 2 | const h = require('hyperdom').html 3 | 4 | class App { 5 | render() { 6 | return h('div.content', 7 | h('h1', 'hello!') 8 | ) 9 | } 10 | } 11 | 12 | hyperdom.append(document.body, new App()); 13 | -------------------------------------------------------------------------------- /hyperx.js: -------------------------------------------------------------------------------- 1 | try { 2 | var hyperx = require('hyperx') 3 | } catch (e) { 4 | if (e.code === 'MODULE_NOT_FOUND') { 5 | throw new Error('to use hyperx with hyperdom you need to install the hyperx package') 6 | } 7 | throw e 8 | } 9 | 10 | module.exports = hyperx(require('./rendering').jsx) 11 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-init/browser/app.jsx: -------------------------------------------------------------------------------- 1 | import hyperdom from 'hyperdom' 2 | import {hello} from './styles.css' 3 | 4 | export default class App { 5 | render () { 6 | return ( 7 |
8 |

Hello from Hyperdom!

9 |
10 | ) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/codesandbox/demo-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "sourceMap": true, 5 | "lib": [ 6 | "es6", 7 | "dom" 8 | ], 9 | "jsx": "react", 10 | "jsxFactory": "hyperdom.jsx", 11 | "target": "ES6", 12 | "strict": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /mapBinding.d.ts: -------------------------------------------------------------------------------- 1 | interface IConversion { 2 | view (model: any): string 3 | 4 | model (view: string): any 5 | } 6 | 7 | type ConversionFn = (view: string) => any 8 | 9 | declare const mapBinding: (model: object, property: string, conversion: IConversion | ConversionFn) => any 10 | 11 | export = mapBinding 12 | -------------------------------------------------------------------------------- /docs/codesandbox/demo-ts-routing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "sourceMap": true, 5 | "lib": [ 6 | "es6", 7 | "dom" 8 | ], 9 | "jsx": "react", 10 | "jsxFactory": "hyperdom.jsx", 11 | "target": "ES6", 12 | "strict": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-ajax-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "sourceMap": true, 5 | "lib": [ 6 | "es6", 7 | "dom" 8 | ], 9 | "jsx": "react", 10 | "jsxFactory": "hyperdom.jsx", 11 | "target": "ES6", 12 | "strict": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-compose-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "sourceMap": true, 5 | "lib": [ 6 | "es6", 7 | "dom" 8 | ], 9 | "jsx": "react", 10 | "jsxFactory": "hyperdom.jsx", 11 | "target": "ES6", 12 | "strict": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-events-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "sourceMap": true, 5 | "lib": [ 6 | "es6", 7 | "dom" 8 | ], 9 | "jsx": "react", 10 | "jsxFactory": "hyperdom.jsx", 11 | "target": "ES6", 12 | "strict": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-init-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "sourceMap": true, 5 | "lib": [ 6 | "es6", 7 | "dom" 8 | ], 9 | "jsx": "react", 10 | "jsxFactory": "hyperdom.jsx", 11 | "target": "ES6", 12 | "strict": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-routing-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "sourceMap": true, 5 | "lib": [ 6 | "es6", 7 | "dom" 8 | ], 9 | "jsx": "react", 10 | "jsxFactory": "hyperdom.jsx", 11 | "strict": true, 12 | "target": "ES6" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /isVdom.js: -------------------------------------------------------------------------------- 1 | var virtualDomVersion = require('virtual-dom/vnode/version') 2 | 3 | module.exports = function (x) { 4 | var type = x.type 5 | if (type === 'VirtualNode' || type === 'VirtualText') { 6 | return x.version === virtualDomVersion 7 | } else { 8 | return type === 'Widget' || type === 'Thunk' 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-bindings-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "sourceMap": true, 5 | "lib": [ 6 | "es6", 7 | "dom" 8 | ], 9 | "jsx": "react", 10 | "jsxFactory": "hyperdom.jsx", 11 | "target": "ES6", 12 | "strict": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /componentWidget.d.ts: -------------------------------------------------------------------------------- 1 | import {VdomFragment} from "./index" 2 | 3 | type RenderFn = (this: T, ...args: any[]) => VdomFragment 4 | 5 | declare function componentWidget (state: T, vdom: VdomFragment | RenderFn): any 6 | declare function componentWidget (vdom: VdomFragment | RenderFn): any 7 | export = componentWidget 8 | -------------------------------------------------------------------------------- /propertyHook.js: -------------------------------------------------------------------------------- 1 | function PropertyHook (value) { 2 | this.value = value 3 | } 4 | 5 | PropertyHook.prototype.hook = function (element, property) { 6 | element[property] = this.value 7 | } 8 | 9 | PropertyHook.prototype.unhook = function (element, property) { 10 | delete element[property] 11 | } 12 | 13 | module.exports = PropertyHook 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:es5/no-es2015" 5 | ], 6 | "env": { 7 | "node": true, 8 | "browser": true 9 | }, 10 | "rules": { 11 | "no-console": "error", 12 | "no-prototype-builtins": 0 13 | }, 14 | "globals": { 15 | "Promise": true, 16 | "Set": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /serverRenderCache.js: -------------------------------------------------------------------------------- 1 | var render = require('./render') 2 | 3 | module.exports = function (key, loadFn) { 4 | var cache = render.currentRender().mount.serverRenderCache || new NoCache() 5 | return cache.cache(key, loadFn) 6 | } 7 | 8 | function NoCache () { 9 | } 10 | 11 | NoCache.prototype.cache = function (key, loadFn) { 12 | return loadFn() 13 | } 14 | -------------------------------------------------------------------------------- /test/browser/karma.index.ts: -------------------------------------------------------------------------------- 1 | // Entry point for karma tests. 2 | // Without this, karma+webpack generate full bundle for each test file. 3 | // Not only this x times slower (where x is the number of test files), 4 | // but it also breaks sourcemaps (breakpoints only work in tests, but not in the library code). 5 | 6 | import "./hyperdomSpec.ts" 7 | import "./routerSpec.ts" 8 | import "./tsxSpec.tsx" 9 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-ajax/browser/styles.css: -------------------------------------------------------------------------------- 1 | :global(body) { 2 | font-family: "Helvetica Neue", Arial, sans-serif; 3 | } 4 | 5 | .hello { 6 | text-transform: uppercase; 7 | } 8 | 9 | .beerList { 10 | border-collapse: collapse; 11 | } 12 | 13 | .beerList tr { 14 | border-bottom: 1px solid lightgrey; 15 | } 16 | 17 | .beerList td, 18 | .beerList th { 19 | padding: 10px; 20 | } 21 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-ajax-ts/browser/styles.css: -------------------------------------------------------------------------------- 1 | :global(body) { 2 | font-family: "Helvetica Neue", Arial, sans-serif; 3 | } 4 | 5 | .hello { 6 | text-transform: uppercase; 7 | } 8 | 9 | .beerList { 10 | border-collapse: collapse; 11 | } 12 | 13 | .beerList tr { 14 | border-bottom: 1px solid lightgrey; 15 | } 16 | 17 | .beerList td, 18 | .beerList th { 19 | padding: 10px; 20 | } 21 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-bindings/browser/styles.css: -------------------------------------------------------------------------------- 1 | :global(body) { 2 | font-family: "Helvetica Neue", Arial, sans-serif; 3 | } 4 | 5 | .hello { 6 | text-transform: uppercase; 7 | } 8 | 9 | .beerList { 10 | border-collapse: collapse; 11 | } 12 | 13 | .beerList tr { 14 | border-bottom: 1px solid lightgrey; 15 | } 16 | 17 | .beerList td, 18 | .beerList th { 19 | padding: 10px; 20 | } 21 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-compose/browser/styles.css: -------------------------------------------------------------------------------- 1 | :global(body) { 2 | font-family: "Helvetica Neue", Arial, sans-serif; 3 | } 4 | 5 | .hello { 6 | text-transform: uppercase; 7 | } 8 | 9 | .beerList { 10 | border-collapse: collapse; 11 | } 12 | 13 | .beerList tr { 14 | border-bottom: 1px solid lightgrey; 15 | } 16 | 17 | .beerList td, 18 | .beerList th { 19 | padding: 10px; 20 | } 21 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-routing/browser/styles.css: -------------------------------------------------------------------------------- 1 | :global(body) { 2 | font-family: "Helvetica Neue", Arial, sans-serif; 3 | } 4 | 5 | .hello { 6 | text-transform: uppercase; 7 | } 8 | 9 | .beerList { 10 | border-collapse: collapse; 11 | } 12 | 13 | .beerList tr { 14 | border-bottom: 1px solid lightgrey; 15 | } 16 | 17 | .beerList td, 18 | .beerList th { 19 | padding: 10px; 20 | } 21 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-compose-ts/browser/styles.css: -------------------------------------------------------------------------------- 1 | :global(body) { 2 | font-family: "Helvetica Neue", Arial, sans-serif; 3 | } 4 | 5 | .hello { 6 | text-transform: uppercase; 7 | } 8 | 9 | .beerList { 10 | border-collapse: collapse; 11 | } 12 | 13 | .beerList tr { 14 | border-bottom: 1px solid lightgrey; 15 | } 16 | 17 | .beerList td, 18 | .beerList th { 19 | padding: 10px; 20 | } 21 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-routing-ts/browser/styles.css: -------------------------------------------------------------------------------- 1 | :global(body) { 2 | font-family: "Helvetica Neue", Arial, sans-serif; 3 | } 4 | 5 | .hello { 6 | text-transform: uppercase; 7 | } 8 | 9 | .beerList { 10 | border-collapse: collapse; 11 | } 12 | 13 | .beerList tr { 14 | border-bottom: 1px solid lightgrey; 15 | } 16 | 17 | .beerList td, 18 | .beerList th { 19 | padding: 10px; 20 | } 21 | -------------------------------------------------------------------------------- /docs/codesandbox/demo-js/browser/index.jsx: -------------------------------------------------------------------------------- 1 | import hyperdom from 'hyperdom'; 2 | 3 | class App { 4 | constructor() { 5 | this.name = 'Sally' 6 | } 7 | 8 | render() { 9 | return
10 | 11 | 12 |
hi {this.name}
13 |
; 14 | } 15 | } 16 | 17 | hyperdom.append(document.body, new App()); 18 | -------------------------------------------------------------------------------- /docs/codesandbox/demo-no-jsx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "webpack-dev-server", 4 | "build": "webpack" 5 | }, 6 | "dependencies": { 7 | "hyperdom": "latest" 8 | }, 9 | "devDependencies": { 10 | "clean-webpack-plugin": "latest", 11 | "html-webpack-plugin": "latest", 12 | "webpack": "latest", 13 | "webpack-cli": "latest", 14 | "webpack-dev-server": "latest" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docs/codesandbox/demo-ts/browser/index.tsx: -------------------------------------------------------------------------------- 1 | import * as hyperdom from "hyperdom"; 2 | 3 | class App extends hyperdom.RenderComponent { 4 | private name: string; 5 | 6 | render() { 7 | return ( 8 |
9 | 10 | 11 |
hi {this.name}
12 |
13 | ); 14 | } 15 | } 16 | 17 | hyperdom.append(document.body, new App()); 18 | -------------------------------------------------------------------------------- /meta.js: -------------------------------------------------------------------------------- 1 | module.exports = function (model, property) { 2 | var hyperdomMeta = model._hyperdomMeta 3 | 4 | if (!hyperdomMeta) { 5 | hyperdomMeta = {} 6 | Object.defineProperty(model, '_hyperdomMeta', {value: hyperdomMeta}) 7 | } 8 | 9 | if (property) { 10 | var meta = hyperdomMeta[property] 11 | 12 | if (!meta) { 13 | meta = hyperdomMeta[property] = {} 14 | } 15 | 16 | return meta 17 | } else { 18 | return hyperdomMeta 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/server/hyperxSpec.ts: -------------------------------------------------------------------------------- 1 | import hx = require('../../hyperx') 2 | import toHtml = require('../../toHtml') 3 | import {expect} from 'chai' 4 | 5 | describe('hyperx', function () { 6 | it('can render with hyperx', function () { 7 | expect(toHtml(hx`
hi
`)).to.equal('
hi
') 8 | }) 9 | it('can render svg with class with hyperx', function () { 10 | expect(toHtml(hx``)).to.equal('') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /docs/codesandbox/demo-ts-routing/browser/index.tsx: -------------------------------------------------------------------------------- 1 | import * as hyperdom from "hyperdom"; 2 | import * as router from "hyperdom/router"; 3 | 4 | const home = router.route("/"); 5 | 6 | class Thing extends hyperdom.RoutesComponent { 7 | tech = "hyperdom"; 8 | 9 | routes() { 10 | return [ 11 | home({ 12 | render: () => { 13 | return
Hello from {this.tech}
; 14 | } 15 | }) 16 | ]; 17 | } 18 | } 19 | 20 | hyperdom.append(document.body, new Thing(), {router}); 21 | -------------------------------------------------------------------------------- /test/server/joinSpec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {html as h} from '../..' 3 | import join = require('../../join') 4 | import toHtml = require('../../toHtml') 5 | 6 | describe('join', function () { 7 | it('can join an array of vdom by a separator', function () { 8 | expect(toHtml(h('div', 9 | join([ 10 | h('code', 'one'), 11 | h('code', 'two'), 12 | h('code', 'three'), 13 | ], ' '), 14 | ))).to.equal('
one two three
') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /listener.js: -------------------------------------------------------------------------------- 1 | var refreshify = require('./render').refreshify 2 | 3 | function ListenerHook (listener) { 4 | this.listener = refreshify(listener) 5 | } 6 | 7 | ListenerHook.prototype.hook = function (element, propertyName) { 8 | element.addEventListener(propertyName.substring(2), this.listener, false) 9 | } 10 | 11 | ListenerHook.prototype.unhook = function (element, propertyName) { 12 | element.removeEventListener(propertyName.substring(2), this.listener) 13 | } 14 | 15 | module.exports = function (listener) { 16 | return new ListenerHook(listener) 17 | } 18 | -------------------------------------------------------------------------------- /debuggingProperties.js: -------------------------------------------------------------------------------- 1 | var runRender = require('./render') 2 | var PropertyHook = require('./propertyHook') 3 | var VirtualNode = require('virtual-dom/vnode/vnode') 4 | 5 | module.exports = function (vdom, model) { 6 | if (process.env.NODE_ENV !== 'production' && vdom && vdom.constructor === VirtualNode) { 7 | if (!vdom.properties) { 8 | vdom.properties = {} 9 | } 10 | 11 | vdom.properties._hyperdomMeta = new PropertyHook({ 12 | component: model, 13 | render: runRender.currentRender() 14 | }) 15 | } 16 | 17 | return vdom 18 | } 19 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-events/browser/app.jsx: -------------------------------------------------------------------------------- 1 | import hyperdom from 'hyperdom'; 2 | import {hello} from './styles.css'; 3 | 4 | export default class App { 5 | renderGreetings() { 6 | if (!this.hideGreetings) { 7 | return
8 |

Hello from Hyperdom!

9 | this.hideGreetings = true}>Next 10 |
11 | } else { 12 | return

Now then...

13 | } 14 | } 15 | 16 | render () { 17 | return
18 | {this.renderGreetings()} 19 |
20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/codesandbox/demo-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-started-init-ts", 3 | "scripts": { 4 | "start": "webpack-dev-server", 5 | "build": "webpack" 6 | }, 7 | "dependencies": { 8 | "hyperdom": "2.1.0" 9 | }, 10 | "devDependencies": { 11 | "clean-webpack-plugin": "latest", 12 | "css-loader": "latest", 13 | "html-webpack-plugin": "latest", 14 | "style-loader": "latest", 15 | "ts-loader": "latest", 16 | "typescript": "latest", 17 | "webpack": "latest", 18 | "webpack-cli": "latest", 19 | "webpack-dev-server": "latest" 20 | } 21 | } -------------------------------------------------------------------------------- /docs/codesandbox/demo-ts-routing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-started-init-ts", 3 | "scripts": { 4 | "start": "webpack-dev-server", 5 | "build": "webpack" 6 | }, 7 | "dependencies": { 8 | "hyperdom": "2.1.0" 9 | }, 10 | "devDependencies": { 11 | "clean-webpack-plugin": "latest", 12 | "css-loader": "latest", 13 | "html-webpack-plugin": "latest", 14 | "style-loader": "latest", 15 | "ts-loader": "latest", 16 | "typescript": "latest", 17 | "webpack": "latest", 18 | "webpack-cli": "latest", 19 | "webpack-dev-server": "latest" 20 | } 21 | } -------------------------------------------------------------------------------- /docs/codesandbox/get-started-ajax-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-started-init-ts", 3 | "scripts": { 4 | "start": "webpack-dev-server", 5 | "build": "webpack" 6 | }, 7 | "dependencies": { 8 | "hyperdom": "2.1.0" 9 | }, 10 | "devDependencies": { 11 | "clean-webpack-plugin": "latest", 12 | "css-loader": "latest", 13 | "html-webpack-plugin": "latest", 14 | "style-loader": "latest", 15 | "ts-loader": "latest", 16 | "typescript": "latest", 17 | "webpack": "latest", 18 | "webpack-cli": "latest", 19 | "webpack-dev-server": "latest" 20 | } 21 | } -------------------------------------------------------------------------------- /docs/codesandbox/get-started-init-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-started-init-ts", 3 | "scripts": { 4 | "start": "webpack-dev-server", 5 | "build": "webpack" 6 | }, 7 | "dependencies": { 8 | "hyperdom": "2.1.0" 9 | }, 10 | "devDependencies": { 11 | "clean-webpack-plugin": "latest", 12 | "css-loader": "latest", 13 | "html-webpack-plugin": "latest", 14 | "style-loader": "latest", 15 | "ts-loader": "latest", 16 | "typescript": "latest", 17 | "webpack": "latest", 18 | "webpack-cli": "latest", 19 | "webpack-dev-server": "latest" 20 | } 21 | } -------------------------------------------------------------------------------- /docs/codesandbox/demo-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "webpack-dev-server", 4 | "build": "webpack" 5 | }, 6 | "dependencies": { 7 | "hyperdom": "latest" 8 | }, 9 | "devDependencies": { 10 | "@babel/core": "latest", 11 | "babel-loader": "latest", 12 | "babel-preset-hyperdom": "latest", 13 | "clean-webpack-plugin": "latest", 14 | "css-loader": "latest", 15 | "html-webpack-plugin": "latest", 16 | "style-loader": "latest", 17 | "webpack": "latest", 18 | "webpack-cli": "latest", 19 | "webpack-dev-server": "latest" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-bindings-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-started-init-ts", 3 | "scripts": { 4 | "start": "webpack-dev-server", 5 | "build": "webpack" 6 | }, 7 | "dependencies": { 8 | "hyperdom": "2.1.0" 9 | }, 10 | "devDependencies": { 11 | "clean-webpack-plugin": "latest", 12 | "css-loader": "latest", 13 | "html-webpack-plugin": "latest", 14 | "style-loader": "latest", 15 | "ts-loader": "latest", 16 | "typescript": "latest", 17 | "webpack": "latest", 18 | "webpack-cli": "latest", 19 | "webpack-dev-server": "latest" 20 | } 21 | } -------------------------------------------------------------------------------- /docs/codesandbox/get-started-compose-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-started-init-ts", 3 | "scripts": { 4 | "start": "webpack-dev-server", 5 | "build": "webpack" 6 | }, 7 | "dependencies": { 8 | "hyperdom": "2.1.0" 9 | }, 10 | "devDependencies": { 11 | "clean-webpack-plugin": "latest", 12 | "css-loader": "latest", 13 | "html-webpack-plugin": "latest", 14 | "style-loader": "latest", 15 | "ts-loader": "latest", 16 | "typescript": "latest", 17 | "webpack": "latest", 18 | "webpack-cli": "latest", 19 | "webpack-dev-server": "latest" 20 | } 21 | } -------------------------------------------------------------------------------- /docs/codesandbox/get-started-events-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-started-init-ts", 3 | "scripts": { 4 | "start": "webpack-dev-server", 5 | "build": "webpack" 6 | }, 7 | "dependencies": { 8 | "hyperdom": "2.1.0" 9 | }, 10 | "devDependencies": { 11 | "clean-webpack-plugin": "latest", 12 | "css-loader": "latest", 13 | "html-webpack-plugin": "latest", 14 | "style-loader": "latest", 15 | "ts-loader": "latest", 16 | "typescript": "latest", 17 | "webpack": "latest", 18 | "webpack-cli": "latest", 19 | "webpack-dev-server": "latest" 20 | } 21 | } -------------------------------------------------------------------------------- /docs/codesandbox/get-started-routing-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-started-init-ts", 3 | "scripts": { 4 | "start": "webpack-dev-server", 5 | "build": "webpack" 6 | }, 7 | "dependencies": { 8 | "hyperdom": "2.1.0" 9 | }, 10 | "devDependencies": { 11 | "clean-webpack-plugin": "latest", 12 | "css-loader": "latest", 13 | "html-webpack-plugin": "latest", 14 | "style-loader": "latest", 15 | "ts-loader": "latest", 16 | "typescript": "latest", 17 | "webpack": "latest", 18 | "webpack-cli": "latest", 19 | "webpack-dev-server": "latest" 20 | } 21 | } -------------------------------------------------------------------------------- /simplePromise.js: -------------------------------------------------------------------------------- 1 | function SimplePromise () { 2 | this.listeners = [] 3 | } 4 | 5 | SimplePromise.prototype.fulfill = function (value) { 6 | if (!this.isFulfilled) { 7 | this.isFulfilled = true 8 | this.value = value 9 | this.listeners.forEach(function (listener) { 10 | listener() 11 | }) 12 | } 13 | } 14 | 15 | SimplePromise.prototype.then = function (success) { 16 | if (this.isFulfilled) { 17 | success(this.value) 18 | } else { 19 | this.listeners.push(success) 20 | } 21 | } 22 | 23 | module.exports = function () { 24 | return new SimplePromise() 25 | } 26 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-ajax/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "webpack-dev-server", 4 | "build": "webpack" 5 | }, 6 | "dependencies": { 7 | "hyperdom": "latest" 8 | }, 9 | "devDependencies": { 10 | "@babel/core": "latest", 11 | "babel-loader": "latest", 12 | "babel-preset-hyperdom": "latest", 13 | "clean-webpack-plugin": "latest", 14 | "css-loader": "latest", 15 | "html-webpack-plugin": "latest", 16 | "style-loader": "latest", 17 | "webpack": "latest", 18 | "webpack-cli": "latest", 19 | "webpack-dev-server": "latest" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-events/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "webpack-dev-server", 4 | "build": "webpack" 5 | }, 6 | "dependencies": { 7 | "hyperdom": "latest" 8 | }, 9 | "devDependencies": { 10 | "@babel/core": "latest", 11 | "babel-loader": "latest", 12 | "babel-preset-hyperdom": "latest", 13 | "clean-webpack-plugin": "latest", 14 | "css-loader": "latest", 15 | "html-webpack-plugin": "latest", 16 | "style-loader": "latest", 17 | "webpack": "latest", 18 | "webpack-cli": "latest", 19 | "webpack-dev-server": "latest" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-init/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "webpack-dev-server", 4 | "build": "webpack" 5 | }, 6 | "dependencies": { 7 | "hyperdom": "latest" 8 | }, 9 | "devDependencies": { 10 | "@babel/core": "latest", 11 | "babel-loader": "latest", 12 | "babel-preset-hyperdom": "latest", 13 | "clean-webpack-plugin": "latest", 14 | "css-loader": "latest", 15 | "html-webpack-plugin": "latest", 16 | "style-loader": "latest", 17 | "webpack": "latest", 18 | "webpack-cli": "latest", 19 | "webpack-dev-server": "latest" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-bindings/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "webpack-dev-server", 4 | "build": "webpack" 5 | }, 6 | "dependencies": { 7 | "hyperdom": "latest" 8 | }, 9 | "devDependencies": { 10 | "@babel/core": "latest", 11 | "babel-loader": "latest", 12 | "babel-preset-hyperdom": "latest", 13 | "clean-webpack-plugin": "latest", 14 | "css-loader": "latest", 15 | "html-webpack-plugin": "latest", 16 | "style-loader": "latest", 17 | "webpack": "latest", 18 | "webpack-cli": "latest", 19 | "webpack-dev-server": "latest" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-compose/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "webpack-dev-server", 4 | "build": "webpack" 5 | }, 6 | "dependencies": { 7 | "hyperdom": "latest" 8 | }, 9 | "devDependencies": { 10 | "@babel/core": "latest", 11 | "babel-loader": "latest", 12 | "babel-preset-hyperdom": "latest", 13 | "clean-webpack-plugin": "latest", 14 | "css-loader": "latest", 15 | "html-webpack-plugin": "latest", 16 | "style-loader": "latest", 17 | "webpack": "latest", 18 | "webpack-cli": "latest", 19 | "webpack-dev-server": "latest" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-routing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "webpack-dev-server", 4 | "build": "webpack" 5 | }, 6 | "dependencies": { 7 | "hyperdom": "latest" 8 | }, 9 | "devDependencies": { 10 | "@babel/core": "latest", 11 | "babel-loader": "latest", 12 | "babel-preset-hyperdom": "latest", 13 | "clean-webpack-plugin": "latest", 14 | "css-loader": "latest", 15 | "html-webpack-plugin": "latest", 16 | "style-loader": "latest", 17 | "webpack": "latest", 18 | "webpack-cli": "latest", 19 | "webpack-dev-server": "latest" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/dummy-defs.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'trytryagain' { 2 | const thing: any 3 | export = thing 4 | } 5 | 6 | declare module 'vdom-to-html' { 7 | const thing: any 8 | export = thing 9 | } 10 | 11 | declare module 'browser-monkey' { 12 | const thing: any 13 | export = thing 14 | } 15 | 16 | declare module 'lowscore/times' { 17 | const thing: any 18 | export = thing 19 | } 20 | 21 | declare module 'lowscore/range' { 22 | const thing: any 23 | export = thing 24 | } 25 | 26 | // tslint:disable-next-line 27 | declare interface JQuery { 28 | sendkeys (keys: string): void 29 | } 30 | -------------------------------------------------------------------------------- /set.js: -------------------------------------------------------------------------------- 1 | if (typeof Set === 'function') { 2 | module.exports = Set 3 | } else { 4 | module.exports = function () { 5 | this.items = [] 6 | } 7 | 8 | module.exports.prototype.add = function (widget) { 9 | if (this.items.indexOf(widget) === -1) { 10 | this.items.push(widget) 11 | } 12 | } 13 | 14 | module.exports.prototype.delete = function (widget) { 15 | var i = this.items.indexOf(widget) 16 | if (i !== -1) { 17 | this.items.splice(i, 1) 18 | } 19 | } 20 | 21 | module.exports.prototype.forEach = function (fn) { 22 | for (var n = 0; n < this.items.length; n++) { 23 | fn(this.items[n]) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-events-ts/browser/app.tsx: -------------------------------------------------------------------------------- 1 | import * as hyperdom from "hyperdom"; 2 | import { hello } from "./styles.css"; 3 | 4 | export default class App extends hyperdom.RenderComponent { 5 | private hideGreetings: boolean = false; 6 | 7 | renderGreetings() { 8 | if (!this.hideGreetings) { 9 | return ( 10 |
11 |

Hello from Hyperdom!

12 | this.hideGreetings = true}>Next 13 |
14 | ); 15 | } else { 16 | return

Now then...

; 17 | } 18 | } 19 | 20 | render() { 21 | return
{this.renderGreetings()}
; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - [Introduction](introduction) 2 | - [Getting started](getting-started) 3 | - [Usage](usage) 4 | - [API Reference](api) 5 | - **Links** 6 | - [![Code](https://icongr.am/feather/code.svg?size=16&color=808080)Demo Sandbox](https://codesandbox.io/embed/github/featurist/hyperdom/tree/master/docs/codesandbox/get-started-routing) 7 | - [![Github](https://icongr.am/devicon/github-original.svg?color=808080&size=16)Github](https://github.com/featurist/hyperdom) 8 | - [![NPM](https://icongram.jgog.in/simple/npm.svg?colored&size=16)NPM](https://www.npmjs.com/package/hyperdom) 9 | - [![Twitter](https://icongram.jgog.in/simple/twitter.svg?colored&size=16)@Hyperdom1](https://twitter.com/Hyperdom1) 10 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "linterOptions": { 4 | "exclude": [ 5 | "**/*.json", 6 | "node_modules" 7 | ] 8 | }, 9 | "extends": [ 10 | "tslint:recommended" 11 | ], 12 | "rules": { 13 | "semicolon": [true, "never"], 14 | "ordered-imports": false, 15 | "only-arrow-functions": false, 16 | "object-literal-sort-keys": false, 17 | "max-classes-per-file": false, 18 | "no-unused-expression": false, 19 | "no-empty": false, 20 | "no-shadowed-variable": false, 21 | "interface-name": false, 22 | "space-before-function-paren": true, 23 | "object-literal-key-quotes": false, 24 | "quotemark": false 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es6", "dom", "esnext"], 4 | "types": ["mocha"], 5 | "noImplicitAny": true, 6 | "strictNullChecks": true, 7 | "strictFunctionTypes": true, 8 | "noImplicitThis": true, 9 | "preserveConstEnums": true, 10 | "sourceMap": true, 11 | "jsx": "react", 12 | "jsxFactory": "hyperdom.jsx", 13 | "noEmit": true, 14 | "plugins": [ 15 | { 16 | "name": "typescript-tslint-plugin", 17 | "ignoreDefinitionFiles": true, 18 | "configFile": "./tslint.json" 19 | } 20 | ], 21 | "baseUrl": "." 22 | }, 23 | "exclude": [ 24 | "node_modules", 25 | "docs", 26 | "website-dist" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /docs/codesandbox/demo-no-jsx/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devServer: { 8 | disableHostCheck: true, 9 | historyApiFallback: true 10 | }, 11 | devtool: 'eval-source-map', 12 | entry: './browser/index.js', 13 | output: { 14 | filename: 'bundle.js', 15 | publicPath: '/', 16 | path: path.resolve(__dirname, 'browser', 'dist') 17 | }, 18 | resolve: { 19 | extensions: ['.js'] 20 | }, 21 | plugins: [ 22 | new CleanWebpackPlugin(), 23 | new HtmlWebpackPlugin({title: 'Hyperdom Demo'}) 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /deprecations.js: -------------------------------------------------------------------------------- 1 | function deprecationWarning () { 2 | var warningIssued = false 3 | 4 | return function (arg) { 5 | if (process.env.NODE_ENV !== 'production' && !warningIssued) { 6 | console.warn(arg) // eslint-disable-line no-console 7 | warningIssued = true 8 | } 9 | } 10 | } 11 | 12 | module.exports = { 13 | refresh: deprecationWarning(), 14 | currentRender: deprecationWarning(), 15 | component: deprecationWarning(), 16 | renderFunction: deprecationWarning(), 17 | norefresh: deprecationWarning(), 18 | mapBinding: deprecationWarning(), 19 | viewComponent: deprecationWarning(), 20 | htmlRawHtml: deprecationWarning(), 21 | htmlBinding: deprecationWarning(), 22 | refreshAfter: deprecationWarning() 23 | } 24 | -------------------------------------------------------------------------------- /test/server/jsdomSpec.ts: -------------------------------------------------------------------------------- 1 | import * as hyperdom from '../..' 2 | import {JSDOM} from 'jsdom' 3 | import {expect} from 'chai' 4 | import {RenderComponent} from "../../index" 5 | 6 | describe('hyperdom', function () { 7 | describe('.append()', function () { 8 | it('renders elements in jsdom', function () { 9 | const {window} = new JSDOM(``) 10 | const app = new class extends RenderComponent { 11 | public render () { 12 | return hyperdom.html('p', 'hello') 13 | } 14 | }() 15 | function requestRender (render: any) { 16 | render() 17 | } 18 | 19 | hyperdom.append(window.document.body, app, { 20 | document: window.document, 21 | requestRender, 22 | window, 23 | }) 24 | expect(window.document.body.childNodes[0].textContent).to.equal('hello') 25 | }) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-bindings/browser/app.jsx: -------------------------------------------------------------------------------- 1 | import hyperdom from 'hyperdom'; 2 | import {hello} from './styles.css'; 3 | 4 | export default class App { 5 | renderGreetings() { 6 | if (!this.hideGreetings) { 7 | return
8 |

Hello from Hyperdom!

9 | this.hideGreetings = true}>Next 10 |
11 | } 12 | } 13 | 14 | renderNameForm() { 15 | if (this.hideGreetings) { 16 | return
17 | 20 | {this.userName &&
You're now a hyperdomsta {this.userName}
} 21 |
22 | } 23 | } 24 | 25 | render() { 26 | return
27 | {this.renderGreetings()} 28 | {this.renderNameForm()} 29 |
30 | } 31 | } 32 | -------------------------------------------------------------------------------- /binding.js: -------------------------------------------------------------------------------- 1 | var meta = require('./meta') 2 | 3 | module.exports = function (b) { 4 | var binding = b 5 | 6 | if (b instanceof Array) { 7 | binding = bindingObject.apply(undefined, b) 8 | } else if (b instanceof Object && (typeof b.set === 'function' || typeof b.get === 'function')) { 9 | binding = b 10 | } else { 11 | throw Error('hyperdom bindings must be either an array [object, property, setter] or an object { get(), set(value) }, instead binding was: ' + JSON.stringify(b)) 12 | } 13 | 14 | return binding 15 | } 16 | 17 | function bindingObject (model, property, setter) { 18 | var _meta 19 | 20 | return { 21 | get: function () { 22 | return model[property] 23 | }, 24 | 25 | set: function (value) { 26 | model[property] = value 27 | if (setter) { 28 | return setter(value) 29 | } 30 | }, 31 | 32 | meta: function () { 33 | return _meta || (_meta = meta(model, property)) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /merge.js: -------------------------------------------------------------------------------- 1 | var domComponent = require('./domComponent') 2 | var Mount = require('./mount') 3 | var vdomParser = require('vdom-parser') 4 | var render = require('./render') 5 | 6 | module.exports = function (element, app, options) { 7 | if (!element) { 8 | throw new Error('merge: element must not be null') 9 | } 10 | 11 | var mount = new Mount(app, options) 12 | 13 | mount.component = domComponent.create(options) 14 | var serverVdom = vdomParser(element) 15 | mount.component.merge(serverVdom, element) 16 | 17 | mount.serverRenderCache = new LoadCache(options && options.loadCache) 18 | 19 | render(mount, function () { 20 | var vdom = mount.render() 21 | mount.component.update(vdom) 22 | }) 23 | 24 | delete mount.serverRenderCache 25 | 26 | return mount 27 | } 28 | 29 | function LoadCache (data) { 30 | this.data = data 31 | } 32 | 33 | LoadCache.prototype.cache = function (key) { 34 | var data = this.data[key] 35 | 36 | return Promise.resolve(data) 37 | } 38 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-compose/browser/app.jsx: -------------------------------------------------------------------------------- 1 | import hyperdom from 'hyperdom'; 2 | import styles from './styles.css'; 3 | import BeerList from './BeerList'; 4 | 5 | export default class App { 6 | constructor () { 7 | this.beerList = new BeerList(); 8 | } 9 | 10 | renderNameForm() { 11 | return ( 12 |
13 | 16 | {this.userName && ( 17 |
18 | You're now a hyperdomsta {this.userName} 19 |
20 | )} 21 | {this.userName && this.beerList} 22 |
23 | ); 24 | } 25 | 26 | renderGreetings() { 27 | return ( 28 |
29 |

Hello from Hyperdom!

30 | (this.hideGreetings = true)}>Next 31 |
32 | ); 33 | } 34 | 35 | render() { 36 | return ( 37 |
{this.hideGreetings ? this.renderNameForm() : this.renderGreetings()}
38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-bindings-ts/browser/app.tsx: -------------------------------------------------------------------------------- 1 | import * as hyperdom from "hyperdom"; 2 | import { hello } from "./styles.css"; 3 | 4 | export default class App extends hyperdom.RenderComponent { 5 | private hideGreetings: boolean = false; 6 | private userName: string = ''; 7 | 8 | renderGreetings() { 9 | if (!this.hideGreetings) { 10 | return ( 11 |
12 |

Hello from Hyperdom!

13 | this.hideGreetings = true}>Next 14 |
15 | ); 16 | } 17 | } 18 | 19 | renderNameForm() { 20 | if (this.hideGreetings) { 21 | return ( 22 |
23 | 24 | {this.userName &&
You're now a hyperdomsta {this.userName}
} 25 |
26 | ); 27 | } 28 | } 29 | 30 | render() { 31 | return ( 32 |
33 | {this.renderGreetings()} 34 | {this.renderNameForm()} 35 |
36 | ); 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Featurist 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var rendering = require('./rendering') 2 | var render = require('./render') 3 | var viewComponent = require('./viewComponent') 4 | var deprecations = require('./deprecations') 5 | 6 | exports.html = rendering.html 7 | exports.html.refreshify = render.refreshify 8 | exports.rawHtml = rendering.rawHtml 9 | exports.jsx = rendering.jsx 10 | exports.attach = rendering.attach 11 | exports.replace = rendering.replace 12 | exports.append = rendering.append 13 | exports.appendVDom = rendering.appendVDom 14 | exports.binding = require('./binding') 15 | exports.meta = require('./meta') 16 | exports.refreshify = render.refreshify 17 | exports.norefresh = require('./refreshEventResult').norefresh 18 | exports.join = require('./join') 19 | exports.viewComponent = viewComponent 20 | exports.component = function (model) { 21 | deprecations.viewComponent('hyperdom.component is deprecated, use hyperdom.viewComponent instead') 22 | return viewComponent(model) 23 | } 24 | 25 | exports.currentRender = render.currentRender 26 | exports.RenderComponent = function () {} // placeholder for typescript 27 | exports.RoutesComponent = function () {} // placeholder for typescript 28 | -------------------------------------------------------------------------------- /vhtml.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var VNode = require('virtual-dom/vnode/vnode.js') 4 | var isHook = require('virtual-dom/vnode/is-vhook') 5 | var xml = require('./xml') 6 | 7 | var softSetHook = require('virtual-dom/virtual-hyperscript/hooks/soft-set-hook.js') 8 | 9 | module.exports = h 10 | 11 | function h (tagName, props, children) { 12 | var tag = tagName 13 | 14 | // support keys 15 | if (props.hasOwnProperty('key')) { 16 | var key = props.key 17 | props.key = undefined 18 | } 19 | 20 | if (props.innerHTML === false) { 21 | props.innerHTML = '' 22 | } 23 | 24 | // support namespace 25 | if (props.hasOwnProperty('namespace')) { 26 | var namespace = props.namespace 27 | props.namespace = undefined 28 | } 29 | 30 | // fix cursor bug 31 | if (tag.toLowerCase() === 'input' && 32 | !namespace && 33 | props.hasOwnProperty('value') && 34 | props.value !== undefined && 35 | !isHook(props.value) 36 | ) { 37 | props.value = softSetHook(props.value) 38 | } 39 | 40 | var vnode = new VNode(tag, props, children, key, namespace) 41 | 42 | if (props.xmlns) { 43 | xml.transform(vnode) 44 | } 45 | 46 | return vnode 47 | } 48 | -------------------------------------------------------------------------------- /docs/codesandbox/demo-js/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devServer: { 8 | disableHostCheck: true, 9 | historyApiFallback: true 10 | }, 11 | devtool: 'eval-source-map', 12 | entry: './browser/index.js', 13 | output: { 14 | filename: 'bundle.js', 15 | publicPath: '/', 16 | path: path.resolve(__dirname, 'browser', 'dist') 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.jsx'] 20 | }, 21 | plugins: [ 22 | new CleanWebpackPlugin(), 23 | new HtmlWebpackPlugin({title: 'Hyperdom Demo'}) 24 | ], 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.jsx?$/, 29 | use: 'babel-loader' 30 | }, 31 | { 32 | test: /\.css$/, 33 | use: [ 34 | { 35 | loader: 'style-loader' 36 | }, 37 | { 38 | loader: 'css-loader', 39 | options: { 40 | modules: true 41 | } 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /toVdom.js: -------------------------------------------------------------------------------- 1 | var Vtext = require('virtual-dom/vnode/vtext.js') 2 | var isVdom = require('./isVdom') 3 | var Component = require('./component') 4 | 5 | function toVdom (object) { 6 | if (object === undefined || object === null || object === false) { 7 | return new Vtext('') 8 | } else if (typeof (object) !== 'object') { 9 | return new Vtext(String(object)) 10 | } else if (object instanceof Date) { 11 | return new Vtext(String(object)) 12 | } else if (object instanceof Error) { 13 | return new Vtext(object.toString()) 14 | } else if (isVdom(object)) { 15 | return object 16 | } else if (typeof object.render === 'function') { 17 | return new Component(object) 18 | } else { 19 | return new Vtext(JSON.stringify(object)) 20 | } 21 | } 22 | 23 | module.exports = toVdom 24 | 25 | function addChild (children, child) { 26 | if (child instanceof Array) { 27 | for (var n = 0; n < child.length; n++) { 28 | addChild(children, child[n]) 29 | } 30 | } else { 31 | children.push(toVdom(child)) 32 | } 33 | } 34 | 35 | module.exports.recursive = function (child) { 36 | var children = [] 37 | addChild(children, child) 38 | return children 39 | } 40 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-ajax/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devServer: { 8 | disableHostCheck: true, 9 | historyApiFallback: true 10 | }, 11 | devtool: 'eval-source-map', 12 | entry: './browser/index.js', 13 | output: { 14 | filename: 'bundle.js', 15 | publicPath: '/', 16 | path: path.resolve(__dirname, 'browser', 'dist') 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.jsx'] 20 | }, 21 | plugins: [ 22 | new CleanWebpackPlugin(), 23 | new HtmlWebpackPlugin({title: 'Hyperdom Demo'}) 24 | ], 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.jsx?$/, 29 | use: 'babel-loader' 30 | }, 31 | { 32 | test: /\.css$/, 33 | use: [ 34 | { 35 | loader: 'style-loader' 36 | }, 37 | { 38 | loader: 'css-loader', 39 | options: { 40 | modules: true 41 | } 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-compose/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devServer: { 8 | disableHostCheck: true, 9 | historyApiFallback: true 10 | }, 11 | devtool: 'eval-source-map', 12 | entry: './browser/index.js', 13 | output: { 14 | filename: 'bundle.js', 15 | publicPath: '/', 16 | path: path.resolve(__dirname, 'browser', 'dist') 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.jsx'] 20 | }, 21 | plugins: [ 22 | new CleanWebpackPlugin(), 23 | new HtmlWebpackPlugin({title: 'Hyperdom Demo'}) 24 | ], 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.jsx?$/, 29 | use: 'babel-loader' 30 | }, 31 | { 32 | test: /\.css$/, 33 | use: [ 34 | { 35 | loader: 'style-loader' 36 | }, 37 | { 38 | loader: 'css-loader', 39 | options: { 40 | modules: true 41 | } 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-events/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devServer: { 8 | disableHostCheck: true, 9 | historyApiFallback: true 10 | }, 11 | devtool: 'eval-source-map', 12 | entry: './browser/index.js', 13 | output: { 14 | filename: 'bundle.js', 15 | publicPath: '/', 16 | path: path.resolve(__dirname, 'browser', 'dist') 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.jsx'] 20 | }, 21 | plugins: [ 22 | new CleanWebpackPlugin(), 23 | new HtmlWebpackPlugin({title: 'Hyperdom Demo'}) 24 | ], 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.jsx?$/, 29 | use: 'babel-loader' 30 | }, 31 | { 32 | test: /\.css$/, 33 | use: [ 34 | { 35 | loader: 'style-loader' 36 | }, 37 | { 38 | loader: 'css-loader', 39 | options: { 40 | modules: true 41 | } 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-init/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devServer: { 8 | disableHostCheck: true, 9 | historyApiFallback: true 10 | }, 11 | devtool: 'eval-source-map', 12 | entry: './browser/index.js', 13 | output: { 14 | filename: 'bundle.js', 15 | publicPath: '/', 16 | path: path.resolve(__dirname, 'browser', 'dist') 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.jsx'] 20 | }, 21 | plugins: [ 22 | new CleanWebpackPlugin(), 23 | new HtmlWebpackPlugin({title: 'Hyperdom Demo'}) 24 | ], 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.jsx?$/, 29 | use: 'babel-loader' 30 | }, 31 | { 32 | test: /\.css$/, 33 | use: [ 34 | { 35 | loader: 'style-loader' 36 | }, 37 | { 38 | loader: 'css-loader', 39 | options: { 40 | modules: true 41 | } 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-routing/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devServer: { 8 | disableHostCheck: true, 9 | historyApiFallback: true 10 | }, 11 | devtool: 'eval-source-map', 12 | entry: './browser/index.js', 13 | output: { 14 | filename: 'bundle.js', 15 | publicPath: '/', 16 | path: path.resolve(__dirname, 'browser', 'dist') 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.jsx'] 20 | }, 21 | plugins: [ 22 | new CleanWebpackPlugin(), 23 | new HtmlWebpackPlugin({title: 'Hyperdom Demo'}) 24 | ], 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.jsx?$/, 29 | use: 'babel-loader' 30 | }, 31 | { 32 | test: /\.css$/, 33 | use: [ 34 | { 35 | loader: 'style-loader' 36 | }, 37 | { 38 | loader: 'css-loader', 39 | options: { 40 | modules: true 41 | } 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/browser/mountHyperdom.js: -------------------------------------------------------------------------------- 1 | var hyperdom = require('../..') 2 | var browserMonkey = require('browser-monkey') 3 | var extend = require('lowscore/extend') 4 | var div 5 | 6 | function createTestDiv () { 7 | if (div && div.parentNode) { 8 | div.parentNode.removeChild(div) 9 | } 10 | 11 | div = window.document.createElement('div') 12 | window.document.body.appendChild(div) 13 | 14 | return div 15 | } 16 | 17 | function createReloadButton (href) { 18 | var link = document.createElement('a') 19 | link.href = href 20 | link.innerText = '⟳ reload' 21 | link.style = 'z-index: 1000;' + 22 | 'position: fixed;' + 23 | 'right: 5px;' + 24 | 'bottom: 5px;' 25 | 26 | return link 27 | } 28 | 29 | window.addEventListener('onload', function () { 30 | document.body.append(createReloadButton(window.location.href)) 31 | }) 32 | 33 | module.exports = function (app, options) { 34 | var testDiv = createTestDiv() 35 | if (options && (options.hash || options.url) && options.router) { 36 | options.router.push(options.url || options.hash) 37 | } 38 | hyperdom.append(testDiv, app, extend({ requestRender: setTimeout }, options)) 39 | return browserMonkey.scope(testDiv) 40 | } 41 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-bindings/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devServer: { 8 | disableHostCheck: true, 9 | historyApiFallback: true 10 | }, 11 | devtool: 'eval-source-map', 12 | entry: './browser/index.js', 13 | output: { 14 | filename: 'bundle.js', 15 | publicPath: '/', 16 | path: path.resolve(__dirname, 'browser', 'dist') 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.jsx'] 20 | }, 21 | plugins: [ 22 | new CleanWebpackPlugin(), 23 | new HtmlWebpackPlugin({title: 'Hyperdom Demo'}) 24 | ], 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.jsx?$/, 29 | use: 'babel-loader' 30 | }, 31 | { 32 | test: /\.css$/, 33 | use: [ 34 | { 35 | loader: 'style-loader' 36 | }, 37 | { 38 | loader: 'css-loader', 39 | options: { 40 | modules: true 41 | } 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-compose-ts/browser/app.tsx: -------------------------------------------------------------------------------- 1 | import * as hyperdom from "hyperdom"; 2 | import * as styles from "./styles.css"; 3 | import BeerList from "./BeerList"; 4 | 5 | export default class App extends hyperdom.RenderComponent { 6 | private hideGreetings = false; 7 | private userName = ""; 8 | private beerList = new BeerList(); 9 | 10 | renderGreetings() { 11 | return ( 12 |
13 |

Hello from Hyperdom!

14 | (this.hideGreetings = true)}> 15 | Next 16 | 17 |
18 | ); 19 | } 20 | 21 | renderNameForm() { 22 | return ( 23 |
24 | 27 | {this.userName && ( 28 |
29 | You're now a hyperdomsta {this.userName} 30 |
31 | )} 32 | {this.userName && this.beerList} 33 |
34 | ); 35 | } 36 | 37 | render() { 38 | return ( 39 |
{this.hideGreetings ? this.renderNameForm() : this.renderGreetings()}
40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /docs/codesandbox/demo-ts/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devServer: { 8 | disableHostCheck: true, 9 | historyApiFallback: true 10 | }, 11 | devtool: 'eval-source-map', 12 | entry: './browser/index.ts', 13 | output: { 14 | filename: 'bundle.js', 15 | publicPath: '/', 16 | path: path.resolve(__dirname, 'browser', 'dist') 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.ts', '.tsx'] 20 | }, 21 | plugins: [ 22 | new CleanWebpackPlugin(), 23 | new HtmlWebpackPlugin({title: 'Hyperdom Demo'}) 24 | ], 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.tsx?$/, 29 | use: { 30 | loader: 'ts-loader' 31 | } 32 | }, 33 | { 34 | test: /\.css$/, 35 | use: [ 36 | { 37 | loader: 'style-loader' 38 | }, 39 | { 40 | loader: 'css-loader', 41 | options: { 42 | modules: true 43 | } 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/codesandbox/demo-ts-routing/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devServer: { 8 | disableHostCheck: true, 9 | historyApiFallback: true 10 | }, 11 | devtool: 'eval-source-map', 12 | entry: './browser/index.ts', 13 | output: { 14 | filename: 'bundle.js', 15 | publicPath: '/', 16 | path: path.resolve(__dirname, 'browser', 'dist') 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.ts', '.tsx'] 20 | }, 21 | plugins: [ 22 | new CleanWebpackPlugin(), 23 | new HtmlWebpackPlugin({title: 'Hyperdom Demo'}) 24 | ], 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.tsx?$/, 29 | use: { 30 | loader: 'ts-loader' 31 | } 32 | }, 33 | { 34 | test: /\.css$/, 35 | use: [ 36 | { 37 | loader: 'style-loader' 38 | }, 39 | { 40 | loader: 'css-loader', 41 | options: { 42 | modules: true 43 | } 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-routing/browser/Beer.jsx: -------------------------------------------------------------------------------- 1 | import hyperdom from 'hyperdom'; 2 | import routes from './routes'; 3 | 4 | export default class Beer { 5 | constructor(beerList) { 6 | this.beerList = beerList; 7 | } 8 | 9 | get beerId() { 10 | return Number(this.beerIdParam); 11 | } 12 | 13 | routes() { 14 | return [ 15 | routes.beer({ 16 | onload: async () => { 17 | this.beer = null; 18 | 19 | if (this.beerList.beers) { 20 | this.beer = this.beerList.beers.find( 21 | beer => beer.id === this.beerId 22 | ); 23 | } else { 24 | const response = await fetch( 25 | `https://api.punkapi.com/v2/beers/${this.beerId}` 26 | ); 27 | this.beer = (await response.json())[0]; 28 | } 29 | }, 30 | bindings: { 31 | id: [this, "beerIdParam"] 32 | }, 33 | render: () => { 34 | return ( 35 |
{!this.beer ? "Loading..." : this.renderCurrentBeer()}
36 | ); 37 | } 38 | }) 39 | ]; 40 | } 41 | 42 | renderCurrentBeer() { 43 | return ; 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /test/server/detachedRenderingSpec.ts: -------------------------------------------------------------------------------- 1 | import * as hyperdom from '../..' 2 | const h = hyperdom.html 3 | 4 | import * as vdomToHtml from 'vdom-to-html' 5 | import {expect} from 'chai' 6 | import hyperdomComponent = require('../../componentWidget') 7 | 8 | describe('hyperdom', function () { 9 | describe('.html(), detached from a real DOM', function () { 10 | it('creates a virtual dom with event handlers', function () { 11 | const model = { counter: 0 } 12 | const vdom = h('.outer', 13 | h('.inner', { 14 | onclick () { 15 | model.counter++ 16 | }, 17 | }), 18 | hyperdomComponent(function () { 19 | return h('.component') 20 | }), 21 | h.rawHtml('div', 'some raw HTML'), 22 | ) 23 | const html = vdomToHtml(vdom) 24 | // tslint:disable-next-line 25 | expect(html).to.equal('
some raw HTML
') 26 | vdom.children[0].properties.onclick.handler() 27 | expect(model.counter).to.equal(1) 28 | vdom.children[0].properties.onclick.handler() 29 | expect(model.counter).to.equal(2) 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-ajax-ts/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devServer: { 8 | disableHostCheck: true, 9 | historyApiFallback: true 10 | }, 11 | devtool: 'eval-source-map', 12 | entry: './browser/index.ts', 13 | output: { 14 | filename: 'bundle.js', 15 | publicPath: '/', 16 | path: path.resolve(__dirname, 'browser', 'dist') 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.ts', '.tsx'] 20 | }, 21 | plugins: [ 22 | new CleanWebpackPlugin(), 23 | new HtmlWebpackPlugin({title: 'Hyperdom Demo'}) 24 | ], 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.tsx?$/, 29 | use: { 30 | loader: 'ts-loader' 31 | } 32 | }, 33 | { 34 | test: /\.css$/, 35 | use: [ 36 | { 37 | loader: 'style-loader' 38 | }, 39 | { 40 | loader: 'css-loader', 41 | options: { 42 | modules: true 43 | } 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-init-ts/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devServer: { 8 | disableHostCheck: true, 9 | historyApiFallback: true 10 | }, 11 | devtool: 'eval-source-map', 12 | entry: './browser/index.ts', 13 | output: { 14 | filename: 'bundle.js', 15 | publicPath: '/', 16 | path: path.resolve(__dirname, 'browser', 'dist') 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.ts', '.tsx'] 20 | }, 21 | plugins: [ 22 | new CleanWebpackPlugin(), 23 | new HtmlWebpackPlugin({title: 'Hyperdom Demo'}) 24 | ], 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.tsx?$/, 29 | use: { 30 | loader: 'ts-loader' 31 | } 32 | }, 33 | { 34 | test: /\.css$/, 35 | use: [ 36 | { 37 | loader: 'style-loader' 38 | }, 39 | { 40 | loader: 'css-loader', 41 | options: { 42 | modules: true 43 | } 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-bindings-ts/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devServer: { 8 | disableHostCheck: true, 9 | historyApiFallback: true 10 | }, 11 | devtool: 'eval-source-map', 12 | entry: './browser/index.ts', 13 | output: { 14 | filename: 'bundle.js', 15 | publicPath: '/', 16 | path: path.resolve(__dirname, 'browser', 'dist') 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.ts', '.tsx'] 20 | }, 21 | plugins: [ 22 | new CleanWebpackPlugin(), 23 | new HtmlWebpackPlugin({title: 'Hyperdom Demo'}) 24 | ], 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.tsx?$/, 29 | use: { 30 | loader: 'ts-loader' 31 | } 32 | }, 33 | { 34 | test: /\.css$/, 35 | use: [ 36 | { 37 | loader: 'style-loader' 38 | }, 39 | { 40 | loader: 'css-loader', 41 | options: { 42 | modules: true 43 | } 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-compose-ts/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devServer: { 8 | disableHostCheck: true, 9 | historyApiFallback: true 10 | }, 11 | devtool: 'eval-source-map', 12 | entry: './browser/index.ts', 13 | output: { 14 | filename: 'bundle.js', 15 | publicPath: '/', 16 | path: path.resolve(__dirname, 'browser', 'dist') 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.ts', '.tsx'] 20 | }, 21 | plugins: [ 22 | new CleanWebpackPlugin(), 23 | new HtmlWebpackPlugin({title: 'Hyperdom Demo'}) 24 | ], 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.tsx?$/, 29 | use: { 30 | loader: 'ts-loader' 31 | } 32 | }, 33 | { 34 | test: /\.css$/, 35 | use: [ 36 | { 37 | loader: 'style-loader' 38 | }, 39 | { 40 | loader: 'css-loader', 41 | options: { 42 | modules: true 43 | } 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-events-ts/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devServer: { 8 | disableHostCheck: true, 9 | historyApiFallback: true 10 | }, 11 | devtool: 'eval-source-map', 12 | entry: './browser/index.ts', 13 | output: { 14 | filename: 'bundle.js', 15 | publicPath: '/', 16 | path: path.resolve(__dirname, 'browser', 'dist') 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.ts', '.tsx'] 20 | }, 21 | plugins: [ 22 | new CleanWebpackPlugin(), 23 | new HtmlWebpackPlugin({title: 'Hyperdom Demo'}) 24 | ], 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.tsx?$/, 29 | use: { 30 | loader: 'ts-loader' 31 | } 32 | }, 33 | { 34 | test: /\.css$/, 35 | use: [ 36 | { 37 | loader: 'style-loader' 38 | }, 39 | { 40 | loader: 'css-loader', 41 | options: { 42 | modules: true 43 | } 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-routing-ts/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devServer: { 8 | disableHostCheck: true, 9 | historyApiFallback: true 10 | }, 11 | devtool: 'eval-source-map', 12 | entry: './browser/index.ts', 13 | output: { 14 | filename: 'bundle.js', 15 | publicPath: '/', 16 | path: path.resolve(__dirname, 'browser', 'dist') 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.ts', '.tsx'] 20 | }, 21 | plugins: [ 22 | new CleanWebpackPlugin(), 23 | new HtmlWebpackPlugin({title: 'Hyperdom Demo'}) 24 | ], 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.tsx?$/, 29 | use: { 30 | loader: 'ts-loader' 31 | } 32 | }, 33 | { 34 | test: /\.css$/, 35 | use: [ 36 | { 37 | loader: 'style-loader' 38 | }, 39 | { 40 | loader: 'css-loader', 41 | options: { 42 | modules: true 43 | } 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-routing/browser/app.jsx: -------------------------------------------------------------------------------- 1 | import hyperdom from 'hyperdom'; 2 | import styles from './styles.css'; 3 | import BeerList from './BeerList'; 4 | import routes from "./routes"; 5 | 6 | export default class App { 7 | constructor () { 8 | this.beerList = new BeerList() 9 | } 10 | 11 | routes() { 12 | return [ 13 | routes.home({ 14 | render: () => { 15 | return this.hideGreetings ? this.renderNameForm() : this.renderGreetings() 16 | } 17 | }), 18 | this.beerList 19 | ] 20 | } 21 | 22 | renderLayout(content) { 23 | return
{content}
24 | } 25 | 26 | renderNameForm() { 27 | return ( 28 |
29 | 32 | { 33 | this.userName &&
You're now a hyperdomsta {this.userName}
34 | } 35 | {this.userName && Have a beer} 36 |
37 | ) 38 | } 39 | 40 | renderGreetings() { 41 | return ( 42 |
43 |

Hello from Hyperdom!

44 | (this.hideGreetings = true)}> 45 | Next 46 | 47 |
48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-routing-ts/browser/Beer.tsx: -------------------------------------------------------------------------------- 1 | import * as hyperdom from "hyperdom"; 2 | import { IBeer } from "./BeerList"; 3 | import routes from "./routes"; 4 | 5 | export default class Beer { 6 | private beerIdParam?: string; 7 | private beer?: IBeer; 8 | private beers: Array = []; 9 | 10 | constructor(beerList: { beers: Array }) { 11 | this.beers = beerList.beers; 12 | } 13 | 14 | get beerId() { 15 | return Number(this.beerIdParam); 16 | } 17 | 18 | routes() { 19 | return [ 20 | routes.beer({ 21 | onload: async () => { 22 | this.beer = undefined; 23 | 24 | if (this.beers.length) { 25 | this.beer = this.beers.find(beer => beer.id === this.beerId); 26 | } else { 27 | const response = await fetch( 28 | `https://api.punkapi.com/v2/beers/${this.beerId}` 29 | ); 30 | this.beer = (await response.json())[0]; 31 | } 32 | }, 33 | bindings: { 34 | id: [this, "beerIdParam"] 35 | }, 36 | render: () => { 37 | return ( 38 |
{!this.beer ? "Loading..." : this.renderCurrentBeer()}
39 | ); 40 | } 41 | }) 42 | ]; 43 | } 44 | 45 | renderCurrentBeer() { 46 | return ; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-routing-ts/browser/app.tsx: -------------------------------------------------------------------------------- 1 | import * as hyperdom from "hyperdom"; 2 | import * as styles from "./styles.css"; 3 | import BeerList from "./BeerList"; 4 | import routes from "./routes"; 5 | 6 | export default class App extends hyperdom.RoutesComponent { 7 | private hideGreetings = false; 8 | private userName = ""; 9 | private beerList = new BeerList(); 10 | 11 | routes() { 12 | return [ 13 | routes.home({ 14 | render: () => { 15 | return this.hideGreetings ? this.renderNameForm() : this.renderGreetings(); 16 | } 17 | }), 18 | this.beerList 19 | ]; 20 | } 21 | 22 | renderLayout(content: hyperdom.VdomFragment) { 23 | return
{content}
; 24 | } 25 | 26 | renderGreetings() { 27 | return ( 28 |
29 |

Hello from Hyperdom!

30 | (this.hideGreetings = true)}> 31 | Next 32 | 33 |
34 | ); 35 | } 36 | 37 | renderNameForm() { 38 | return ( 39 |
40 | 43 | {this.userName && ( 44 |
45 | You're now a hyperdomsta {this.userName} 46 |
47 | )} 48 | {this.userName && Have a beer} 49 |
50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /storeCache.js: -------------------------------------------------------------------------------- 1 | module.exports = StoreCache 2 | 3 | function StoreCache () { 4 | this.data = {} 5 | this.loadPromises = [] 6 | 7 | var self = this 8 | this.refreshify = function (fn) { 9 | return function () { 10 | var result = fn.apply(this, arguments) 11 | if (result && typeof result.then === 'function') { 12 | self.loadPromises.push(result) 13 | } 14 | } 15 | } 16 | } 17 | 18 | StoreCache.prototype.cache = function (key, loadFn) { 19 | var self = this 20 | 21 | var loadPromise = loadFn().then(function (data) { 22 | return (self.data[key] = data) 23 | }) 24 | 25 | return modifyPromiseChain(loadPromise, function (p) { 26 | if (!self.waitingForLoad) { 27 | self.loadPromises.push(p) 28 | } 29 | }) 30 | } 31 | 32 | function modifyPromiseChain (promise, modify) { 33 | modify(promise) 34 | 35 | var then = promise.then 36 | var _catch = promise.catch 37 | 38 | promise.then = function () { 39 | var p = then.apply(this, arguments) 40 | modifyPromiseChain(p, modify) 41 | return p 42 | } 43 | 44 | promise.catch = function () { 45 | var p = _catch.apply(this, arguments) 46 | modifyPromiseChain(p, modify) 47 | return p 48 | } 49 | 50 | return promise 51 | } 52 | 53 | StoreCache.prototype.loaded = function () { 54 | var self = this 55 | this.waitingForLoad = true 56 | return Promise.all(this.loadPromises).then(function () { 57 | self.waitingForLoad = false 58 | self.loadPromises = [] 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # hyperdom [![npm version](https://img.shields.io/npm/v/hyperdom.svg)](https://www.npmjs.com/package/hyperdom) [![npm](https://img.shields.io/npm/dm/hyperdom.svg)](https://www.npmjs.com/package/hyperdom) [![Build Status](https://travis-ci.org/featurist/hyperdom.svg?branch=master)](https://travis-ci.org/featurist/hyperdom) 2 | 3 | 4 | 5 | A simple, fast, feature rich framework for building dynamic browser applications. 6 | 7 | Hyperdom supports a simple event-update-render cycle, promises for asynchronous operations, JSX, non-JSX, typescript, client-side routing, SVG, two-way data binding, server-side rendering, and optimises for performance, developer usability and simplicity of application architecture. 8 | 9 | Hyperdom is influenced by [react](http://facebook.github.io/react/) and uses [virtual-dom](https://github.com/Matt-Esch/virtual-dom) for the DOM patching. Unlike react though, there is no need for state management libraries (this problem simply does not exist in hyperdom), the API is compact and the routing is built in. 10 | 11 | Documentation - https://hyperdom.org 12 | 13 | ## We're Hiring! 14 | 15 | Join our remote team and help us build amazing software. Check out [our career opportunities](https://www.featurist.co.uk/careers/). 16 | 17 | Sponsored by: 18 | 19 | [![Browserstack](https://www.browserstack.com/images/mail/newsletter-bs-logo.png)](https://www.Browserstack.com/). 20 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Hyperdom is a simple, fast, feature rich framework for building dynamic browser applications. 4 | 5 | Hyperdom supports: 6 | - a simple event-update-render cycle 7 | - promises for asynchronous operations 8 | - JSX 9 | - non-JSX 10 | - typescript 11 | - client-side routing 12 | - SVG 13 | - two-way data binding 14 | - server-side rendering 15 | 16 | Hyperdom optimises for performance, developer usability and simplicity of application architecture. 17 | 18 | Hyperdom is influenced by react and uses virtual-dom for the DOM patching. Unlike react though, there is no need for state management libraries (this problem simply does not exist in hyperdom), the API is compact and the routing is built in. 19 | 20 | ## Size 21 | 22 | * `hyperdom.min.js`: 31K 23 | * `hyperdom.min.js.gz`: 9.8K 24 | 25 | ## Browser Support 26 | 27 | * IE 9, 10, 11 28 | * Edge 29 | * Safari 30 | * Safari iOS 31 | * Firefox 32 | * Chrome 33 | 34 | Other browsers are likely to work but aren't routinely tested. 35 | 36 | ## Sister Projects 37 | 38 | * [hyperdom-ace-editor](https://github.com/featurist/hyperdom-ace-editor) 39 | * [hyperdom-draggabilly](https://github.com/featurist/hyperdom-draggabilly) 40 | * [hyperdom-medium-editor](https://github.com/featurist/hyperdom-medium-editor) 41 | * [hyperdom-ckeditor](https://github.com/featurist/hyperdom-ckeditor) 42 | * [hyperdom-semantic-ui](https://github.com/featurist/hyperdom-semantic-ui) 43 | * [hyperdom-sortable](https://github.com/featurist/hyperdom-sortable) 44 | * [hyperdom-zeroclipboard](https://github.com/featurist/hyperdom-zeroclipboard) 45 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-compose/browser/BeerList.jsx: -------------------------------------------------------------------------------- 1 | import hyperdom from 'hyperdom'; 2 | import styles from './styles.css'; 3 | 4 | export default class BeerList { 5 | 6 | async getBeers() { 7 | delete this.beers; 8 | this.isLoadingBeer = true; 9 | 10 | const response = await fetch("https://api.punkapi.com/v2/beers"); 11 | this.beers = await response.json(); 12 | 13 | this.isLoadingBeer = false; 14 | } 15 | 16 | renderTable() { 17 | if (this.beers) { 18 | return ( 19 |
20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | {this.beers.map(({name, tagline, image_url}) => { 30 | return ( 31 | 32 | 35 | 36 | 37 | 38 | ) 39 | })} 40 | 41 |
24 | NameTagline
33 | 34 | {name}{tagline}
42 |
43 | ); 44 | } 45 | } 46 | 47 | render() { 48 | if (this.isLoadingBeer) { 49 | return
Loading...
50 | } else { 51 | return ( 52 |
53 | 54 | {this.renderTable()} 55 |
56 | ); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /domComponent.js: -------------------------------------------------------------------------------- 1 | var createElement = require('virtual-dom/create-element') 2 | var diff = require('virtual-dom/diff') 3 | var patch = require('virtual-dom/patch') 4 | var toVdom = require('./toVdom') 5 | var isVdom = require('./isVdom') 6 | 7 | function DomComponent (options) { 8 | this.document = options && options.document 9 | } 10 | 11 | function prepareVdom (object) { 12 | var vdom = toVdom(object) 13 | if (!isVdom(vdom)) { 14 | throw new Error('expected render to return vdom') 15 | } else { 16 | return vdom 17 | } 18 | } 19 | 20 | DomComponent.prototype.create = function (vdom) { 21 | this.vdom = prepareVdom(vdom) 22 | return (this.element = createElement(this.vdom, {document: this.document})) 23 | } 24 | 25 | DomComponent.prototype.merge = function (vdom, element) { 26 | this.vdom = prepareVdom(vdom) 27 | return (this.element = element) 28 | } 29 | 30 | DomComponent.prototype.update = function (vdom) { 31 | var oldVdom = this.vdom 32 | this.vdom = prepareVdom(vdom) 33 | var patches = diff(oldVdom, this.vdom) 34 | return (this.element = patch(this.element, patches)) 35 | } 36 | 37 | DomComponent.prototype.destroy = function (options) { 38 | function destroyWidgets (vdom) { 39 | if (vdom.type === 'Widget') { 40 | vdom.destroy() 41 | } else if (vdom.children) { 42 | vdom.children.forEach(destroyWidgets) 43 | } 44 | } 45 | 46 | destroyWidgets(this.vdom) 47 | 48 | if (options && options.removeElement && this.element.parentNode) { 49 | this.element.parentNode.removeChild(this.element) 50 | } 51 | } 52 | 53 | function domComponent (options) { 54 | return new DomComponent(options) 55 | } 56 | 57 | exports.create = domComponent 58 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-routing/browser/BeerList.jsx: -------------------------------------------------------------------------------- 1 | import hyperdom from 'hyperdom'; 2 | import routes from './routes'; 3 | import styles from './styles.css'; 4 | import Beer from './Beer'; 5 | 6 | export default class BeerList { 7 | constructor() { 8 | this.showBeer = new Beer(this); 9 | } 10 | 11 | routes() { 12 | return [ 13 | routes.beers({ 14 | onload: async () => { 15 | if (!this.beers) { 16 | const response = await fetch("https://api.punkapi.com/v2/beers"); 17 | this.beers = await response.json(); 18 | } 19 | }, 20 | render: () => { 21 | return
{!this.beers ? "Loading..." : this.renderTable()}
; 22 | } 23 | }), 24 | this.showBeer 25 | ]; 26 | } 27 | 28 | renderTable() { 29 | return ( 30 |
31 | 32 | 33 | 34 | 36 | 37 | 39 | 40 | 41 | {this.beers.map(({ id, name, tagline, image_url }) => { 42 | return ( 43 | 44 | 47 | 48 | 49 | 52 | 53 | ); 54 | })} 55 | 56 |
35 | NameTagline 38 |
45 | 46 | {name}{tagline} 50 | show 51 |
57 |
58 | ); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-compose-ts/browser/BeerList.tsx: -------------------------------------------------------------------------------- 1 | import * as hyperdom from "hyperdom"; 2 | import * as styles from "./styles.css"; 3 | 4 | interface Beer { 5 | name: string; 6 | tagline: string; 7 | image_url: string; 8 | } 9 | 10 | export default class BeerList extends hyperdom.RenderComponent { 11 | private isLoadingBeer = false; 12 | private beers: Array = []; 13 | 14 | render() { 15 | if (this.isLoadingBeer) { 16 | return
Loading...
; 17 | } else { 18 | return ( 19 |
20 | 23 | {this.renderTable()} 24 |
25 | ); 26 | } 27 | } 28 | 29 | private renderTable() { 30 | if (!this.beers.length) { 31 | return; 32 | } 33 | 34 | return ( 35 | 36 | 37 | 38 | 40 | 41 | 42 | 43 | 44 | {this.beers.map(({ name, tagline, image_url }) => { 45 | return ( 46 | 47 | 50 | 51 | 52 | 53 | ); 54 | })} 55 | 56 |
39 | NameTagline
48 | 49 | {name}{tagline}
57 | ); 58 | } 59 | 60 | private async getBeers() { 61 | this.isLoadingBeer = true; 62 | 63 | const response = await fetch("https://api.punkapi.com/v2/beers"); 64 | this.beers = await response.json(); 65 | 66 | this.isLoadingBeer = false; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /render.js: -------------------------------------------------------------------------------- 1 | var simplePromise = require('./simplePromise') 2 | 3 | function runRender (mount, fn) { 4 | if (runRender._currentRender) { 5 | return 6 | } 7 | 8 | var render = new Render(mount) 9 | try { 10 | runRender._currentRender = render 11 | 12 | var vdom = fn() 13 | render.finished.fulfill() 14 | return vdom 15 | } finally { 16 | runRender._currentRender = undefined 17 | } 18 | } 19 | runRender._currentRender = undefined 20 | 21 | function Render (mount) { 22 | this.finished = simplePromise() 23 | this.mount = mount 24 | this.attachment = mount 25 | } 26 | 27 | Render.prototype.transformFunctionAttribute = function () { 28 | return this.mount.transformFunctionAttribute.apply(this.mount, arguments) 29 | } 30 | 31 | module.exports = runRender 32 | module.exports.currentRender = currentRender 33 | module.exports.refreshify = refreshify 34 | module.exports.RefreshHook = RefreshHook 35 | 36 | function currentRender () { 37 | return runRender._currentRender || defaultRender 38 | } 39 | 40 | var defaultRender = { 41 | mount: { 42 | setupModelComponent: function () { }, 43 | refreshify: function (fn) { return fn } 44 | }, 45 | 46 | transformFunctionAttribute: function (key, value) { 47 | return new RefreshHook(value) 48 | }, 49 | 50 | finished: { 51 | then: function (fn) { 52 | fn() 53 | } 54 | } 55 | } 56 | 57 | function refreshify (fn, options) { 58 | return runRender.currentRender().mount.refreshify(fn, options) 59 | } 60 | 61 | function RefreshHook (handler) { 62 | this.handler = handler 63 | } 64 | 65 | RefreshHook.prototype.hook = function (node, property) { 66 | node[property] = refreshify(this.handler) 67 | } 68 | 69 | RefreshHook.prototype.unhook = function (node, property) { 70 | node[property] = null 71 | } 72 | -------------------------------------------------------------------------------- /docs/readme.markdown: -------------------------------------------------------------------------------- 1 | ## Hyperdom docs website 2 | 3 | The website - https://hyperdom.org - hosted on github-pages (`gh-pages` branch). 4 | 5 | It's assembled together from individual `.md` files at runtime (that is, when user navigates to the website) by [docsify](https://docsify.js.org/#/) 6 | 7 | ### Runnable examples 8 | 9 | Hyperdom docs website features embedded codesanbox examples. Those live in `/docs/codesandbox` and turned into `iframe`s at runtime as well. 10 | 11 | A special link in the markdown source facilitates the trick. E.g.: 12 | 13 | ``` 14 | [codesandbox](https://codesandbox.io/embed/github/featurist/hyperdom/tree/master/docs/codesandbox/get-started-init?fontsize=14) 15 | ``` 16 | 17 | will result in a codesandbox iframe containing the project from `/docs/codesandbox/get-started-init` 18 | 19 | #### Example code blocks 20 | 21 | `/docs/codesandbox` are also used to populate code example blocks (not to be mixed with runnable examples). 22 | 23 | This is done with another magic link. E.g.: 24 | ``` 25 | [view code](docs/codesandbox/get-started-events/src/browser/app.jsx#L3) 26 | ``` 27 | 28 | This it __NOT__ a runtime thing though and it requires preprocessing. It's built into `yarn dev-website` and `yarn publish-website` so you don't need to do anything. 29 | 30 | ### Development 31 | 32 | #### Serve docs locally 33 | 34 | ``` 35 | yarn docs 36 | ``` 37 | 38 | This runs the above and then starts serving `./docs/index.html` 39 | 40 | #### Watch and rebuild code example blocks 41 | 42 | You could use [entr](http://eradman.com/entrproject/): 43 | 44 | ``` 45 | ls docs/src/*.md | entr yarn populate-code-blocks 46 | ``` 47 | 48 | ### Publishing 49 | 50 | The following updates `gh-pages` branch with the difference between `gh-pages` and your local code: 51 | 52 | ``` 53 | yarn publish-website 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-routing-ts/browser/BeerList.tsx: -------------------------------------------------------------------------------- 1 | import * as hyperdom from "hyperdom"; 2 | import * as styles from "./styles.css"; 3 | import routes from "./routes"; 4 | import Beer from "./Beer"; 5 | 6 | export interface IBeer { 7 | id: number; 8 | name: string; 9 | tagline: string; 10 | image_url: string; 11 | } 12 | 13 | export default class BeerList extends hyperdom.RoutesComponent { 14 | public beers: Array = []; 15 | private showBeer: Beer; 16 | 17 | constructor() { 18 | super(); 19 | this.showBeer = new Beer(this); 20 | } 21 | 22 | async onload() { 23 | const response = await fetch("https://api.punkapi.com/v2/beers"); 24 | this.beers = await response.json(); 25 | } 26 | 27 | routes() { 28 | return [ 29 | routes.beers({ 30 | render: () => { 31 | return ( 32 |
{this.beers.length ? this.renderTable() : "Loading..."}
33 | ); 34 | } 35 | }), 36 | this.showBeer 37 | ]; 38 | } 39 | 40 | private renderTable() { 41 | if (!this.beers.length) { 42 | return; 43 | } 44 | 45 | return ( 46 | 47 | 48 | 49 | 51 | 52 | 54 | 55 | 56 | {this.beers.map(({ id, name, tagline, image_url }) => { 57 | return ( 58 | 59 | 62 | 63 | 64 | 67 | 68 | ); 69 | })} 70 | 71 |
50 | NameTagline 53 |
60 | 61 | {name}{tagline} 65 | show 66 |
72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/server/toHtmlSpec.ts: -------------------------------------------------------------------------------- 1 | import * as hyperdom from '../..' 2 | import toHtml = require('../../toHtml') 3 | const h = hyperdom.html 4 | import {expect} from 'chai' 5 | import {RenderComponent} from "../../index" 6 | 7 | describe('to html', function () { 8 | it('can render regular virtual dom to HTML', function () { 9 | const vdom = h('div.class', {'data-something': 'something'}, 10 | h('div', 'child'), 11 | ) 12 | 13 | expect(toHtml(vdom)).to.equal( 14 | '
child
', 15 | ) 16 | }) 17 | 18 | it('can render model components to HTML', function () { 19 | const vdom = h('div', 20 | { 21 | render () { 22 | return h('div', 'component') 23 | }, 24 | }, 25 | ) 26 | expect(toHtml(vdom)).to.equal( 27 | '
component
', 28 | ) 29 | }) 30 | 31 | it('can render top-level model components to HTML', function () { 32 | const vdom = new class extends RenderComponent { 33 | public render () { 34 | return h('div', 'component') 35 | } 36 | }() 37 | 38 | expect(toHtml(vdom)).to.equal( 39 | '
component
', 40 | ) 41 | }) 42 | 43 | it('can render view components to HTML', function () { 44 | const vdom = h('div', 45 | hyperdom.viewComponent(new class extends RenderComponent { 46 | public render () { 47 | return h('div', 'component') 48 | } 49 | }()), 50 | ) 51 | expect(toHtml(vdom)).to.equal( 52 | '
component
', 53 | ) 54 | }) 55 | 56 | it('can render top-level view components to HTML', function () { 57 | const vdom = hyperdom.viewComponent(new class extends RenderComponent { 58 | public render () { 59 | return h('div', 'component') 60 | } 61 | }()) 62 | 63 | expect(toHtml(vdom)).to.equal( 64 | '
component
', 65 | ) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /router.d.ts: -------------------------------------------------------------------------------- 1 | import {VdomFragment, Binding, Component} from "." 2 | 3 | // TODO: what else is there? 4 | export interface RouteDefinition { 5 | definition: { 6 | pattern: string, 7 | } 8 | } 9 | 10 | export type NotFound = object 11 | 12 | export interface Router { 13 | push (url: string): void 14 | 15 | reset (): void 16 | 17 | route (path: string): RouteHandler 18 | 19 | url (): string 20 | 21 | notFound (handler: (url: string, routesTried: RouteDefinition[]) => string): NotFound 22 | } 23 | 24 | export interface RouteHistory { 25 | push (url: string): void 26 | 27 | url (): string 28 | } 29 | 30 | export interface ParamsToPushMap { 31 | [param: string]: boolean 32 | } 33 | 34 | export type ParamsToPushFn = (oldParams: object, newParams: object) => boolean 35 | export type ParamsToPush = ParamsToPushMap | ParamsToPushFn 36 | 37 | export interface ParamsBindings { 38 | [param: string]: Binding 39 | } 40 | 41 | export interface RoutableComponent { 42 | bindings?: ParamsBindings 43 | push?: ParamsToPush 44 | 45 | redirect? (params: object): string | undefined 46 | 47 | render? (): VdomFragment | Component | string // TODO add `render() => string` test to hyperdomSpec 48 | // TODO: Promise ? 49 | onload? (params: object): void 50 | } 51 | 52 | export interface RouteHandler { 53 | (component: RoutableComponent): Route 54 | 55 | isActive (params?: object): boolean 56 | 57 | push (options?: object): void 58 | 59 | url (params: object): string 60 | 61 | href (params?: object): string 62 | 63 | replace (): void 64 | } 65 | 66 | export interface Routes { 67 | [route: string]: RouteHandler 68 | } 69 | 70 | export type Route = object 71 | 72 | export function reset (): void 73 | 74 | export function route (path: string): RouteHandler 75 | 76 | export function router (options: object): Router 77 | 78 | export function hash (): RouteHistory 79 | 80 | export function pushState (): RouteHistory 81 | 82 | export function memory (): RouteHistory 83 | -------------------------------------------------------------------------------- /sync.js: -------------------------------------------------------------------------------- 1 | var hyperdom = require('.') 2 | var h = hyperdom.html 3 | 4 | module.exports = function () { 5 | var refresh 6 | var throttle 7 | 8 | var promise, awaitingPromise, lastTime, lastValue, timeout 9 | 10 | var currentValue 11 | var currentFn 12 | 13 | function callFn () { 14 | if (promise) { 15 | if (!awaitingPromise) { 16 | promise.then(function () { 17 | promise = undefined 18 | awaitingPromise = undefined 19 | sync() 20 | }) 21 | 22 | awaitingPromise = true 23 | } 24 | } else { 25 | var result = currentFn(currentValue) 26 | if (result && typeof result.then === 'function') { 27 | promise = result 28 | promise.then(refresh) 29 | } 30 | valueChanged() 31 | lastTime = Date.now() 32 | } 33 | } 34 | 35 | function valueHasChanged () { 36 | return lastValue !== normalisedValue(currentValue) 37 | } 38 | 39 | function valueChanged () { 40 | lastValue = normalisedValue(currentValue) 41 | } 42 | 43 | function sync () { 44 | var now = Date.now() 45 | 46 | if (valueHasChanged()) { 47 | if (!lastTime || (lastTime + throttle < now)) { 48 | callFn() 49 | } else if (!timeout) { 50 | var timeoutDuration = lastTime - now + throttle 51 | timeout = setTimeout(function () { 52 | timeout = undefined 53 | callFn() 54 | }, timeoutDuration) 55 | } 56 | } 57 | } 58 | 59 | return function (value, options, fn) { 60 | if (typeof options === 'function') { 61 | fn = options 62 | options = undefined 63 | } 64 | 65 | refresh = h.refresh 66 | throttle = options && options.hasOwnProperty('throttle') && options.throttle !== undefined ? options.throttle : 0 67 | 68 | currentValue = value 69 | currentFn = fn 70 | 71 | sync() 72 | } 73 | } 74 | 75 | function normalisedValue (value) { 76 | return value.constructor === Object || value instanceof Array 77 | ? JSON.stringify(value) 78 | : value 79 | } 80 | -------------------------------------------------------------------------------- /serverRender.js: -------------------------------------------------------------------------------- 1 | var vdomToHtml = require('vdom-to-html') 2 | var Mount = require('./mount') 3 | var runRender = require('./render') 4 | var router = require('./router') 5 | var StoreCache = require('./storeCache') 6 | var toVdom = require('./toVdom') 7 | 8 | module.exports.hasRoute = function (app, url) { 9 | return router.hasRoute(app, url) 10 | } 11 | 12 | module.exports.render = function (app, url) { 13 | var renderRequested = false 14 | 15 | var cache = new StoreCache() 16 | var mount = new Mount(app, {window: {}, 17 | router: router.create({history: new ServerHistory(url)}), 18 | requestRender: function () { 19 | renderRequested = true 20 | }}) 21 | 22 | mount.serverRenderCache = cache 23 | mount.refreshify = cache.refreshify 24 | 25 | function renderUntilAllLoaded (mount, cache, options) { 26 | var maxRenders = typeof options === 'object' && options.hasOwnProperty('maxRenders') ? options.maxRenders : undefined 27 | var renders = typeof options === 'object' && options.hasOwnProperty('renders') ? options.renders : 0 28 | 29 | if (renders >= maxRenders) { 30 | throw new Error('page could not load all resources') 31 | } 32 | 33 | var vdom 34 | var html 35 | 36 | renderRequested = false 37 | 38 | runRender(mount, function () { 39 | vdom = toVdom(mount.render()) 40 | html = vdomToHtml(vdom) 41 | }) 42 | 43 | if (cache.loadPromises.length) { 44 | return cache.loaded().then(function () { 45 | return renderUntilAllLoaded(mount, cache, {maxRenders: maxRenders, renders: renders + 1}) 46 | }) 47 | } else if (renderRequested) { 48 | return renderUntilAllLoaded(mount, cache, {maxRenders: maxRenders, renders: renders + 1}) 49 | } else { 50 | return Promise.resolve({ 51 | vdom: vdom, 52 | html: html, 53 | data: cache.data 54 | }) 55 | } 56 | } 57 | 58 | return renderUntilAllLoaded(mount, cache, {maxRenders: 10}) 59 | } 60 | 61 | function ServerHistory (url) { 62 | this._url = url 63 | } 64 | 65 | ServerHistory.prototype.url = function () { 66 | return this._url 67 | } 68 | 69 | ServerHistory.prototype.start = function () {} 70 | -------------------------------------------------------------------------------- /refreshEventResult.js: -------------------------------------------------------------------------------- 1 | var deprecations = require('./deprecations') 2 | 3 | module.exports = refreshEventResult 4 | 5 | var norefresh = {} 6 | 7 | function norefreshFunction () { 8 | return norefresh 9 | } 10 | 11 | module.exports.norefresh = norefreshFunction 12 | 13 | function refreshEventResult (result, mount, options) { 14 | var onlyRefreshAfterPromise = options && options.refresh === 'promise' 15 | var componentToRefresh = options && options.component 16 | 17 | function handlePromiseResult (result) { 18 | var opts = cloneOptions(options) 19 | opts.refresh = undefined 20 | refreshEventResult(result, mount, opts) 21 | } 22 | 23 | if (result && typeof (result.then) === 'function') { 24 | result.then(handlePromiseResult, function (error) { 25 | handlePromiseResult(error) 26 | throw error 27 | }) 28 | } 29 | 30 | if (onlyRefreshAfterPromise) { 31 | return 32 | } 33 | 34 | if (isComponent(result)) { 35 | mount.refreshComponent(result) 36 | } else if (result instanceof Array) { 37 | for (var i = 0; i < result.length; i++) { 38 | refreshEventResult(result[i], mount, options) 39 | } 40 | } else if (componentToRefresh) { 41 | if (componentToRefresh.refreshComponent) { 42 | componentToRefresh.refreshComponent() 43 | } else { 44 | componentToRefresh.refresh() 45 | } 46 | } else if (result === norefresh) { 47 | // don't refresh; 48 | } else if (result === norefreshFunction) { 49 | deprecations.norefresh('hyperdom.norefresh is deprecated, please use hyperdom.norefresh()') 50 | // don't refresh; 51 | } else { 52 | mount.refresh() 53 | return result 54 | } 55 | } 56 | 57 | function isComponent (component) { 58 | return component && 59 | ((typeof component.init === 'function' && 60 | typeof component.update === 'function' && 61 | typeof component.destroy === 'function') || (typeof component.refreshComponent === 'function')) 62 | } 63 | 64 | function cloneOptions (options) { 65 | if (options) { 66 | return { 67 | norefresh: options.norefresh, 68 | refresh: options.refresh, 69 | component: options.component 70 | } 71 | } else { 72 | return {} 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-ajax/browser/app.jsx: -------------------------------------------------------------------------------- 1 | import hyperdom from 'hyperdom'; 2 | import styles from './styles.css'; 3 | 4 | export default class App { 5 | renderGreetings() { 6 | return ( 7 |
8 |

Hello from Hyperdom!

9 | (this.hideGreetings = true)}> 10 | Next 11 | 12 |
13 | ); 14 | } 15 | 16 | renderNameForm() { 17 | return ( 18 |
19 | 22 | { 23 | this.userName && 24 |
25 |
You're now a hyperdomsta {this.userName}
26 | 29 |
30 | } 31 | {this.isLoadingBeer ? "Loading..." : this.renderBeerList()} 32 |
33 | ); 34 | } 35 | 36 | renderBeerList() { 37 | if (!this.beers) { 38 | return; 39 | } 40 | 41 | return ( 42 | 43 | 44 | 45 | 47 | 48 | 49 | 50 | 51 | {this.beers.map(({ name, tagline, image_url }) => { 52 | return ( 53 | 54 | 57 | 58 | 59 | 60 | ); 61 | })} 62 | 63 |
46 | NameTagline
55 | 56 | {name}{tagline}
64 | ); 65 | } 66 | 67 | async getBeers() { 68 | delete this.beers; 69 | this.isLoadingBeer = true; 70 | 71 | const response = await fetch("https://api.punkapi.com/v2/beers"); 72 | this.beers = await response.json(); 73 | 74 | this.isLoadingBeer = false; 75 | } 76 | 77 | render() { 78 | return ( 79 |
{this.hideGreetings ? this.renderNameForm() : this.renderGreetings()}
80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/server/storeCacheSpec.ts: -------------------------------------------------------------------------------- 1 | import serverRenderCache = require('../../serverRenderCache') 2 | import StoreCache = require('../../storeCache') 3 | import {expect} from 'chai' 4 | import render = require('../../render') 5 | const refreshify = render.refreshify 6 | 7 | describe('store cache', function () { 8 | let storeCache: any 9 | let oldCurrentRender: any 10 | 11 | beforeEach(function () { 12 | storeCache = new StoreCache() 13 | 14 | oldCurrentRender = render._currentRender 15 | render._currentRender = { 16 | mount: { 17 | refreshify: storeCache.refreshify, 18 | serverRenderCache: storeCache, 19 | }, 20 | } 21 | }) 22 | 23 | afterEach(function () { 24 | render._currentRender = oldCurrentRender 25 | }) 26 | 27 | function load (data: string) { 28 | return wait(10).then(function () { return data }) 29 | } 30 | 31 | function wait (n: number) { 32 | return new Promise(function (resolve) { 33 | setTimeout(resolve, n) 34 | }) 35 | } 36 | 37 | it('can store data loaded', function () { 38 | let setData1: string 39 | let setData2: string 40 | 41 | refreshify(function () { 42 | return serverRenderCache('key', function () { return load('some data') }).then(function (data: string) { 43 | expect(data).to.equal('some data') 44 | return (setData1 = data) 45 | }).then(function (data: string) { 46 | return (setData2 = data) 47 | }) 48 | })() 49 | 50 | return storeCache.loaded().then(function () { 51 | expect(storeCache.data).to.eql({ 52 | key: 'some data', 53 | }) 54 | expect(setData1).to.equal('some data') 55 | expect(setData2).to.equal('some data') 56 | }) 57 | }) 58 | 59 | it("can store data even if promise isn't returned", function () { 60 | let setData: string 61 | 62 | refreshify(function () { 63 | serverRenderCache('key', function () { return load('some data') }).then(function (data: string) { 64 | expect(data).to.equal('some data') 65 | setData = data 66 | }) 67 | })() 68 | 69 | return storeCache.loaded().then(function () { 70 | expect(storeCache.data).to.eql({key: 'some data'}) 71 | expect(setData).to.equal('some data') 72 | }) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /docs/codesandbox/get-started-ajax-ts/browser/app.tsx: -------------------------------------------------------------------------------- 1 | import * as hyperdom from "hyperdom"; 2 | import * as styles from "./styles.css"; 3 | 4 | interface Beer { 5 | name: string; 6 | tagline: string; 7 | image_url: string; 8 | } 9 | 10 | export default class App extends hyperdom.RenderComponent { 11 | private hideGreetings = false; 12 | private userName = ""; 13 | private isLoadingBeer = false; 14 | private beers: Array = []; 15 | 16 | renderGreetings() { 17 | return ( 18 |
19 |

Hello from Hyperdom!

20 | (this.hideGreetings = true)}>Next 21 |
22 | ); 23 | } 24 | 25 | renderNameForm() { 26 | return ( 27 |
28 | 31 | {this.userName && ( 32 |
33 |
34 | You're now a hyperdomsta {this.userName} 35 |
36 | 39 |
40 | )} 41 | {this.isLoadingBeer ? "Loading..." : this.renderBeerList()} 42 |
43 | ); 44 | } 45 | 46 | renderBeerList() { 47 | if (!this.beers.length) { 48 | return; 49 | } 50 | 51 | return ( 52 | 53 | 54 | 55 | 57 | 58 | 59 | 60 | 61 | {this.beers.map(({ name, tagline, image_url }) => { 62 | return ( 63 | 64 | 65 | 66 | 67 | 68 | ); 69 | })} 70 | 71 |
56 | NameTagline
{name}{tagline}
72 | ); 73 | } 74 | 75 | async getBeers() { 76 | this.isLoadingBeer = true; 77 | 78 | const response = await fetch("https://api.punkapi.com/v2/beers"); 79 | this.beers = await response.json(); 80 | 81 | this.isLoadingBeer = false; 82 | } 83 | 84 | render() { 85 | return ( 86 |
{this.hideGreetings ? this.renderNameForm() : this.renderGreetings()}
87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | hyperdom - A fast, feature rich and simple framework for building dynamic browser applications. 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 |
19 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /windowEvents.js: -------------------------------------------------------------------------------- 1 | var domComponent = require('./domComponent') 2 | var rendering = require('./rendering') 3 | var VText = require('virtual-dom/vnode/vtext.js') 4 | 5 | function WindowWidget (attributes) { 6 | this.attributes = attributes 7 | this.vdom = new VText('') 8 | this.component = domComponent.create() 9 | 10 | var self = this 11 | this.cache = {} 12 | Object.keys(this.attributes).forEach(function (key) { 13 | self.cache[key] = rendering.html.refreshify(self.attributes[key]) 14 | }) 15 | } 16 | 17 | WindowWidget.prototype.type = 'Widget' 18 | 19 | WindowWidget.prototype.init = function () { 20 | applyPropertyDiffs(window, {}, this.attributes, {}, this.cache) 21 | return (this.element = document.createTextNode('')) 22 | } 23 | 24 | function uniq (array) { 25 | var sortedArray = array.slice() 26 | sortedArray.sort() 27 | 28 | var last 29 | 30 | for (var n = 0; n < sortedArray.length;) { 31 | var current = sortedArray[n] 32 | 33 | if (last === current) { 34 | sortedArray.splice(n, 1) 35 | } else { 36 | n++ 37 | } 38 | last = current 39 | } 40 | 41 | return sortedArray 42 | } 43 | 44 | function applyPropertyDiffs (element, previous, current, previousCache, currentCache) { 45 | uniq(Object.keys(previous).concat(Object.keys(current))).forEach(function (key) { 46 | if (/^on/.test(key)) { 47 | var event = key.slice(2) 48 | 49 | var prev = previous[key] 50 | var curr = current[key] 51 | var refreshPrev = previousCache[key] 52 | var refreshCurr = currentCache[key] 53 | 54 | if (prev !== undefined && curr === undefined) { 55 | element.removeEventListener(event, refreshPrev) 56 | } else if (prev !== undefined && curr !== undefined && prev !== curr) { 57 | element.removeEventListener(event, refreshPrev) 58 | element.addEventListener(event, refreshCurr) 59 | } else if (prev === undefined && curr !== undefined) { 60 | element.addEventListener(event, refreshCurr) 61 | } 62 | } 63 | }) 64 | } 65 | 66 | WindowWidget.prototype.update = function (previous) { 67 | applyPropertyDiffs(window, previous.attributes, this.attributes, previous.cache, this.cache) 68 | this.component = previous.component 69 | return this.element 70 | } 71 | 72 | WindowWidget.prototype.destroy = function () { 73 | applyPropertyDiffs(window, this.attributes, {}, this.cache, {}) 74 | } 75 | 76 | module.exports = function (attributes) { 77 | return new WindowWidget(attributes) 78 | } 79 | -------------------------------------------------------------------------------- /test/browser/tsxSpec.tsx: -------------------------------------------------------------------------------- 1 | import * as $ from 'jquery' 2 | import * as browserMonkey from 'browser-monkey' 3 | import * as hyperdom from "../.." 4 | 5 | const browser = browserMonkey.find('.test') 6 | 7 | describe('tsx integration', function () { 8 | let $div: HTMLElement 9 | 10 | function mount (app: hyperdom.Component | hyperdom.FnComponent) { 11 | hyperdom.append($div, app) 12 | } 13 | 14 | beforeEach(function () { 15 | $('.test').remove() 16 | $div = $('
').appendTo(document.body)[0] 17 | }) 18 | 19 | it('renders standard html nodes', async function () { 20 | function render () { 21 | return
Blue
22 | } 23 | mount(render) 24 | await browser.find('div').shouldHave({text: 'Blue'}) 25 | }) 26 | 27 | // just checking compilation errors here - hence xit 28 | xit('supports hyperdom specific node attributes', function () { 29 | function render (this: hyperdom.FnComponent) { 30 | return ( 31 |
32 | 33 |
    34 |
  • one
  • 35 |
  • two
  • 36 |
  • three
  • 37 |
38 | 39 | 40 |
41 | 42 | 43 |
44 | ) 45 | } 46 | 47 | class Circle extends hyperdom.RenderComponent { 48 | public render () { 49 | return 50 | 51 | 52 | } 53 | } 54 | }) 55 | 56 | it('renders hyperdom viewComponent', async function () { 57 | class Blue extends hyperdom.RenderComponent { 58 | private readonly title: string 59 | 60 | constructor (properties: { title: string }, readonly children: hyperdom.Renderable[]) { 61 | super() 62 | this.title = properties.title 63 | } 64 | 65 | public render () { 66 | return ( 67 |
68 |

{this.title}

69 | {this.children} 70 |
71 | ) 72 | } 73 | } 74 | 75 | function render () { 76 | return ( 77 |
78 | 79 |
Orange
80 |
Green
81 |
82 |
83 | ) 84 | } 85 | mount(render) 86 | await browser.find('div h1').shouldHave({text: 'Blue'}) 87 | await browser.find('.orange').shouldHave({text: 'Orange'}) 88 | await browser.find('.green').shouldHave({text: 'Green'}) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /prepareAttributes.js: -------------------------------------------------------------------------------- 1 | var render = require('./render') 2 | var bindModel = require('./bindModel') 3 | 4 | module.exports = function (tag, attributes, childElements) { 5 | var dataset 6 | var currentRender = render.currentRender() 7 | 8 | if (attributes.binding) { 9 | bindModel(tag, attributes, childElements) 10 | delete attributes.binding 11 | } 12 | 13 | var keys = Object.keys(attributes) 14 | for (var k = 0; k < keys.length; k++) { 15 | var key = keys[k] 16 | var attribute = attributes[key] 17 | 18 | if (typeof (attribute) === 'function' && currentRender) { 19 | attributes[key] = currentRender.transformFunctionAttribute(key, attribute) 20 | } 21 | 22 | var rename = renames[key] 23 | if (rename) { 24 | attributes[rename] = attribute 25 | delete attributes[key] 26 | continue 27 | } 28 | 29 | if (dataAttributeRegex.test(key)) { 30 | if (!dataset) { 31 | dataset = attributes.dataset 32 | 33 | if (!dataset) { 34 | dataset = attributes.dataset = {} 35 | } 36 | } 37 | 38 | var datakey = key 39 | .replace(dataAttributeRegex, '') 40 | .replace(/-([a-z])/ig, function (_, x) { return x.toUpperCase() }) 41 | 42 | dataset[datakey] = attribute 43 | delete attributes[key] 44 | continue 45 | } 46 | } 47 | 48 | if (process.env.NODE_ENV !== 'production' && attributes.__source) { 49 | if (!dataset) { 50 | dataset = attributes.dataset 51 | 52 | if (!dataset) { 53 | dataset = attributes.dataset = {} 54 | } 55 | } 56 | 57 | dataset.fileName = attributes.__source.fileName 58 | dataset.lineNumber = attributes.__source.lineNumber 59 | } 60 | 61 | if (attributes.className) { 62 | attributes.className = generateClassName(attributes.className) 63 | } 64 | 65 | if (attributes.innerHTML === false) { 66 | delete attributes.innerHTML 67 | } 68 | 69 | return attributes 70 | } 71 | 72 | var renames = { 73 | for: 'htmlFor', 74 | class: 'className', 75 | contenteditable: 'contentEditable', 76 | tabindex: 'tabIndex', 77 | colspan: 'colSpan' 78 | } 79 | 80 | var dataAttributeRegex = /^data-/ 81 | 82 | function generateClassName (obj) { 83 | if (typeof (obj) === 'object') { 84 | if (obj instanceof Array) { 85 | var names = obj.map(function (item) { 86 | return generateClassName(item) 87 | }) 88 | return names.join(' ') || undefined 89 | } else { 90 | return generateConditionalClassNames(obj) 91 | } 92 | } else { 93 | return obj 94 | } 95 | } 96 | 97 | function generateConditionalClassNames (obj) { 98 | return Object.keys(obj).filter(function (key) { 99 | return obj[key] 100 | }).join(' ') || undefined 101 | } 102 | -------------------------------------------------------------------------------- /mapBinding.js: -------------------------------------------------------------------------------- 1 | var bindingMeta = require('./meta') 2 | 3 | function makeConverter (converter) { 4 | if (typeof converter === 'function') { 5 | return { 6 | view: function (model) { 7 | return model 8 | }, 9 | model: function (view) { 10 | return converter(view) 11 | } 12 | } 13 | } else { 14 | return converter 15 | } 16 | } 17 | 18 | function chainConverters (startIndex, converters) { 19 | function makeConverters () { 20 | if (!_converters) { 21 | _converters = new Array(converters.length - startIndex) 22 | 23 | for (var n = startIndex; n < converters.length; n++) { 24 | _converters[n - startIndex] = makeConverter(converters[n]) 25 | } 26 | } 27 | } 28 | 29 | if ((converters.length - startIndex) === 1) { 30 | return makeConverter(converters[startIndex]) 31 | } else { 32 | var _converters 33 | return { 34 | view: function (model) { 35 | makeConverters() 36 | var intermediateValue = model 37 | for (var n = 0; n < _converters.length; n++) { 38 | intermediateValue = _converters[n].view(intermediateValue) 39 | } 40 | return intermediateValue 41 | }, 42 | 43 | model: function (view) { 44 | makeConverters() 45 | var intermediateValue = view 46 | for (var n = _converters.length - 1; n >= 0; n--) { 47 | intermediateValue = _converters[n].model(intermediateValue) 48 | } 49 | return intermediateValue 50 | } 51 | } 52 | } 53 | } 54 | 55 | module.exports = function (model, property) { 56 | var _meta 57 | function hyperdomMeta () { 58 | return _meta || (_meta = bindingMeta(model, property)) 59 | } 60 | 61 | var converter = chainConverters(2, arguments) 62 | 63 | return { 64 | get: function () { 65 | var meta = hyperdomMeta() 66 | var modelValue = model[property] 67 | var modelText 68 | 69 | if (meta.error) { 70 | return meta.view 71 | } else if (meta.view === undefined) { 72 | modelText = converter.view(modelValue) 73 | meta.view = modelText 74 | return modelText 75 | } else { 76 | var previousValue 77 | try { 78 | previousValue = converter.model(meta.view) 79 | } catch (e) { 80 | meta.error = e 81 | return meta.view 82 | } 83 | modelText = converter.view(modelValue) 84 | var normalisedPreviousText = converter.view(previousValue) 85 | 86 | if (modelText === normalisedPreviousText) { 87 | return meta.view 88 | } else { 89 | meta.view = modelText 90 | return modelText 91 | } 92 | } 93 | }, 94 | 95 | set: function (view) { 96 | var meta = hyperdomMeta() 97 | meta.view = view 98 | 99 | try { 100 | model[property] = converter.model(view, model[property]) 101 | delete meta.error 102 | } catch (e) { 103 | meta.error = e 104 | } 105 | }, 106 | 107 | meta: function () { 108 | return hyperdomMeta() 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /xml.js: -------------------------------------------------------------------------------- 1 | var AttributeHook = require('virtual-dom/virtual-hyperscript/hooks/attribute-hook') 2 | 3 | var namespaceRegex = /^([a-z0-9_-]+)(--|:)([a-z0-9_-]+)$/i 4 | var xmlnsRegex = /^xmlns(--|:)([a-z0-9_-]+)$/i 5 | var SVG_NAMESPACE = 'http://www.w3.org/2000/svg' 6 | 7 | function transformTanName (vnode, namespaces) { 8 | var tagNamespace = namespaceRegex.exec(vnode.tagName) 9 | if (tagNamespace) { 10 | var namespaceKey = tagNamespace[1] 11 | var namespace = namespaces[namespaceKey] 12 | if (namespace) { 13 | vnode.tagName = tagNamespace[1] + ':' + tagNamespace[3] 14 | vnode.namespace = namespace 15 | } 16 | } else if (!vnode.namespace) { 17 | vnode.namespace = namespaces[''] 18 | } 19 | } 20 | 21 | function transformProperties (vnode, namespaces) { 22 | var properties = vnode.properties 23 | 24 | if (properties) { 25 | var attributes = properties.attributes || (properties.attributes = {}) 26 | 27 | var keys = Object.keys(properties) 28 | for (var k = 0, l = keys.length; k < l; k++) { 29 | var key = keys[k] 30 | if (key !== 'style' && key !== 'attributes') { 31 | var match = namespaceRegex.exec(key) 32 | if (match) { 33 | properties[match[1] + ':' + match[3]] = new AttributeHook(namespaces[match[1]], properties[key]) 34 | delete properties[key] 35 | } else { 36 | if (vnode.namespace === SVG_NAMESPACE && key === 'className') { 37 | attributes['class'] = properties.className 38 | delete properties.className 39 | } else { 40 | var property = properties[key] 41 | var type = typeof property 42 | if (type === 'string' || type === 'number' || type === 'boolean') { 43 | attributes[key] = property 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | 52 | function declaredNamespaces (vnode) { 53 | var namespaces = { 54 | '': vnode.properties.xmlns, 55 | xmlns: 'http://www.w3.org/2000/xmlns/' 56 | } 57 | 58 | var keys = Object.keys(vnode.properties) 59 | 60 | for (var k = 0, l = keys.length; k < l; k++) { 61 | var key = keys[k] 62 | var value = vnode.properties[key] 63 | 64 | if (key === 'xmlns') { 65 | namespaces[''] = value 66 | } else { 67 | var match = xmlnsRegex.exec(key) 68 | 69 | if (match) { 70 | namespaces[match[2]] = value 71 | } 72 | } 73 | } 74 | 75 | return namespaces 76 | } 77 | 78 | function transform (vnode) { 79 | var namespaces = declaredNamespaces(vnode) 80 | 81 | function transformChildren (vnode, namespaces) { 82 | transformTanName(vnode, namespaces) 83 | transformProperties(vnode, namespaces) 84 | 85 | if (vnode.children) { 86 | for (var c = 0, l = vnode.children.length; c < l; c++) { 87 | var child = vnode.children[c] 88 | if (!(child.properties && child.properties.xmlns)) { 89 | transformChildren(child, namespaces) 90 | } 91 | } 92 | } 93 | } 94 | 95 | transformChildren(vnode, namespaces) 96 | 97 | return vnode 98 | } 99 | 100 | module.exports.transform = transform 101 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at enquiries@featurist.co.uk. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperdom", 3 | "version": "2.1.0", 4 | "description": "A fast, feature rich and simple framework for building dynamic browser applications.", 5 | "main": "index.js", 6 | "dependencies": { 7 | "vdom-parser": "1.3.4", 8 | "vdom-to-html": "2.3.1", 9 | "virtual-dom": "2.1.1" 10 | }, 11 | "devDependencies": { 12 | "@types/chai": "^4.1.4", 13 | "@types/jquery": "^3.3.6", 14 | "@types/jsdom": "^11.12.0", 15 | "@types/mocha": "^5.2.5", 16 | "babel-preset-hyperdom": "^1.0.0", 17 | "browser-monkey": "2.4.1", 18 | "chai": "3.5.0", 19 | "codesandbox-example-links": "^1.0.0", 20 | "detect-browser": "1.6.2", 21 | "docsify-cli": "^4.3.0", 22 | "electron": "1.8.2", 23 | "electron-mocha": "3.4.0", 24 | "eslint": "7.22.0", 25 | "eslint-config-standard": "16.0.2", 26 | "eslint-plugin-es5": "1.5.0", 27 | "eslint-plugin-import": "2.22.1", 28 | "eslint-plugin-node": "11.1.0", 29 | "eslint-plugin-promise": "4.3.1", 30 | "eslint-plugin-standard": "5.0.0", 31 | "gh-pages": "^2.0.1", 32 | "hyperx": "2.3.0", 33 | "jquery": "3.3.1", 34 | "jquery-sendkeys": "4.0.0", 35 | "jsdom": "12.1.0", 36 | "karma": "3.0.0", 37 | "karma-browserstack-launcher": "1.5.1", 38 | "karma-chrome-launcher": "2.2.0", 39 | "karma-cli": "1.0.1", 40 | "karma-electron-launcher": "0.2.0", 41 | "karma-firefox-launcher": "1.1.0", 42 | "karma-ievms": "0.1.0", 43 | "karma-mocha": "1.3.0", 44 | "karma-mocha-reporter": "2.2.5", 45 | "karma-safari-launcher": "1.0.0", 46 | "karma-sourcemap-loader": "^0.3.7", 47 | "karma-webpack": "^3.0.5", 48 | "lie": "3.1.1", 49 | "lowscore": "1.12.1", 50 | "mocha": "3.2.0", 51 | "trytryagain": "1.2.0", 52 | "ts-loader": "^5.2.2", 53 | "ts-node": "^7.0.1", 54 | "tslint": "^5.11.0", 55 | "typescript": "^3.2.1", 56 | "typescript-tslint-plugin": "^0.1.2", 57 | "uglify-js": "3.6.0", 58 | "watchify": "3.9.0", 59 | "webpack": "4.35.0", 60 | "webpack-cli": "^3.3.5" 61 | }, 62 | "scripts": { 63 | "test": "./node_modules/.bin/tsc && npm run karma && npm run mocha && eslint . && npm run tslint", 64 | "test-all": "BROWSERS=all npm test", 65 | "tslint": "tslint --project . *.d.ts test/**/*.ts{,x}", 66 | "karma": "karma start --single-run", 67 | "mocha": "TS_NODE_FILES=true mocha -r ts-node/register test/server/*Spec.ts", 68 | "build": "webpack index.js && uglifyjs --compress --mangle -o dist/hyperdom.min.js dist/main.js", 69 | "size": "npm run build && gzip < dist/hyperdom.min.js > dist/hyperdom.min.js.gz && ls -lh dist/hyperdom.*", 70 | "dev-website": "yarn build-website && docsify serve ./website-dist", 71 | "watch-docs": "ls docs/*.md | entr yarn codesandbox-example-links --output-dir=./website-dist ./docs/*.md", 72 | "build-website": "rm -rf ./website-dist && cp -r ./docs ./website-dist && codesandbox-example-links --output-dir=./website-dist ./docs/*.md", 73 | "publish-website": "yarn build-website && gh-pages -t -d website-dist" 74 | }, 75 | "keywords": [ 76 | "virtual-dom", 77 | "front-end", 78 | "mvc", 79 | "framework", 80 | "html", 81 | "plastiq", 82 | "hyperdom" 83 | ], 84 | "author": "Tim Macfarlane ", 85 | "license": "MIT", 86 | "files": [ 87 | "*.js", 88 | "*.d.ts", 89 | "*.ts" 90 | ], 91 | "repository": { 92 | "type": "git", 93 | "url": "https://github.com/featurist/hyperdom.git" 94 | }, 95 | "bugs": { 96 | "url": "https://github.com/featurist/hyperdom/issues" 97 | }, 98 | "homepage": "https://github.com/featurist/hyperdom" 99 | } 100 | -------------------------------------------------------------------------------- /componentWidget.js: -------------------------------------------------------------------------------- 1 | var VText = require('virtual-dom/vnode/vtext.js') 2 | var domComponent = require('./domComponent') 3 | var render = require('./render') 4 | var deprecations = require('./deprecations') 5 | 6 | function ComponentWidget (state, vdom) { 7 | if (!vdom) { 8 | throw new Error('hyperdom.html.component([options], vdom) expects a vdom argument') 9 | } 10 | 11 | this.state = state 12 | this.key = state.key 13 | var currentRender = render.currentRender() 14 | 15 | if (typeof vdom === 'function') { 16 | this.render = function () { 17 | if (currentRender && state.on) { 18 | currentRender.transformFunctionAttribute = function (key, value) { 19 | return state.on(key.replace(/^on/, ''), value) 20 | } 21 | } 22 | return vdom.apply(this.state, arguments) 23 | } 24 | this.canRefresh = true 25 | } else { 26 | vdom = vdom || new VText('') 27 | this.render = function () { 28 | return vdom 29 | } 30 | } 31 | this.cacheKey = state.cacheKey 32 | this.component = domComponent.create() 33 | 34 | var renderFinished = currentRender && currentRender.finished 35 | if (renderFinished) { 36 | this.afterRender = function (fn) { 37 | renderFinished.then(fn) 38 | } 39 | } else { 40 | this.afterRender = function () {} 41 | } 42 | } 43 | 44 | ComponentWidget.prototype.type = 'Widget' 45 | 46 | ComponentWidget.prototype.init = function () { 47 | var self = this 48 | 49 | if (self.state.onbeforeadd) { 50 | self.state.onbeforeadd() 51 | } 52 | 53 | var vdom = this.render(this) 54 | if (vdom instanceof Array) { 55 | throw new Error('vdom returned from component cannot be an array') 56 | } 57 | 58 | var element = this.component.create(vdom) 59 | 60 | if (self.state.onadd) { 61 | this.afterRender(function () { 62 | self.state.onadd(element) 63 | }) 64 | } 65 | 66 | if (self.state.detached) { 67 | return document.createTextNode('') 68 | } else { 69 | return element 70 | } 71 | } 72 | 73 | ComponentWidget.prototype.update = function (previous) { 74 | var self = this 75 | 76 | var refresh = !this.cacheKey || this.cacheKey !== previous.cacheKey 77 | 78 | if (refresh) { 79 | if (self.state.onupdate) { 80 | this.afterRender(function () { 81 | self.state.onupdate(self.component.element) 82 | }) 83 | } 84 | } 85 | 86 | this.component = previous.component 87 | 88 | if (previous.state && this.state) { 89 | var keys = Object.keys(this.state) 90 | for (var n = 0; n < keys.length; n++) { 91 | var key = keys[n] 92 | previous.state[key] = self.state[key] 93 | } 94 | this.state = previous.state 95 | } 96 | 97 | if (refresh) { 98 | var element = this.component.update(this.render(this)) 99 | 100 | if (self.state.detached) { 101 | return document.createTextNode('') 102 | } else { 103 | return element 104 | } 105 | } 106 | } 107 | 108 | ComponentWidget.prototype.refresh = function () { 109 | this.component.update(this.render(this)) 110 | if (this.state.onupdate) { 111 | this.state.onupdate(this.component.element) 112 | } 113 | } 114 | 115 | ComponentWidget.prototype.destroy = function (element) { 116 | var self = this 117 | 118 | if (self.state.onremove) { 119 | this.afterRender(function () { 120 | self.state.onremove(element) 121 | }) 122 | } 123 | 124 | this.component.destroy() 125 | } 126 | 127 | module.exports = function (state, vdom) { 128 | deprecations.component('hyperdom.html.component is deprecated, please use hyperdom.viewComponent') 129 | if (typeof state === 'function') { 130 | return new ComponentWidget({}, state) 131 | } else if (state.constructor === Object) { 132 | return new ComponentWidget(state, vdom) 133 | } else { 134 | return new ComponentWidget({}, state) 135 | } 136 | } 137 | 138 | module.exports.ComponentWidget = ComponentWidget 139 | -------------------------------------------------------------------------------- /mount.js: -------------------------------------------------------------------------------- 1 | var hyperdomMeta = require('./meta') 2 | var runRender = require('./render') 3 | var domComponent = require('./domComponent') 4 | var Set = require('./set') 5 | var refreshEventResult = require('./refreshEventResult') 6 | 7 | var lastId = 0 8 | 9 | function Mount (model, options) { 10 | var win = (options && options.window) || window 11 | var router = typeof options === 'object' && options.hasOwnProperty('router') ? options.router : undefined 12 | this.requestRender = (options && options.requestRender) || win.requestAnimationFrame || win.setTimeout 13 | 14 | this.document = (options && options.document) || document 15 | this.model = model 16 | 17 | this.renderQueued = false 18 | this.mountRenderRequested = false 19 | this.componentRendersRequested = undefined 20 | this.id = ++lastId 21 | this.renderId = 0 22 | this.mounted = true 23 | this.router = router 24 | } 25 | 26 | Mount.prototype.refreshify = function (fn, options) { 27 | if (!fn) { 28 | return fn 29 | } 30 | 31 | if (options && (options.norefresh === true || options.refresh === false)) { 32 | return fn 33 | } 34 | 35 | var self = this 36 | 37 | return function () { 38 | var result = fn.apply(this, arguments) 39 | return refreshEventResult(result, self, options) 40 | } 41 | } 42 | 43 | Mount.prototype.transformFunctionAttribute = function (key, value) { 44 | return this.refreshify(value) 45 | } 46 | 47 | Mount.prototype.queueRender = function () { 48 | if (!this.renderQueued) { 49 | var self = this 50 | 51 | var requestRender = this.requestRender 52 | this.renderQueued = true 53 | 54 | requestRender(function () { 55 | self.renderQueued = false 56 | 57 | if (self.mounted) { 58 | if (self.mountRenderRequested) { 59 | self.refreshImmediately() 60 | } else if (self.componentRendersRequested) { 61 | self.refreshComponentsImmediately() 62 | } 63 | } 64 | }) 65 | } 66 | } 67 | 68 | Mount.prototype.createDomComponent = function () { 69 | return domComponent.create({ document: this.document }) 70 | } 71 | 72 | Mount.prototype.render = function () { 73 | if (this.router) { 74 | return this.router.render(this.model) 75 | } else { 76 | return this.model 77 | } 78 | } 79 | 80 | Mount.prototype.refresh = function () { 81 | this.mountRenderRequested = true 82 | this.queueRender() 83 | } 84 | 85 | Mount.prototype.refreshImmediately = function () { 86 | var self = this 87 | 88 | runRender(self, function () { 89 | self.renderId++ 90 | var vdom = self.render() 91 | self.component.update(vdom) 92 | self.mountRenderRequested = false 93 | }) 94 | } 95 | 96 | Mount.prototype.refreshComponentsImmediately = function () { 97 | var self = this 98 | 99 | runRender(self, function () { 100 | for (var i = 0, l = self.componentRendersRequested.length; i < l; i++) { 101 | var w = self.componentRendersRequested[i] 102 | w.refresh() 103 | } 104 | self.componentRendersRequested = undefined 105 | }) 106 | } 107 | 108 | Mount.prototype.refreshComponent = function (component) { 109 | if (!this.componentRendersRequested) { 110 | this.componentRendersRequested = [] 111 | } 112 | 113 | this.componentRendersRequested.push(component) 114 | this.queueRender() 115 | } 116 | 117 | Mount.prototype.isComponentInDom = function (component) { 118 | var meta = hyperdomMeta(component) 119 | return meta.lastRenderId === this.renderId 120 | } 121 | 122 | Mount.prototype.setupModelComponent = function (model) { 123 | var self = this 124 | 125 | var meta = hyperdomMeta(model) 126 | 127 | if (!meta.mount) { 128 | meta.mount = this 129 | meta.components = new Set() 130 | 131 | model.refresh = function () { 132 | self.refresh() 133 | } 134 | 135 | model.refreshImmediately = function () { 136 | self.refreshImmediately() 137 | } 138 | 139 | model.refreshComponent = function () { 140 | var meta = hyperdomMeta(this) 141 | meta.components.forEach(function (w) { 142 | self.refreshComponent(w) 143 | }) 144 | } 145 | 146 | if (typeof model.onload === 'function') { 147 | this.refreshify(function () { return model.onload() }, {refresh: 'promise'})() 148 | } 149 | } 150 | } 151 | 152 | Mount.prototype.detach = function () { 153 | this.mounted = false 154 | } 155 | 156 | Mount.prototype.remove = function () { 157 | if (this.router) { 158 | this.router.reset() 159 | } 160 | this.component.destroy({removeElement: true}) 161 | this.mounted = false 162 | } 163 | 164 | module.exports = Mount 165 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sat Dec 27 2014 08:06:04 GMT+0100 (CET) 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | // frameworks to use 11 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 12 | frameworks: ['mocha'], 13 | 14 | // list of files / patterns to load in the browser 15 | files: [ 16 | 'test/browser/karma.index.ts' 17 | ], 18 | 19 | // list of files to exclude 20 | exclude: [ 21 | '**/.*.sw?' 22 | ], 23 | 24 | // preprocess matching files before serving them to the browser 25 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 26 | preprocessors: { 27 | 'test/browser/karma.index.ts': ['webpack'] 28 | }, 29 | 30 | webpack: { 31 | mode: 'development', 32 | optimization: { 33 | nodeEnv: false 34 | }, 35 | devtool: 'inline-source-map', 36 | resolve: { 37 | extensions: ['.js', '.ts', '.tsx'] 38 | }, 39 | module: { 40 | rules: [ 41 | { 42 | test: /\.tsx?$/, 43 | loader: 'ts-loader', 44 | options: { 45 | // karma does not fail on compilation errors - so get rid of typechecking to save few seconds. 46 | transpileOnly: true, 47 | compilerOptions: { 48 | noEmit: false, 49 | target: 'es5' 50 | } 51 | }, 52 | exclude: process.cwd() + '/node_modules' 53 | } 54 | ] 55 | } 56 | }, 57 | 58 | // test results reporter to use 59 | // possible values: 'dots', 'progress' 60 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 61 | reporters: process.env.BROWSERS ? ['dots'] : ['mocha'], 62 | 63 | electronOpts: { 64 | show: false 65 | }, 66 | 67 | mochaReporter: { 68 | showDiff: true 69 | }, 70 | 71 | client: { 72 | mocha: { 73 | timeout: process.env.CI ? 61000 : 2000 74 | } 75 | }, 76 | 77 | // web server port 78 | port: 9876, 79 | 80 | // enable / disable colors in the output (reporters and logs) 81 | colors: true, 82 | 83 | // level of logging 84 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 85 | logLevel: config.LOG_WARN, 86 | concurrency: process.env.BROWSERS === 'all' ? 2 : Infinity, 87 | 88 | // enable / disable watching file and executing tests whenever any file changes 89 | autoWatch: true, 90 | 91 | // start these browsers 92 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 93 | browsers: process.env.BROWSERS === 'all' ? Object.keys(browsers) : ['Chrome'], 94 | 95 | browserStack: { 96 | username: process.env.BROWSERSTACK_USER, 97 | accessKey: process.env.BROWSERSTACK_PASSWORD, 98 | captureTimeout: 300 99 | }, 100 | 101 | // Continuous Integration mode 102 | // if true, Karma captures browsers, runs the tests and exits 103 | singleRun: false, 104 | 105 | customLaunchers: browsers, 106 | 107 | browserNoActivityTimeout: 60000 108 | }) 109 | } 110 | 111 | var browsers = { 112 | 'browserstack-windows-firefox': { 113 | base: 'BrowserStack', 114 | browser: 'Firefox', 115 | os: 'Windows', 116 | os_version: '10', 117 | resolution: '1280x1024' 118 | }, 119 | // 'browserstack-osx-firefox': { 120 | // base: 'BrowserStack', 121 | // browser: 'Firefox', 122 | // os: 'OS X', 123 | // os_version: 'Mojave', 124 | // resolution: '1280x1024' 125 | // }, 126 | 'browserstack-safari': { 127 | base: 'BrowserStack', 128 | browser: 'Safari', 129 | os: 'OS X', 130 | os_version: 'Mojave', 131 | resolution: '1280x1024' 132 | }, 133 | 'browserstack-windows-chrome': { 134 | base: 'BrowserStack', 135 | browser: 'Chrome', 136 | os: 'Windows', 137 | os_version: '10', 138 | resolution: '1280x1024' 139 | }, 140 | 'browserstack-osx-chrome': { 141 | base: 'BrowserStack', 142 | browser: 'Chrome', 143 | os: 'OS X', 144 | os_version: 'Mojave', 145 | resolution: '1280x1024' 146 | }, 147 | 'browserstack-ie11': { 148 | base: 'BrowserStack', 149 | browser: 'IE', 150 | os: 'Windows', 151 | os_version: '10', 152 | resolution: '1280x1024' 153 | }, 154 | 'browserstack-edge': { 155 | base: 'BrowserStack', 156 | browser: 'Edge', 157 | os: 'Windows', 158 | os_version: '10', 159 | resolution: '1280x1024' 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /component.js: -------------------------------------------------------------------------------- 1 | var hyperdomMeta = require('./meta') 2 | var render = require('./render') 3 | var Vtext = require('virtual-dom/vnode/vtext.js') 4 | var debuggingProperties = require('./debuggingProperties') 5 | 6 | function Component (model, options) { 7 | this.isViewComponent = options && options.hasOwnProperty('viewComponent') && options.viewComponent 8 | this.model = model 9 | this.key = model.renderKey 10 | this.component = undefined 11 | } 12 | 13 | Component.prototype.type = 'Widget' 14 | 15 | Component.prototype.init = function () { 16 | var self = this 17 | 18 | var vdom = this.render() 19 | 20 | var meta = hyperdomMeta(this.model) 21 | meta.components.add(this) 22 | 23 | var currentRender = render.currentRender() 24 | this.component = currentRender.mount.createDomComponent() 25 | var element = this.component.create(vdom) 26 | 27 | if (self.model.detached) { 28 | return document.createTextNode('') 29 | } else { 30 | return element 31 | } 32 | } 33 | 34 | function beforeUpdate (model, element) { 35 | if (model.onbeforeupdate) { 36 | model.onbeforeupdate(element) 37 | } 38 | 39 | if (model.onbeforerender) { 40 | model.onbeforerender(element) 41 | } 42 | } 43 | 44 | function afterUpdate (model, element, oldElement) { 45 | if (model.onupdate) { 46 | model.onupdate(element, oldElement) 47 | } 48 | 49 | if (model.onrender) { 50 | model.onrender(element, oldElement) 51 | } 52 | } 53 | 54 | Component.prototype.update = function (previous) { 55 | if (previous.key !== this.key || this.model.constructor !== previous.model.constructor) { 56 | previous.destroy() 57 | return this.init() 58 | } else { 59 | var self = this 60 | 61 | if (this.isViewComponent) { 62 | var keys = Object.keys(this.model) 63 | for (var n = 0; n < keys.length; n++) { 64 | var key = keys[n] 65 | previous.model[key] = self.model[key] 66 | } 67 | this.model = previous.model 68 | } 69 | 70 | this.component = previous.component 71 | var oldElement = this.component.element 72 | 73 | var element = this.component.update(this.render(oldElement)) 74 | 75 | if (self.model.detached) { 76 | return document.createTextNode('') 77 | } else { 78 | return element 79 | } 80 | } 81 | } 82 | 83 | Component.prototype.renderModel = function (oldElement) { 84 | var self = this 85 | var model = this.model 86 | var currentRender = render.currentRender() 87 | currentRender.mount.setupModelComponent(model) 88 | 89 | if (!oldElement) { 90 | if (self.model.onbeforeadd) { 91 | self.model.onbeforeadd() 92 | } 93 | if (self.model.onbeforerender) { 94 | self.model.onbeforerender() 95 | } 96 | 97 | if (self.model.onadd || self.model.onrender) { 98 | currentRender.finished.then(function () { 99 | if (self.model.onadd) { 100 | self.model.onadd(self.component.element) 101 | } 102 | if (self.model.onrender) { 103 | self.model.onrender(self.component.element) 104 | } 105 | }) 106 | } 107 | } else { 108 | beforeUpdate(model, oldElement) 109 | 110 | if (model.onupdate || model.onrender) { 111 | currentRender.finished.then(function () { 112 | afterUpdate(model, self.component.element, oldElement) 113 | }) 114 | } 115 | } 116 | 117 | var vdom = typeof model.render === 'function' ? model.render() : new Vtext(JSON.stringify(model)) 118 | 119 | if (vdom instanceof Array) { 120 | throw new Error('vdom returned from component cannot be an array') 121 | } 122 | 123 | return debuggingProperties(vdom, model) 124 | } 125 | 126 | Component.prototype.render = function (oldElement) { 127 | var model = this.model 128 | 129 | var meta = hyperdomMeta(model) 130 | meta.lastRenderId = render.currentRender().mount.renderId 131 | 132 | if (typeof model.renderCacheKey === 'function') { 133 | var key = model.renderCacheKey() 134 | if (key !== undefined && meta.cacheKey === key && meta.cachedVdom) { 135 | return meta.cachedVdom 136 | } else { 137 | meta.cacheKey = key 138 | return (meta.cachedVdom = this.renderModel(oldElement)) 139 | } 140 | } else { 141 | return this.renderModel(oldElement) 142 | } 143 | } 144 | 145 | Component.prototype.refresh = function () { 146 | var currentRender = render.currentRender() 147 | if (currentRender.mount.isComponentInDom(this.model)) { 148 | var oldElement = this.component.element 149 | beforeUpdate(this.model, oldElement) 150 | this.component.update(this.render()) 151 | afterUpdate(this.model, this.component.element, oldElement) 152 | } 153 | } 154 | 155 | Component.prototype.destroy = function (element) { 156 | var self = this 157 | 158 | var meta = hyperdomMeta(this.model) 159 | meta.components.delete(this) 160 | 161 | if (self.model.onbeforeremove) { 162 | self.model.onbeforeremove(element) 163 | } 164 | 165 | if (self.model.onremove) { 166 | var currentRender = render.currentRender() 167 | currentRender.finished.then(function () { 168 | self.model.onremove(element) 169 | }) 170 | } 171 | 172 | this.component.destroy() 173 | } 174 | 175 | module.exports = Component 176 | -------------------------------------------------------------------------------- /bindModel.js: -------------------------------------------------------------------------------- 1 | var listener = require('./listener') 2 | var binding = require('./binding') 3 | var RefreshHook = require('./render').RefreshHook 4 | 5 | module.exports = function (tag, attributes, children) { 6 | var type = inputType(tag, attributes) 7 | var bind = inputTypeBindings[type] || bindTextInput 8 | 9 | bind(attributes, children, binding(attributes.binding)) 10 | } 11 | 12 | var inputTypeBindings = { 13 | text: bindTextInput, 14 | 15 | textarea: bindTextInput, 16 | 17 | checkbox: function (attributes, children, binding) { 18 | attributes.checked = binding.get() 19 | 20 | attachEventHandler(attributes, 'onclick', function (ev) { 21 | attributes.checked = ev.target.checked 22 | return binding.set(ev.target.checked) 23 | }, binding) 24 | }, 25 | 26 | radio: function (attributes, children, binding) { 27 | var value = attributes.value 28 | attributes.checked = binding.get() === attributes.value 29 | attributes.on_hyperdomsyncchecked = listener(function (event) { 30 | attributes.checked = event.target.checked 31 | }) 32 | 33 | attachEventHandler(attributes, 'onclick', function (event) { 34 | var name = event.target.name 35 | if (name) { 36 | var inputs = document.getElementsByName(name) 37 | for (var i = 0, l = inputs.length; i < l; i++) { 38 | inputs[i].dispatchEvent(customEvent('_hyperdomsyncchecked')) 39 | } 40 | } 41 | return binding.set(value) 42 | }, binding) 43 | }, 44 | 45 | select: function (attributes, children, binding) { 46 | var currentValue = binding.get() 47 | 48 | var options = [] 49 | var tagName 50 | children.forEach(function (child) { 51 | tagName = child.tagName && child.tagName.toLowerCase() 52 | 53 | switch (tagName) { 54 | case 'optgroup': 55 | child.children.forEach(function (optChild) { 56 | if (optChild.tagName && optChild.tagName.toLowerCase() === 'option') { 57 | options.push(optChild) 58 | } 59 | }) 60 | break 61 | case 'option': 62 | options.push(child) 63 | break 64 | } 65 | }) 66 | 67 | var values = [] 68 | 69 | var valueSelected = attributes.multiple 70 | ? function (value) { return currentValue instanceof Array && currentValue.indexOf(value) >= 0 } 71 | : function (value) { return currentValue === value } 72 | 73 | for (var n = 0; n < options.length; n++) { 74 | var option = options[n] 75 | var hasValue = option.properties.hasOwnProperty('value') 76 | var value = option.properties.value 77 | var text = option.children.map(function (x) { return x.text }).join('') 78 | 79 | values.push(hasValue ? value : text) 80 | 81 | var selected = valueSelected(hasValue ? value : text) 82 | 83 | option.properties.selected = selected 84 | } 85 | 86 | attachEventHandler(attributes, 'onchange', function (ev) { 87 | if (ev.target.multiple) { 88 | var options = ev.target.options 89 | 90 | var selectedValues = [] 91 | 92 | for (var n = 0; n < options.length; n++) { 93 | var op = options[n] 94 | if (op.selected) { 95 | selectedValues.push(values[n]) 96 | } 97 | } 98 | return binding.set(selectedValues) 99 | } else { 100 | attributes.selectedIndex = ev.target.selectedIndex 101 | return binding.set(values[ev.target.selectedIndex]) 102 | } 103 | }, binding) 104 | }, 105 | 106 | file: function (attributes, children, binding) { 107 | var multiple = attributes.multiple 108 | 109 | attachEventHandler(attributes, 'onchange', function (ev) { 110 | if (multiple) { 111 | return binding.set(ev.target.files) 112 | } else { 113 | return binding.set(ev.target.files[0]) 114 | } 115 | }, binding) 116 | } 117 | } 118 | 119 | function inputType (selector, attributes) { 120 | if (/^textarea\b/i.test(selector)) { 121 | return 'textarea' 122 | } else if (/^select\b/i.test(selector)) { 123 | return 'select' 124 | } else { 125 | return attributes.type || 'text' 126 | } 127 | } 128 | 129 | function bindTextInput (attributes, children, binding) { 130 | var textEventNames = ['onkeyup', 'oninput', 'onpaste', 'textInput'] 131 | 132 | var bindingValue = binding.get() 133 | if (!(bindingValue instanceof Error)) { 134 | attributes.value = bindingValue !== undefined ? bindingValue : '' 135 | } 136 | 137 | attachEventHandler(attributes, textEventNames, function (ev) { 138 | if (binding.get() !== ev.target.value) { 139 | return binding.set(ev.target.value) 140 | } 141 | }, binding) 142 | } 143 | 144 | function attachEventHandler (attributes, eventNames, handler) { 145 | if (eventNames instanceof Array) { 146 | for (var n = 0; n < eventNames.length; n++) { 147 | insertEventHandler(attributes, eventNames[n], handler) 148 | } 149 | } else { 150 | insertEventHandler(attributes, eventNames, handler) 151 | } 152 | } 153 | 154 | function insertEventHandler (attributes, eventName, handler) { 155 | var previousHandler = attributes[eventName] 156 | if (previousHandler) { 157 | attributes[eventName] = sequenceFunctions(handler, previousHandler) 158 | } else { 159 | attributes[eventName] = handler 160 | } 161 | } 162 | 163 | function sequenceFunctions (handler1, handler2) { 164 | return function (ev) { 165 | handler1(ev) 166 | if (handler2 instanceof RefreshHook) { 167 | return handler2.handler(ev) 168 | } else { 169 | return handler2(ev) 170 | } 171 | } 172 | } 173 | 174 | function customEvent (name) { 175 | if (typeof Event === 'function') { 176 | return new window.Event(name) 177 | } else { 178 | var event = document.createEvent('Event') 179 | event.initEvent(name, false, false) 180 | return event 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /rendering.js: -------------------------------------------------------------------------------- 1 | var vhtml = require('./vhtml') 2 | var domComponent = require('./domComponent') 3 | var bindingMeta = require('./meta') 4 | var toVdom = require('./toVdom') 5 | var parseTag = require('virtual-dom/virtual-hyperscript/parse-tag') 6 | var Mount = require('./mount') 7 | var Component = require('./component') 8 | var render = require('./render') 9 | var deprecations = require('./deprecations') 10 | var prepareAttributes = require('./prepareAttributes') 11 | var binding = require('./binding') 12 | var refreshAfter = require('./refreshAfter') 13 | var refreshEventResult = require('./refreshEventResult') 14 | 15 | exports.append = function (element, render, model, options) { 16 | return startAttachment(render, model, options, function (mount, domComponentOptions) { 17 | var component = domComponent.create(domComponentOptions) 18 | var vdom = mount.render() 19 | element.appendChild(component.create(vdom)) 20 | return component 21 | }) 22 | } 23 | 24 | exports.replace = function (element, render, model, options) { 25 | return startAttachment(render, model, options, function (mount, domComponentOptions) { 26 | var component = domComponent.create(domComponentOptions) 27 | var vdom = mount.render() 28 | element.parentNode.replaceChild(component.create(vdom), element) 29 | return component 30 | }) 31 | } 32 | 33 | exports.appendVDom = function (vdom, render, model, options) { 34 | return startAttachment(render, model, options, function (mount) { 35 | var component = { 36 | create: function (newVDom) { 37 | vdom.children = [] 38 | if (newVDom) { 39 | vdom.children.push(toVdom(newVDom)) 40 | } 41 | }, 42 | update: function (newVDom) { 43 | vdom.children = [] 44 | if (newVDom) { 45 | vdom.children.push(toVdom(newVDom)) 46 | } 47 | } 48 | } 49 | component.create(mount.render()) 50 | return component 51 | }) 52 | } 53 | 54 | function startAttachment (render, model, options, attachToDom) { 55 | if (typeof render === 'object') { 56 | return start(render, attachToDom, model) 57 | } else { 58 | deprecations.renderFunction('hyperdom.append and hyperdom.replace with render functions are deprecated, please pass a component') 59 | return start({render: function () { return render(model) }}, attachToDom, options) 60 | } 61 | } 62 | 63 | function start (model, attachToDom, options) { 64 | var mount = new Mount(model, options) 65 | render(mount, function () { 66 | if (options) { 67 | var domComponentOptions = {document: options.document} 68 | } 69 | try { 70 | mount.component = attachToDom(mount, domComponentOptions) 71 | } catch (e) { 72 | mount.component = { 73 | update: function () {}, 74 | destroy: function () {} 75 | } 76 | throw e 77 | } 78 | }) 79 | return mount 80 | } 81 | 82 | /** 83 | * this function is quite ugly and you may be very tempted 84 | * to refactor it into smaller functions, I certainly am. 85 | * however, it was written like this for performance 86 | * so think of that before refactoring! :) 87 | */ 88 | exports.html = function (hierarchySelector) { 89 | var hasHierarchy = hierarchySelector.indexOf(' ') >= 0 90 | var selector, selectorElements 91 | 92 | if (hasHierarchy) { 93 | selectorElements = hierarchySelector.match(/\S+/g) 94 | selector = selectorElements[selectorElements.length - 1] 95 | } else { 96 | selector = hierarchySelector 97 | } 98 | 99 | var childElements 100 | var vdom 101 | var tag 102 | var attributes = arguments[1] 103 | 104 | if (attributes && attributes.constructor === Object && typeof attributes.render !== 'function') { 105 | childElements = toVdom.recursive(Array.prototype.slice.call(arguments, 2)) 106 | prepareAttributes(selector, attributes, childElements) 107 | tag = parseTag(selector, attributes) 108 | vdom = vhtml(tag, attributes, childElements) 109 | } else { 110 | attributes = {} 111 | childElements = toVdom.recursive(Array.prototype.slice.call(arguments, 1)) 112 | tag = parseTag(selector, attributes) 113 | vdom = vhtml(tag, attributes, childElements) 114 | } 115 | 116 | if (hasHierarchy) { 117 | for (var n = selectorElements.length - 2; n >= 0; n--) { 118 | vdom = vhtml(selectorElements[n], {}, [vdom]) 119 | } 120 | } 121 | 122 | return vdom 123 | } 124 | 125 | exports.jsx = function (tag, attributes) { 126 | var childElements = toVdom.recursive(Array.prototype.slice.call(arguments, 2)) 127 | if (typeof tag === 'string') { 128 | if (attributes) { 129 | prepareAttributes(tag, attributes, childElements) 130 | } 131 | return vhtml(tag, attributes || {}, childElements) 132 | } else { 133 | return new Component(new tag(attributes || {}, childElements), {viewComponent: true}) // eslint-disable-line new-cap 134 | } 135 | } 136 | 137 | Object.defineProperty(exports.html, 'currentRender', {get: function () { 138 | deprecations.currentRender('hyperdom.html.currentRender is deprecated, please use hyperdom.currentRender() instead') 139 | return render._currentRender 140 | }}) 141 | 142 | Object.defineProperty(exports.html, 'refresh', {get: function () { 143 | deprecations.refresh('hyperdom.html.refresh is deprecated, please use component.refresh() instead') 144 | if (render._currentRender) { 145 | var currentRender = render._currentRender 146 | return function (result) { 147 | refreshEventResult(result, currentRender.mount) 148 | } 149 | } else { 150 | throw new Error('Please assign hyperdom.html.refresh during a render cycle if you want to use it in event handlers. See https://github.com/featurist/hyperdom#refresh-outside-render-cycle') 151 | } 152 | }}) 153 | 154 | Object.defineProperty(exports.html, 'norefresh', {get: function () { 155 | deprecations.norefresh('hyperdom.html.norefresh is deprecated, please use hyperdom.norefresh() instead') 156 | return refreshEventResult.norefresh 157 | }}) 158 | 159 | Object.defineProperty(exports.html, 'binding', {get: function () { 160 | deprecations.htmlBinding('hyperdom.html.binding() is deprecated, please use hyperdom.binding() instead') 161 | return binding 162 | }}) 163 | 164 | Object.defineProperty(exports.html, 'refreshAfter', {get: function () { 165 | deprecations.refreshAfter("hyperdom.html.refreshAfter() is deprecated, please use require('hyperdom/refreshAfter')() instead") 166 | return refreshAfter 167 | }}) 168 | 169 | exports.html.meta = bindingMeta 170 | 171 | function rawHtml () { 172 | var selector 173 | var html 174 | var options 175 | 176 | if (arguments.length === 2) { 177 | selector = arguments[0] 178 | html = arguments[1] 179 | options = {innerHTML: html} 180 | return exports.html(selector, options) 181 | } else { 182 | selector = arguments[0] 183 | options = arguments[1] 184 | html = arguments[2] 185 | options.innerHTML = html 186 | return exports.html(selector, options) 187 | } 188 | } 189 | 190 | exports.html.rawHtml = function () { 191 | deprecations.htmlRawHtml('hyperdom.html.rawHtml() is deprecated, please use hyperdom.rawHtml() instead') 192 | return rawHtml.apply(undefined, arguments) 193 | } 194 | 195 | exports.rawHtml = rawHtml 196 | -------------------------------------------------------------------------------- /gh-md-toc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # Steps: 5 | # 6 | # 1. Download corresponding html file for some README.md: 7 | # curl -s $1 8 | # 9 | # 2. Discard rows where no substring 'user-content-' (github's markup): 10 | # awk '/user-content-/ { ... 11 | # 12 | # 3.1 Get last number in each row like ' ... sitemap.js.*<\/h/)+2, RLENGTH-5) 21 | # 22 | # 5. Find anchor and insert it inside "(...)": 23 | # substr($0, match($0, "href=\"[^\"]+?\" ")+6, RLENGTH-8) 24 | # 25 | 26 | gh_toc_version="0.5.0" 27 | 28 | gh_user_agent="gh-md-toc v$gh_toc_version" 29 | 30 | # 31 | # Download rendered into html README.md by its url. 32 | # 33 | # 34 | gh_toc_load() { 35 | local gh_url=$1 36 | 37 | if type curl &>/dev/null; then 38 | curl --user-agent "$gh_user_agent" -s "$gh_url" 39 | elif type wget &>/dev/null; then 40 | wget --user-agent="$gh_user_agent" -qO- "$gh_url" 41 | else 42 | echo "Please, install 'curl' or 'wget' and try again." 43 | exit 1 44 | fi 45 | } 46 | 47 | # 48 | # Converts local md file into html by GitHub 49 | # 50 | # ➥ curl -X POST --data '{"text": "Hello world github/linguist#1 **cool**, and #1!"}' https://api.github.com/markdown 51 | #

Hello world github/linguist#1 cool, and #1!

'" 52 | gh_toc_md2html() { 53 | local gh_file_md=$1 54 | URL=https://api.github.com/markdown/raw 55 | TOKEN="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt" 56 | if [ -f "$TOKEN" ]; then 57 | URL="$URL?access_token=$(cat $TOKEN)" 58 | fi 59 | OUTPUT="$(curl -s --user-agent "$gh_user_agent" \ 60 | --data-binary @"$gh_file_md" -H "Content-Type:text/plain" \ 61 | $URL)" 62 | 63 | if [ "$?" != "0" ]; then 64 | echo "XXNetworkErrorXX" 65 | fi 66 | if [ "$(echo "${OUTPUT}" | awk '/API rate limit exceeded/')" != "" ]; then 67 | echo "XXRateLimitXX" 68 | else 69 | echo "${OUTPUT}" 70 | fi 71 | } 72 | 73 | 74 | # 75 | # Is passed string url 76 | # 77 | gh_is_url() { 78 | case $1 in 79 | https* | http*) 80 | echo "yes";; 81 | *) 82 | echo "no";; 83 | esac 84 | } 85 | 86 | # 87 | # TOC generator 88 | # 89 | gh_toc(){ 90 | local gh_src=$1 91 | local gh_src_copy=$1 92 | local gh_ttl_docs=$2 93 | local need_replace=$3 94 | 95 | if [ "$gh_src" = "" ]; then 96 | echo "Please, enter URL or local path for a README.md" 97 | exit 1 98 | fi 99 | 100 | 101 | # Show "TOC" string only if working with one document 102 | if [ "$gh_ttl_docs" = "1" ]; then 103 | 104 | echo "Table of Contents" 105 | echo "=================" 106 | echo "" 107 | gh_src_copy="" 108 | 109 | fi 110 | 111 | if [ "$(gh_is_url "$gh_src")" == "yes" ]; then 112 | gh_toc_load "$gh_src" | gh_toc_grab "$gh_src_copy" 113 | if [ "${PIPESTATUS[0]}" != "0" ]; then 114 | echo "Could not load remote document." 115 | echo "Please check your url or network connectivity" 116 | exit 1 117 | fi 118 | if [ "$need_replace" = "yes" ]; then 119 | echo 120 | echo "!! '$gh_src' is not a local file" 121 | echo "!! Can't insert the TOC into it." 122 | echo 123 | fi 124 | else 125 | local rawhtml=$(gh_toc_md2html "$gh_src") 126 | if [ "$rawhtml" == "XXNetworkErrorXX" ]; then 127 | echo "Parsing local markdown file requires access to github API" 128 | echo "Please make sure curl is installed and check your network connectivity" 129 | exit 1 130 | fi 131 | if [ "$rawhtml" == "XXRateLimitXX" ]; then 132 | echo "Parsing local markdown file requires access to github API" 133 | echo "Error: You exceeded the hourly limit. See: https://developer.github.com/v3/#rate-limiting" 134 | TOKEN="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt" 135 | echo "or place github auth token here: $TOKEN" 136 | exit 1 137 | fi 138 | local toc=`echo "$rawhtml" | gh_toc_grab "$gh_src_copy"` 139 | echo "$toc" 140 | if [ "$need_replace" = "yes" ]; then 141 | local ts="<\!--ts-->" 142 | local te="<\!--te-->" 143 | local dt=`date +'%F_%H%M%S'` 144 | local ext=".orig.${dt}" 145 | local toc_path="${gh_src}.toc.${dt}" 146 | local toc_footer="" 147 | # http://fahdshariff.blogspot.ru/2012/12/sed-mutli-line-replacement-between-two.html 148 | # clear old TOC 149 | sed -i${ext} "/${ts}/,/${te}/{//!d;}" "$gh_src" 150 | # create toc file 151 | echo "${toc}" > "${toc_path}" 152 | echo -e "\n${toc_footer}\n" >> "$toc_path" 153 | # insert toc file 154 | if [[ "`uname`" == "Darwin" ]]; then 155 | sed -i "" "/${ts}/r ${toc_path}" "$gh_src" 156 | else 157 | sed -i "/${ts}/r ${toc_path}" "$gh_src" 158 | fi 159 | echo 160 | echo "!! TOC was added into: '$gh_src'" 161 | echo "!! Origin version of the file: '${gh_src}${ext}'" 162 | echo "!! TOC added into a separate file: '${toc_path}'" 163 | echo 164 | fi 165 | fi 166 | } 167 | 168 | # 169 | # Grabber of the TOC from rendered html 170 | # 171 | # $1 — a source url of document. 172 | # It's need if TOC is generated for multiple documents. 173 | # 174 | gh_toc_grab() { 175 | # if closed is on the new line, then move it on the prev line 176 | # for example: 177 | # was: The command foo1 178 | # 179 | # became: The command foo1 180 | sed -e ':a' -e 'N' -e '$!ba' -e 's/\n<\/h/<\/h/g' | 181 | # find strings that corresponds to template 182 | grep -E -o '//' | sed 's/<\/code>//' | 185 | # now all rows are like: 186 | # ... .*<\/h/)+2, RLENGTH-5)"](" gh_url substr($0, match($0, "href=\"[^\"]+?\" ")+6, RLENGTH-8) ")"}' | sed 'y/+/ /; s/%/\\x/g')" 191 | } 192 | 193 | # 194 | # Returns filename only from full path or url 195 | # 196 | gh_toc_get_filename() { 197 | echo "${1##*/}" 198 | } 199 | 200 | # 201 | # Options hendlers 202 | # 203 | gh_toc_app() { 204 | local app_name=$(basename $0) 205 | local need_replace="no" 206 | 207 | if [ "$1" = '--help' ] || [ $# -eq 0 ] ; then 208 | echo "GitHub TOC generator ($app_name): $gh_toc_version" 209 | echo "" 210 | echo "Usage:" 211 | echo " $app_name [--insert] src [src] Create TOC for a README file (url or local path)" 212 | echo " $app_name - Create TOC for markdown from STDIN" 213 | echo " $app_name --help Show help" 214 | echo " $app_name --version Show version" 215 | return 216 | fi 217 | 218 | if [ "$1" = '--version' ]; then 219 | echo "$gh_toc_version" 220 | return 221 | fi 222 | 223 | if [ "$1" = "-" ]; then 224 | if [ -z "$TMPDIR" ]; then 225 | TMPDIR="/tmp" 226 | elif [ -n "$TMPDIR" -a ! -d "$TMPDIR" ]; then 227 | mkdir -p "$TMPDIR" 228 | fi 229 | local gh_tmp_md 230 | gh_tmp_md=$(mktemp $TMPDIR/tmp.XXXXXX) 231 | while read input; do 232 | echo "$input" >> "$gh_tmp_md" 233 | done 234 | gh_toc_md2html "$gh_tmp_md" | gh_toc_grab "" 235 | return 236 | fi 237 | 238 | if [ "$1" = '--insert' ]; then 239 | need_replace="yes" 240 | shift 241 | fi 242 | 243 | for md in "$@" 244 | do 245 | echo "" 246 | gh_toc "$md" "$#" "$need_replace" 247 | done 248 | 249 | echo "" 250 | echo "Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc)" 251 | } 252 | 253 | # 254 | # Entry point 255 | # 256 | gh_toc_app "$@" 257 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | ## Install 2 | 3 | ```sh 4 | yarn create hyperdom-app --jsx myapp # or npx create-hyperdom-app --jsx myapp 5 | cd myapp 6 | yarn install 7 | ``` 8 | 9 | Run dev server: 10 | 11 | ```sh 12 | yarn dev 13 | ``` 14 | 15 | Open `http://localhost:5000` 16 | 17 | ## Hyperdom App 18 | 19 | An object with a `render()` method is a valid hyperdom component (we call top level component an app). The one in your project looks like this: 20 | 21 | 22 | 23 | #### ** Javascript ** 24 | 25 | _./browser/app.jsx_ 26 | 27 | [view code](/docs/codesandbox/get-started-init/browser/app.jsx) 28 | 29 | It's mounted into the DOM in _./browser/index.js_: 30 | 31 | [view code](/docs/codesandbox/get-started-init/browser/index.js) 32 | 33 | [codesandbox](/docs/codesandbox/get-started-init) 34 | 35 | #### ** Typescript ** 36 | 37 | _./browser/app.tsx_ 38 | 39 | [view code](/docs/codesandbox/get-started-init-ts/browser/app.tsx) 40 | 41 | It's mounted into the DOM in _./browser/index.ts_: 42 | 43 | [view code](/docs/codesandbox/get-started-init-ts/browser/index.ts) 44 | 45 | [codesandbox](/docs/codesandbox/get-started-init-ts) 46 | 47 | 48 | 49 | ## State Management 50 | 51 | It's rare to have to think about state management in Hyperdom. Just like React app, a Hyperdom app is often composed of multiple components. Unlike React though, Hyperdom does not _recreate_ them on each render - your app has total control over how long those components live. Another crucial difference is that Hyperdom always re-renders the whole app, no matter which component triggered an update. 52 | 53 | This means you can use normal JavaScript objects to store state and simply refer to those objects in jsx. 54 | 55 | ## Events and Bindings 56 | 57 | Hyperdom rerenders immediately after each UI event your app handles. There are two ways of handling UI events in hyperdom, event handlers (for things like mouse clicks) and input bindings (for things like text boxes). 58 | 59 | ## Event Handlers 60 | 61 | Event handlers run some code when a user clicks on something. Let's modify our `App` class: 62 | 63 | 64 | 65 | #### ** Javascript ** 66 | 67 | _./browser/app.jsx_ 68 | 69 | [view code](/docs/codesandbox/get-started-events/browser/app.jsx#L4) 70 | 71 | [codesandbox](/docs/codesandbox/get-started-events) 72 | 73 | #### ** Typescript ** 74 | 75 | _./browser/app.tsx_ 76 | 77 | [view code](/docs/codesandbox/get-started-events-ts/browser/app.tsx#L4) 78 | 79 | [codesandbox](/docs/codesandbox/get-started-events-ts) 80 | 81 | 82 | 83 | When "Next" link is clicked, the `onclick` handler is executed. After that, hyperdom re-renders (that is, calls the `render()` method, compares the result with the current DOM and updates it if needed). 84 | 85 | Read more about Events [here](api#event-handler-on-attributes) 86 | 87 | ## Input Bindings 88 | 89 | This is how we bind html inputs onto the state. Let's see it in action: 90 | 91 | 92 | 93 | #### ** Javascript ** 94 | 95 | _./browser/app.jsx_ 96 | 97 | [view code](/docs/codesandbox/get-started-bindings/browser/app.jsx#L13) 98 | 99 | [codesandbox](/docs/codesandbox/get-started-bindings) 100 | 101 | #### ** Typescript ** 102 | 103 | _./browser/app.tsx_ 104 | 105 | [view code](/docs/codesandbox/get-started-bindings-ts/browser/app.tsx#L19) 106 | 107 | [codesandbox](/docs/codesandbox/get-started-bindings-ts) 108 | 109 | 110 | 111 | Each time user types into the input, hyperdom re-renders. 112 | 113 | Read more about Bindings [here](api#the-binding-attribute) 114 | 115 | ## Calling Ajax 116 | 117 | The above examples represent _synchronous_ state change. Where it gets interesting though is how much trouble it would be to keep the page in sync with the _asynchronous_ changes. Calling an http endpoint is a prime example. Let's make one: 118 | 119 | 120 | 121 | #### ** Javascript ** 122 | 123 | _./browser/app.jsx_ 124 | 125 | [view code](/docs/codesandbox/get-started-ajax/browser/app.jsx#L15) 126 | 127 | [codesandbox](/docs/codesandbox/get-started-ajax) 128 | 129 | #### ** Typescript ** 130 | 131 | _./browser/app.tsx_ 132 | 133 | [view code](/docs/codesandbox/get-started-ajax-ts/browser/app.tsx#L25) 134 | 135 | [codesandbox](/docs/codesandbox/get-started-ajax-ts) 136 | 137 | 138 | 139 | When "Have a beer" button is clicked hyperdom executes the `onclick` handler and re-renders - just like in the "Events" example above. Unlike that previous example though, hyperdom spots that the handler returned a promise and schedules _another_ render to be executed when that promise resolves/rejects. 140 | 141 | Note how we take advantage of the two renders rule to toggle "Loading...". 142 | 143 | ## Composing Components 144 | 145 | Our `App` class is getting pretty hairy - why not to extact a component out of it? Like that beer table: 146 | 147 | 148 | 149 | #### ** Javascript ** 150 | 151 | _./browser/BeerList.jsx_ 152 | 153 | [view code](/docs/codesandbox/get-started-compose/browser/BeerList.jsx#L4) 154 | 155 | And use it in the main app: 156 | 157 | _./browser/app.jsx_ 158 | 159 | [view code](/docs/codesandbox/get-started-compose/browser/app.jsx#L3) 160 | 161 | [codesandbox](/docs/codesandbox/get-started-compose) 162 | 163 | #### ** Typescript ** 164 | 165 | _./browser/BeerList.tsx_ 166 | 167 | [view code](/docs/codesandbox/get-started-compose-ts/browser/BeerList.tsx#L4) 168 | 169 | And use it in the main app: 170 | 171 | _./browser/app.tsx_ 172 | 173 | [view code](/docs/codesandbox/get-started-compose-ts/browser/app.tsx#L4) 174 | 175 | [codesandbox](/docs/codesandbox/get-started-compose-ts) 176 | 177 | 178 | 179 | ?> Since `this.beerList` is a component, we can specify it in place in the jsx. Hyperdom will implicitly call its `render()` method. 180 | 181 | ## Routes 182 | 183 | Routing is essential in most non-trivial applications. That's why hyperdom has routing built in - so you don't have to spend time choosing and implementing one. 184 | 185 | We are going to add new routes to the beer site: `/beers` - to show the beers table - and `/beer/:id` to show an individual beer. And, of course, there is still a `/` route. 186 | 187 | First we need to tell hyperdom that this the routing is involved. We do this by mounting the app with a router: 188 | 189 | 190 | 191 | #### ** Javascript ** 192 | 193 | _./browser/index.js_ 194 | 195 | [view code](/docs/codesandbox/get-started-routing/browser/index.js) 196 | 197 | #### ** Typescript ** 198 | 199 | _./browser/index.ts_ 200 | 201 | [view code](/docs/codesandbox/get-started-routing-ts/browser/index.ts) 202 | 203 | 204 | 205 | Next, let's define what the top level path `/` is going to render: 206 | 207 | 208 | 209 | #### ** Javascript ** 210 | 211 | _./browser/app.jsx_ 212 | 213 | [view code](/docs/codesandbox/get-started-routing/browser/app.jsx#L4-L20) 214 | 215 | #### ** Typescript ** 216 | 217 | _./browser/app.tsx_ 218 | 219 | [view code](/docs/codesandbox/get-started-routing-ts/browser/app.tsx#L4-L20) 220 | 221 | 222 | 223 | The original `render()` method is gone. Two other special methods - `routes()` and (optional) `renderLayout()` - took its place. The former one is where the magic happens, so let's take a closer look. In a nutshell, `routes()` returns a "url -> render function" mapping. A particular render is invoked only if the current url matches. The "key" in that mapping is not actually a string url but a route definition. `routes.home()` in the above example is a route definition. 224 | 225 | We declare those route definitions separately so that they can be used anywhere in the project: 226 | 227 | 228 | 229 | #### ** Javascript ** 230 | 231 | _./browser/routes.js_ 232 | 233 | [view code](/docs/codesandbox/get-started-routing/browser/routes.js) 234 | 235 | #### ** Typescript ** 236 | 237 | _./browser/routes.ts_ 238 | 239 | [view code](/docs/codesandbox/get-started-routing-ts/browser/routes.ts) 240 | 241 | 242 | 243 | Route definition can also generate URLs strings. E.g. `routes.beer.href({id: 23})` returns `/beers/23`. This is how a link to `/beers` page looks in our example: 244 | 245 | 246 | 247 | #### ** Javascript ** 248 | 249 | _./browser/app.jsx_ 250 | 251 | [view code](/docs/codesandbox/get-started-routing/browser/app.jsx#L35-L35) 252 | 253 | #### ** Typescript ** 254 | 255 | _./browser/app.tsx_ 256 | 257 | [view code](/docs/codesandbox/get-started-routing-ts/browser/app.tsx#L48-L48) 258 | 259 | 260 | 261 | Apart from route definitions, there is one other thing that can be a part of the array returned from `routes()`. If you look closely at the above example, you'll notice `this.beerList` is also there. 262 | 263 | This works because `this.beerList` itself a has a `routes()` and that's where our second path - `/beers` - is mapped onto a render. The pattern then repeats itself with `this.showBeer` plugging in the final `/beers/:id` path. 264 | 265 | 266 | 267 | #### ** Javascript ** 268 | 269 | _./browser/BeerList.jsx_ 270 | 271 | [view code](/docs/codesandbox/get-started-routing/browser/BeerList.jsx#L3-L26) 272 | 273 | _./browser/Beer.jsx_ 274 | 275 | [view code](/docs/codesandbox/get-started-routing/browser/Beer.jsx#L2-L40) 276 | 277 | Here is the entire example on codesandbox: 278 | 279 | [codesandbox](/docs/codesandbox/get-started-routing) 280 | 281 | #### ** Typescript ** 282 | 283 | _./browser/BeerList.tsx_ 284 | 285 | [view code](/docs/codesandbox/get-started-routing-ts/browser/BeerList.tsx#L3-L26) 286 | 287 | _./browser/Beer.tsx_ 288 | 289 | [view code](/docs/codesandbox/get-started-routing-ts/browser/Beer.tsx#L2-L43) 290 | 291 | Here is the entire example on codesandbox: 292 | 293 | [codesandbox](/docs/codesandbox/get-started-routing-ts) 294 | 295 | 296 | 297 | When user navigates to the `/beers` page for the _first_ time, an `onload()` method is called by hyperdom (if provided). In our example, it performs an ajax request to fetch the data. Since the `onload` returns a promise, the UI will render again once the promise is resolved/rejected. 298 | 299 | A similar `onload()` method is implemented on the `/beers/:id` page. Except, if we happen to navigate from the `/beers` page, it won't perform an ajax call, but instead draw from the list of beers fetched previously. 300 | 301 | This is a pretty advanced setup as the app will only call the api once, no matter which page user happens to land on. 302 | 303 | Speaking of `/beers/:id`, note how the `:id` parameter is bound onto a component property using `bindings` property. This is very similar to the input bindings we saw earlier. 304 | 305 | ?> Note how we use a custom `beerId` getter to coerce `:id` param into a number. That's because all url bindings produce string values. 306 | 307 | Learn more about routing [here](api#routing) 308 | 309 | ## Testing 310 | 311 | We've touched all hyperdom bases - there aren't that many! - and this is definitely enough to get you started. To help you keep going past that, `create-hyperdom-app` contains a fast, _full stack_ browser tests powered by [electron-mocha](https://github.com/jprichardson/electron-mocha) runner and [browser-monkey](https://github.com/featurist/browser-monkey) for dom assertions/manipulations. It's only a `yarn test` away. 312 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | ## Rendering the Virtual DOM 2 | 3 | ```js 4 | const vdomFragment = hyperdom.html(selector, [attributes], children, ...); 5 | ``` 6 | 7 | * `vdomFragment` - a virtual DOM fragment. This will be compared with the previous virtual DOM fragment, and the differences applied to the real DOM. 8 | * `selector` - (almost) any selector, containing element names, classes and ids: `tag.class#id`, or small hierarchies `pre code`. 9 | * `attributes` - (optional) the attributes of the HTML element, may contain `style`, event handlers, etc. 10 | * `children` - any number of children, which can be arrays of children, strings, or other vdomFragments. 11 | 12 | ### The `binding` Attribute 13 | 14 | Form input elements can be passed a `binding` attribute, which is expected to be either: 15 | 16 | * An array with two items, the first being the model and second the field name, the third being an optional function that is called when the binding is set, for examle, you can initiate some further processing when the value changes. 17 | 18 | ```js 19 | [object, fieldName, setter(value)] 20 | ``` 21 | 22 | * `object` - an object 23 | * `fieldName` - the name of a field on `object` 24 | * `setter(value)` (optional) - a function called with the value when setting the model. 25 | 26 | * An object with two methods, `get` and `set`, to get and set the new value, respectively. 27 | 28 | ```js 29 | { 30 | get: function () { 31 | return model.property; 32 | }, 33 | set: function (value) { 34 | model.property = value; 35 | }, 36 | options: { 37 | // options passed directly to `hyperdom.binding()` 38 | } 39 | } 40 | ``` 41 | 42 | ### Event Handler `on*` Attributes 43 | 44 | Event handlers follow the same semantics as normal HTML event handlers. They have the same names, e.g. `onclick`, `onchange`, `onmousedown` etc. They are passed an `Event` object as the first argument. 45 | 46 | When event handlers complete, the entire page's virtual DOM is re-rendered. Of course only the differences will by applied to the real DOM. 47 | 48 | ### Promises 49 | 50 | If the event handler returns a [Promise](https://promisesaplus.com/), then the view is re-rendered after the promise is fulfilled or rejected. 51 | 52 | ## Virtual Dom API 53 | 54 | ### Selectors (`hyperdom.html` only) 55 | 56 | Use `tagname`, with any number of `.class` and `#id`. 57 | 58 | ```js 59 | h('div.class#id', 'hi ', model.name); 60 | ``` 61 | 62 | Spaces are taken to be small hierarchies of HTML elements, this will produce `
...
`: 63 | 64 | ```js 65 | h('pre code', 'hi ', model.name); 66 | ``` 67 | 68 | ### Add HTML Attributes 69 | 70 | JS 71 | 72 | ```js 73 | h('span', { style: { color: 'red' } }, 'name: ', this.name); 74 | ``` 75 | 76 | JSX 77 | 78 | ```jsx 79 | name: {this.name} 80 | name: {this.name} 81 | ``` 82 | 83 | [virtual-dom](https://github.com/Matt-Esch/virtual-dom) uses JavaScript names for HTML attributes like `className`, `htmlFor` and `tabIndex`. Hyperdom supports these, but also allows regular HTML names so you can use `class`, `for` and `tabindex`. These are much more familiar to people and you don't have to learn anything new. 84 | 85 | Non-standard HTML attribtes can be placed in the `attributes` key: 86 | 87 | ```js 88 | h('span', {attributes: {'my-html-attribute': 'stuff'}}, 'name: ', model.name); 89 | ``` 90 | 91 | ### Keys 92 | 93 | Hyperdom (or rather [virtual-dom](https://github.com/Matt-Esch/virtual-dom)) is not clever enough to be able to compare lists of elements. For example, say you render the following: 94 | 95 | ```jsx 96 |
    97 |
  • one
  • 98 |
  • two
  • 99 |
  • three
  • 100 |
101 | ``` 102 | 103 | And then, followed by: 104 | 105 | ```jsx 106 |
    107 |
  • zero
  • 108 |
  • one
  • 109 |
  • two
  • 110 |
  • three
  • 111 |
112 | ``` 113 | 114 | The lists will be compared like this, and lots of work will be done to change the DOM: 115 | 116 | ```html 117 |
  • one
  • =>
  • zero
  • (change) 118 |
  • two
  • =>
  • one
  • (change) 119 |
  • three
  • =>
  • two
  • (change) 120 |
  • three
  • (new) 121 | ``` 122 | 123 | If we put a unique `key` (String or Number) into the attributes, then we can avoid all that extra work, and just insert the `
  • zero
  • `. 124 | 125 | ```jsx 126 |
      127 |
    • one
    • 128 |
    • two
    • 129 |
    • three
    • 130 |
    131 | ``` 132 | 133 | And: 134 | 135 | ```jsx 136 |
      137 |
    • zero
    • 138 |
    • one
    • 139 |
    • two
    • 140 |
    • three
    • 141 |
    142 | ``` 143 | 144 | It will be compared like this, and is much faster: 145 | 146 | ```html 147 |
  • zero
  • (new) 148 |
  • one
  • =>
  • one
  • 149 |
  • two
  • =>
  • two
  • 150 |
  • three
  • =>
  • three
  • 151 | ``` 152 | 153 | Its not all about performance, there are other things that can be affected by this too, including CSS transitions when CSS classes or style is changed. 154 | 155 | ### Raw HTML 156 | 157 | Insert raw unescaped HTML. Be careful! Make sure there's no chance of script injection. 158 | 159 | ```js 160 | hyperdom.rawHtml('div', 161 | {style: { color: 'red' } }, 162 | 'some dangerous HTML' 163 | ) 164 | ``` 165 | 166 | This can be useful for rendering HTML entities too. For example, to put ` ` in a table cell use `hyperdom.rawHtml('td', ' ')`. 167 | 168 | ### Classes 169 | 170 | Classes have some additional features: 171 | 172 | * a string, e.g. `'item selected'`. 173 | * an array - the classes will be all the items space delimited, e.g. `['item', 'selected']`. 174 | * an object - the classes will be all the keys with truthy values, space delimited, e.g. `{item: true, selected: item.selected}`. 175 | 176 | JS 177 | 178 | ```js 179 | this.items.map(item => { 180 | return h('span', { class: { selected: item == this.selectedItem } }, item.name) 181 | }) 182 | ``` 183 | 184 | JSX 185 | 186 | ```jsx 187 | this.items.map(item => { 188 | return
  • {item.name}
  • 189 | }) 190 | ``` 191 | 192 | ### Joining VDOM Arrays 193 | 194 | You may have an array of vdom elements that you want to join together with a separator, something very much like `Array.prototype.join()`, but for vdom. 195 | 196 | ```jsx 197 | const items = ['one', 'two', 'three'] 198 | hyperdom.join(items.map(i => {i}), ', ') 199 | ``` 200 | 201 | Will produce this HTML: 202 | 203 | ```html 204 | one, two, three 205 | ``` 206 | 207 | ### Data Attributes 208 | 209 | You can use either `data-*` attributes or set the `data` attribute to an object: 210 | 211 | ```jsx 212 | h('div', {'data-stuff': 'something'}) 213 | h('div', {dataset: {stuff: 'something'}}) 214 |
    215 |
    216 | ``` 217 | 218 | ### Responding to Events 219 | 220 | Pass a function to any regular HTML `on*` event handler in, such as `onclick`. That event handler can modify the state of the application, and once finished, the HTML will be re-rendered to reflect the new state. 221 | 222 | If you return a promise from your event handler then the HTML will be re-rendered twice: once when the event handler initially returns, and again when the promise resolves. 223 | 224 | ```jsx 225 | class App { 226 | constructor() { 227 | this.people = [] 228 | } 229 | 230 | addPerson() { 231 | this.people.push({name: 'Person ' + (this.people.length + 1)}) 232 | } 233 | 234 | render() { 235 | return
    236 |
      237 | { 238 | this.people.map(person =>
    • {person.name}
    • ) 239 | } 240 |
    241 | 242 |
    243 | } 244 | } 245 | 246 | hyperdom.append(document.body, new App()) 247 | ``` 248 | 249 | ### Binding the Inputs 250 | 251 | This applies to `textarea` and input types `text`, `url`, `date`, `email`, `color`, `range`, `checkbox`, `number`, and a few more obscure ones. Most of them. 252 | 253 | The `binding` attribute can be used to bind an input to a model field. You can pass either an array `[model, 'fieldName']`, or an object containing `get` and `set` methods: `{get(), set(value)}`. See [bindings](#the_binding_attribute) for more details. 254 | 255 | ```jsx 256 | class App { 257 | render() { 258 | return
    259 | 260 | 261 |
    hi {this.name}
    262 |
    263 | } 264 | } 265 | ``` 266 | 267 | ### Radio Buttons 268 | 269 | Bind the model to each radio button. The buttons can be bound to complex (non-string) values, such as the `blue` object below. 270 | 271 | ```jsx 272 | const blue = { name: 'blue' }; 273 | 274 | class App { 275 | constructor() { 276 | this.colour: blue 277 | } 278 | 279 | render() { 280 | return
    281 | 282 | 283 |
    284 | colour: {JSON.stringify(this.colour)} 285 |
    286 |
    287 | } 288 | } 289 | 290 | hyperdom.append(document.body, new App()); 291 | ``` 292 | 293 | ### Select Dropdowns 294 | 295 | Bind the model onto the `select` element. The `option`s can have complex (non-string) values. 296 | 297 | ```jsx 298 | const blue = { name: 'blue' }; 299 | 300 | class App { 301 | constructor() { 302 | this.colour = blue 303 | } 304 | 305 | render() { 306 | return
    307 | 311 | {JSON.stringify(this.colour)} 312 |
    313 | } 314 | } 315 | 316 | hyperdom.append(document.body, new App()); 317 | ``` 318 | 319 | ### File Inputs 320 | 321 | The file input is much like any other binding, except that only the binding's `set` method ever called, never the `get` method - the file input can only be set by a user selecting a file. 322 | 323 | ```js 324 | class App { 325 | constructor () { 326 | this.filename = '(no file selected)' 327 | this.contents = '' 328 | } 329 | 330 | render() { 331 | return
    332 | this.loadFile(file) } }> 333 |

    {this.filename}

    334 |
    335 |         {this.contents}
    336 |       
    337 |
    338 | } 339 | 340 | loadFile(file) { 341 | return new Promise((resolve) => { 342 | const reader = new FileReader(); 343 | reader.readAsText(file); 344 | 345 | reader.onloadend = () => { 346 | this.filename = file.name; 347 | this.contents = reader.result; 348 | resolve(); 349 | }; 350 | }); 351 | } 352 | } 353 | 354 | hyperdom.append(document.body, new App()) 355 | ``` 356 | 357 | ### Window Events 358 | 359 | You can attach event handlers to `window`, such as `window.onscroll` and `window.onresize`. Return a `windowEvents()` from your render function passing an object containing the event handlers to attach. When the window vdom is shown, the event handlers are added to `window`, when the window vdom is not shown, the event handlers are removed from `window`. 360 | 361 | E.g. to add an `onresize` handler: 362 | 363 | ```js 364 | const windowEvents = require('hyperdom/windowEvents'); 365 | 366 | class App { 367 | render() { 368 | return
    369 | width = {window.innerWidth}, height = {window.innerHeight} 370 | { 371 | windowEvents({ 372 | onresize: () => console.log('resizing') 373 | }) 374 | } 375 | ) 376 | } 377 | } 378 | ``` 379 | 380 | ### Mapping the model to the view 381 | 382 | Sometimes you have an input that doesn't map cleanly to a view, this is often just because the HTML input element represents a string value, while the model represents something else like a number or a date. 383 | 384 | For this you can use a `mapBinding`, found in `hyperdom/mapBinding`. 385 | 386 | ```jsx 387 | const mapBinding = require('hyperdom/mapBinding') 388 | 389 | const integer = { 390 | view (model) { 391 | // convert the model value to a string for the view 392 | return model.toString() 393 | }, 394 | 395 | model (view) { 396 | // convert the input value to an integer for the model 397 | return Number(view) 398 | } 399 | } 400 | 401 | 402 | ``` 403 | 404 | As is often the case, it's possible that the user enters an invalid value for the model, for example they type `xyz` into a field that should be a number. When this happens, you can throw an exception on the `model(value)` method. When this happens, the model is not modified, and so keeps the old value, but also, crucially, the view continues to be rendered with the invalid value. This way, the user can go from a valid value, they can pass through some invalid values as they type in finally a valid value. For example, when typing the date `2020-02-04`, it's not until the date is fully typed that it becomes valid. 405 | 406 | ```js 407 | const mapBinding = require('hyperdom/mapBinding') 408 | 409 | const date = { 410 | view (date) { 411 | // convert the model value into the user input value 412 | return `${date.getFullYear()}-${date.getUTCMonth() + 1}-${date.getUTCDate()}` 413 | }, 414 | model (view) { 415 | // test the date format 416 | if (!/^\d{4}-\d{2}-\d{2}$/.test(view)) { 417 | // not correct, keep typing 418 | throw new Error('Must be a date of the format YYYY-MM-DD'); 419 | } else { 420 | // correct format, set the model 421 | return new Date(view); 422 | } 423 | } 424 | } 425 | 426 | 427 | ``` 428 | 429 | Under the hood, hyperdom stores the intermediate value and the exception in the model's [meta](#meta) area. You can get the exception by calling `hyperdom.meta(model, field).error`. 430 | 431 | ## Raw HTML 432 | 433 | **Careful of script injection attacks!** Make sure the HTML is trusted or free of `