├── .gitignore ├── .npmignore ├── LICENSE ├── karma.config.js ├── package.json ├── readme.md ├── src ├── hyperscript-helpers.ts ├── index.ts └── jsx-factory.ts ├── test ├── hyperscript-helpers.ts ├── jsx-factory.tsx ├── render.ts └── tsconfig.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /lib 3 | /coverage 4 | node_modules 5 | package-lock.json 6 | pnpm-lock.yaml 7 | shrinkwrap.yaml 8 | yarn.lock 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /src 3 | /test 4 | /coverage 5 | .gitignore 6 | node_modules 7 | package-lock.json 8 | shrinkwrap.yaml 9 | yarn.lock 10 | tsconfig.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Andre 'Staltz' Medeiros 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /karma.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | basePath: './', 4 | frameworks: ['mocha', 'karma-typescript'], 5 | preprocessors: { 6 | 'src/**/*.ts': ['karma-typescript'], 7 | 'test/**/*.ts': ['karma-typescript'], 8 | 'test/**/*.tsx': ['karma-typescript'], 9 | }, 10 | files: [{pattern: 'src/**/*.ts'}, {pattern: 'test/**/*.ts'}], 11 | plugins: ['karma-mocha', 'karma-chrome-launcher', 'karma-typescript'], 12 | exclude: [], 13 | browserNoActivityTimeout: 1000000, 14 | karmaTypescriptConfig: { 15 | coverageOptions: { 16 | exclude: /test\//, 17 | }, 18 | bundlerOptions: { 19 | transforms: [require('karma-typescript-es6-transform')()], 20 | }, 21 | tsconfig: './test/tsconfig.json', 22 | }, 23 | reporters: ['dots', 'karma-typescript'], 24 | port: 9876, 25 | colors: true, 26 | autoWatch: true, 27 | browsers: ['Chrome'], 28 | singleRun: true, 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cycle/react-dom", 3 | "version": "2.4.0", 4 | "description": "Cycle.js driver that uses React DOM to render the view", 5 | "author": "Andre Staltz ", 6 | "license": "MIT", 7 | "bugs": "https://github.com/cyclejs/react-dom/issues", 8 | "homepage": "https://github.com/cyclejs/react-dom", 9 | "repository": "https://github.com/cyclejs/react-dom/tree/master", 10 | "keywords": [ 11 | "react", 12 | "react-dom", 13 | "cyclejs", 14 | "xstream", 15 | "mvi", 16 | "react-native", 17 | "driver" 18 | ], 19 | "main": "lib/cjs/index.js", 20 | "typings": "lib/cjs/index.d.ts", 21 | "types": "lib/cjs/index.d.ts", 22 | "peerDependencies": { 23 | "@cycle/react": "^2.2.0", 24 | "react": ">=16.4.x", 25 | "react-dom": ">=16.4.x", 26 | "xstream": "11.x.x" 27 | }, 28 | "devDependencies": { 29 | "@cycle/react": "^2.7.0", 30 | "@cycle/run": "^5.1.0", 31 | "@types/mocha": "^2.2.40", 32 | "@types/node": "^10.5.2", 33 | "@types/react": "16.4.x", 34 | "@types/react-dom": "16.x.x", 35 | "karma": "2.0.2", 36 | "karma-chrome-launcher": "^2.2.0", 37 | "karma-cli": "^1.0.1", 38 | "karma-firefox-launcher": "^1.1.0", 39 | "karma-mocha": "^1.3.0", 40 | "karma-typescript": "^3.0.12", 41 | "karma-typescript-es6-transform": "^1.0.4", 42 | "mocha": "^5.2.0", 43 | "react": "16.5.2", 44 | "react-dom": "16.5.2", 45 | "ts-node": "^7.0.1", 46 | "typescript": "^3.4.5", 47 | "xstream": "11.x.x" 48 | }, 49 | "publishConfig": { 50 | "access": "public" 51 | }, 52 | "scripts": { 53 | "prepublishOnly": "npm run compile", 54 | "compile": "npm run compile-cjs && npm run compile-es6", 55 | "compile-cjs": "tsc --module commonjs --outDir ./lib/cjs", 56 | "compile-es6": "echo 'TODO' : tsc --module es6 --outDir ./lib/es6", 57 | "test": "karma start karma.config.js" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Cycle ReactDOM 2 | 3 | > Cycle.js driver that uses React DOM to render the view 4 | 5 | - Provides a driver factory `makeDOMDriver` 6 | - Contains hyperscript helper functions, like in Cycle DOM 7 | 8 | ``` 9 | npm install @cycle/react-dom 10 | ``` 11 | 12 | ## Example 13 | 14 | ```js 15 | import xs from 'xstream'; 16 | import {run} from '@cycle/run'; 17 | import {makeDOMDriver, div, h1, button} from '@cycle/react-dom'; 18 | 19 | function main(sources) { 20 | const inc = Symbol(); 21 | const inc$ = sources.react.select(inc).events('click'); 22 | 23 | const count$ = inc$.fold(count => count + 1, 0); 24 | 25 | const vdom$ = count$.map(i => 26 | div([ 27 | h1(`Counter: ${i}`), 28 | button(inc, 'Increment'), 29 | ]), 30 | ); 31 | 32 | return { 33 | react: vdom$, 34 | }; 35 | } 36 | 37 | run(main, { 38 | react: makeDOMDriver(document.getElementById('app')), 39 | }); 40 | ``` 41 | 42 | ## API 43 | 44 | ### `makeDOMDriver(container)` 45 | 46 | Returns a driver that uses ReactDOM to render your Cycle.js app into the given `container` element. 47 | 48 | ### Hyperscript helpers 49 | 50 | Import hyperscript helpers such as `div`, `span`, `p`, `button`, `input`, etc to create React elements to represent the respective HTML elements: `
`, ``, `

`, ` 143 | 144 |

145 | )); 146 | } 147 | ``` 148 | 149 | ## Notes 150 | 151 | Please ensure you are depending on compatible versions of `@cycle/react` and `@cycle/react-dom`. They should both be at least version `2.1.x`. 152 | 153 | ``` 154 | yarn list @cycle/react 155 | ``` 156 | 157 | should return a single result. 158 | 159 | 160 | ## License 161 | 162 | MIT, Andre 'Staltz' Medeiros 2018 163 | 164 | -------------------------------------------------------------------------------- /src/hyperscript-helpers.ts: -------------------------------------------------------------------------------- 1 | import {h} from '@cycle/react'; 2 | import {ReactElement} from 'react'; 3 | 4 | function parseShortcut(param: any) { 5 | if (typeof param === 'symbol') { 6 | return {id: undefined, className: undefined, sel: param}; 7 | } 8 | if (typeof param !== 'string' || param.length === 0) { 9 | return {id: undefined, className: undefined, sel: undefined}; 10 | } 11 | const parts = (param as string) 12 | .split(/(?=[\#\.])/g) 13 | .filter(part => part.length >= 2); 14 | const possiblyIds = parts.filter(part => part[0] === '#'); 15 | const possiblyClasses = parts.filter(part => part[0] === '.'); 16 | const removeFirstChar = part => part.slice(1); 17 | const id = possiblyIds.map(removeFirstChar).find(() => true); 18 | const className = possiblyClasses.map(removeFirstChar).join(' '); 19 | const sel = parts.find(part => part[0] !== '#' && part[0] !== '.'); 20 | return {id, className, sel}; 21 | } 22 | 23 | function createTagFunction(tagName: string): Function { 24 | return function hyperscript(a: any, b?: any, c?: any): ReactElement { 25 | const hasA = typeof a !== 'undefined'; 26 | const hasB = typeof b !== 'undefined'; 27 | const hasBChildren = Array.isArray(b) || typeof b === 'string'; 28 | const hasC = typeof c !== 'undefined'; 29 | const {id, className, sel} = parseShortcut(a); 30 | const hasId = !!id; 31 | const hasClassName = !!className; 32 | const hasSelector = !!sel; 33 | if (hasId || hasClassName) { 34 | if (hasB && hasC) { 35 | return h(tagName, {...b, id, className, sel}, c); 36 | } else if (hasB && hasBChildren) { 37 | return h(tagName, {id, className, sel}, b); 38 | } else if (hasB) { 39 | return h(tagName, {...b, id, className, sel}); 40 | } else { 41 | return h(tagName, {id, className, sel}); 42 | } 43 | } else if (hasC) { 44 | return h(tagName, {sel: a, ...b}, c); 45 | } else if (hasB && hasSelector && hasBChildren) { 46 | return h(tagName, {sel}, b); 47 | } else if (hasB && hasSelector) { 48 | return h(tagName, {sel, ...b}); 49 | } else if (hasB) { 50 | return h(tagName, a, b); 51 | } else if (hasA && typeof sel === 'symbol') { 52 | return h(tagName, {sel}); 53 | } else if (hasA) { 54 | return h(tagName, a); 55 | } else { 56 | return h(tagName, {}); 57 | } 58 | }; 59 | } 60 | 61 | const SVG_TAG_NAMES = [ 62 | 'a', 63 | 'altGlyph', 64 | 'altGlyphDef', 65 | 'altGlyphItem', 66 | 'animate', 67 | 'animateColor', 68 | 'animateMotion', 69 | 'animateTransform', 70 | 'circle', 71 | 'clipPath', 72 | 'colorProfile', 73 | 'cursor', 74 | 'defs', 75 | 'desc', 76 | 'ellipse', 77 | 'feBlend', 78 | 'feColorMatrix', 79 | 'feComponentTransfer', 80 | 'feComposite', 81 | 'feConvolveMatrix', 82 | 'feDiffuseLighting', 83 | 'feDisplacementMap', 84 | 'feDistantLight', 85 | 'feFlood', 86 | 'feFuncA', 87 | 'feFuncB', 88 | 'feFuncG', 89 | 'feFuncR', 90 | 'feGaussianBlur', 91 | 'feImage', 92 | 'feMerge', 93 | 'feMergeNode', 94 | 'feMorphology', 95 | 'feOffset', 96 | 'fePointLight', 97 | 'feSpecularLighting', 98 | 'feSpotlight', 99 | 'feTile', 100 | 'feTurbulence', 101 | 'filter', 102 | 'font', 103 | 'fontFace', 104 | 'fontFaceFormat', 105 | 'fontFaceName', 106 | 'fontFaceSrc', 107 | 'fontFaceUri', 108 | 'foreignObject', 109 | 'g', 110 | 'glyph', 111 | 'glyphRef', 112 | 'hkern', 113 | 'image', 114 | 'line', 115 | 'linearGradient', 116 | 'marker', 117 | 'mask', 118 | 'metadata', 119 | 'missingGlyph', 120 | 'mpath', 121 | 'path', 122 | 'pattern', 123 | 'polygon', 124 | 'polyline', 125 | 'radialGradient', 126 | 'rect', 127 | 'script', 128 | 'set', 129 | 'stop', 130 | 'style', 131 | 'switch', 132 | 'symbol', 133 | 'text', 134 | 'textPath', 135 | 'title', 136 | 'tref', 137 | 'tspan', 138 | 'use', 139 | 'view', 140 | 'vkern', 141 | ]; 142 | 143 | const svg = createTagFunction('svg'); 144 | 145 | SVG_TAG_NAMES.forEach(tag => { 146 | svg[tag] = createTagFunction(tag); 147 | }); 148 | 149 | const TAG_NAMES = [ 150 | 'a', 151 | 'abbr', 152 | 'address', 153 | 'area', 154 | 'article', 155 | 'aside', 156 | 'audio', 157 | 'b', 158 | 'base', 159 | 'bdi', 160 | 'bdo', 161 | 'blockquote', 162 | 'body', 163 | 'br', 164 | 'button', 165 | 'canvas', 166 | 'caption', 167 | 'cite', 168 | 'code', 169 | 'col', 170 | 'colgroup', 171 | 'dd', 172 | 'del', 173 | 'dfn', 174 | 'dir', 175 | 'div', 176 | 'dl', 177 | 'dt', 178 | 'em', 179 | 'embed', 180 | 'fieldset', 181 | 'figcaption', 182 | 'figure', 183 | 'footer', 184 | 'form', 185 | 'h1', 186 | 'h2', 187 | 'h3', 188 | 'h4', 189 | 'h5', 190 | 'h6', 191 | 'head', 192 | 'header', 193 | 'hgroup', 194 | 'hr', 195 | 'html', 196 | 'i', 197 | 'iframe', 198 | 'img', 199 | 'input', 200 | 'ins', 201 | 'kbd', 202 | 'keygen', 203 | 'label', 204 | 'legend', 205 | 'li', 206 | 'link', 207 | 'main', 208 | 'map', 209 | 'mark', 210 | 'menu', 211 | 'meta', 212 | 'nav', 213 | 'noscript', 214 | 'object', 215 | 'ol', 216 | 'optgroup', 217 | 'option', 218 | 'p', 219 | 'param', 220 | 'pre', 221 | 'progress', 222 | 'q', 223 | 'rp', 224 | 'rt', 225 | 'ruby', 226 | 's', 227 | 'samp', 228 | 'script', 229 | 'section', 230 | 'select', 231 | 'small', 232 | 'source', 233 | 'span', 234 | 'strong', 235 | 'style', 236 | 'sub', 237 | 'sup', 238 | 'table', 239 | 'tbody', 240 | 'td', 241 | 'textarea', 242 | 'tfoot', 243 | 'th', 244 | 'thead', 245 | 'time', 246 | 'title', 247 | 'tr', 248 | 'u', 249 | 'ul', 250 | 'video', 251 | ]; 252 | 253 | const exported = { 254 | SVG_TAG_NAMES, 255 | TAG_NAMES, 256 | svg, 257 | parseShortcut, 258 | createTagFunction, 259 | }; 260 | TAG_NAMES.forEach(n => { 261 | exported[n] = createTagFunction(n); 262 | }); 263 | export default (exported as any) as HyperScriptHelpers; 264 | 265 | export interface HyperScriptHelperFn { 266 | (selector?: any, properties?: any, children?: any): ReactElement; 267 | } 268 | 269 | export interface SVGHelperFn extends HyperScriptHelperFn { 270 | a: HyperScriptHelperFn; 271 | altGlyph: HyperScriptHelperFn; 272 | altGlyphDef: HyperScriptHelperFn; 273 | altGlyphItem: HyperScriptHelperFn; 274 | animate: HyperScriptHelperFn; 275 | animateColor: HyperScriptHelperFn; 276 | animateMotion: HyperScriptHelperFn; 277 | animateTransform: HyperScriptHelperFn; 278 | circle: HyperScriptHelperFn; 279 | clipPath: HyperScriptHelperFn; 280 | colorProfile: HyperScriptHelperFn; 281 | cursor: HyperScriptHelperFn; 282 | defs: HyperScriptHelperFn; 283 | desc: HyperScriptHelperFn; 284 | ellipse: HyperScriptHelperFn; 285 | feBlend: HyperScriptHelperFn; 286 | feColorMatrix: HyperScriptHelperFn; 287 | feComponentTransfer: HyperScriptHelperFn; 288 | feComposite: HyperScriptHelperFn; 289 | feConvolveMatrix: HyperScriptHelperFn; 290 | feDiffuseLighting: HyperScriptHelperFn; 291 | feDisplacementMap: HyperScriptHelperFn; 292 | feDistantLight: HyperScriptHelperFn; 293 | feFlood: HyperScriptHelperFn; 294 | feFuncA: HyperScriptHelperFn; 295 | feFuncB: HyperScriptHelperFn; 296 | feFuncG: HyperScriptHelperFn; 297 | feFuncR: HyperScriptHelperFn; 298 | feGaussianBlur: HyperScriptHelperFn; 299 | feImage: HyperScriptHelperFn; 300 | feMerge: HyperScriptHelperFn; 301 | feMergeNode: HyperScriptHelperFn; 302 | feMorphology: HyperScriptHelperFn; 303 | feOffset: HyperScriptHelperFn; 304 | fePointLight: HyperScriptHelperFn; 305 | feSpecularLighting: HyperScriptHelperFn; 306 | feSpotlight: HyperScriptHelperFn; 307 | feTile: HyperScriptHelperFn; 308 | feTurbulence: HyperScriptHelperFn; 309 | filter: HyperScriptHelperFn; 310 | font: HyperScriptHelperFn; 311 | fontFace: HyperScriptHelperFn; 312 | fontFaceFormat: HyperScriptHelperFn; 313 | fontFaceName: HyperScriptHelperFn; 314 | fontFaceSrc: HyperScriptHelperFn; 315 | fontFaceUri: HyperScriptHelperFn; 316 | foreignObject: HyperScriptHelperFn; 317 | g: HyperScriptHelperFn; 318 | glyph: HyperScriptHelperFn; 319 | glyphRef: HyperScriptHelperFn; 320 | hkern: HyperScriptHelperFn; 321 | image: HyperScriptHelperFn; 322 | line: HyperScriptHelperFn; 323 | linearGradient: HyperScriptHelperFn; 324 | marker: HyperScriptHelperFn; 325 | mask: HyperScriptHelperFn; 326 | metadata: HyperScriptHelperFn; 327 | missingGlyph: HyperScriptHelperFn; 328 | mpath: HyperScriptHelperFn; 329 | path: HyperScriptHelperFn; 330 | pattern: HyperScriptHelperFn; 331 | polygon: HyperScriptHelperFn; 332 | polyline: HyperScriptHelperFn; 333 | radialGradient: HyperScriptHelperFn; 334 | rect: HyperScriptHelperFn; 335 | script: HyperScriptHelperFn; 336 | set: HyperScriptHelperFn; 337 | stop: HyperScriptHelperFn; 338 | style: HyperScriptHelperFn; 339 | switch: HyperScriptHelperFn; 340 | symbol: HyperScriptHelperFn; 341 | text: HyperScriptHelperFn; 342 | textPath: HyperScriptHelperFn; 343 | title: HyperScriptHelperFn; 344 | tref: HyperScriptHelperFn; 345 | tspan: HyperScriptHelperFn; 346 | use: HyperScriptHelperFn; 347 | view: HyperScriptHelperFn; 348 | vkern: HyperScriptHelperFn; 349 | } 350 | 351 | export interface HyperScriptHelpers { 352 | svg: SVGHelperFn; 353 | a: HyperScriptHelperFn; 354 | abbr: HyperScriptHelperFn; 355 | address: HyperScriptHelperFn; 356 | area: HyperScriptHelperFn; 357 | article: HyperScriptHelperFn; 358 | aside: HyperScriptHelperFn; 359 | audio: HyperScriptHelperFn; 360 | b: HyperScriptHelperFn; 361 | base: HyperScriptHelperFn; 362 | bdi: HyperScriptHelperFn; 363 | bdo: HyperScriptHelperFn; 364 | blockquote: HyperScriptHelperFn; 365 | body: HyperScriptHelperFn; 366 | br: HyperScriptHelperFn; 367 | button: HyperScriptHelperFn; 368 | canvas: HyperScriptHelperFn; 369 | caption: HyperScriptHelperFn; 370 | cite: HyperScriptHelperFn; 371 | code: HyperScriptHelperFn; 372 | col: HyperScriptHelperFn; 373 | colgroup: HyperScriptHelperFn; 374 | dd: HyperScriptHelperFn; 375 | del: HyperScriptHelperFn; 376 | dfn: HyperScriptHelperFn; 377 | dir: HyperScriptHelperFn; 378 | div: HyperScriptHelperFn; 379 | dl: HyperScriptHelperFn; 380 | dt: HyperScriptHelperFn; 381 | em: HyperScriptHelperFn; 382 | embed: HyperScriptHelperFn; 383 | fieldset: HyperScriptHelperFn; 384 | figcaption: HyperScriptHelperFn; 385 | figure: HyperScriptHelperFn; 386 | footer: HyperScriptHelperFn; 387 | form: HyperScriptHelperFn; 388 | h1: HyperScriptHelperFn; 389 | h2: HyperScriptHelperFn; 390 | h3: HyperScriptHelperFn; 391 | h4: HyperScriptHelperFn; 392 | h5: HyperScriptHelperFn; 393 | h6: HyperScriptHelperFn; 394 | head: HyperScriptHelperFn; 395 | header: HyperScriptHelperFn; 396 | hgroup: HyperScriptHelperFn; 397 | hr: HyperScriptHelperFn; 398 | html: HyperScriptHelperFn; 399 | i: HyperScriptHelperFn; 400 | iframe: HyperScriptHelperFn; 401 | img: HyperScriptHelperFn; 402 | input: HyperScriptHelperFn; 403 | ins: HyperScriptHelperFn; 404 | kbd: HyperScriptHelperFn; 405 | keygen: HyperScriptHelperFn; 406 | label: HyperScriptHelperFn; 407 | legend: HyperScriptHelperFn; 408 | li: HyperScriptHelperFn; 409 | link: HyperScriptHelperFn; 410 | main: HyperScriptHelperFn; 411 | map: HyperScriptHelperFn; 412 | mark: HyperScriptHelperFn; 413 | menu: HyperScriptHelperFn; 414 | meta: HyperScriptHelperFn; 415 | nav: HyperScriptHelperFn; 416 | noscript: HyperScriptHelperFn; 417 | object: HyperScriptHelperFn; 418 | ol: HyperScriptHelperFn; 419 | optgroup: HyperScriptHelperFn; 420 | option: HyperScriptHelperFn; 421 | p: HyperScriptHelperFn; 422 | param: HyperScriptHelperFn; 423 | pre: HyperScriptHelperFn; 424 | progress: HyperScriptHelperFn; 425 | q: HyperScriptHelperFn; 426 | rp: HyperScriptHelperFn; 427 | rt: HyperScriptHelperFn; 428 | ruby: HyperScriptHelperFn; 429 | s: HyperScriptHelperFn; 430 | samp: HyperScriptHelperFn; 431 | script: HyperScriptHelperFn; 432 | section: HyperScriptHelperFn; 433 | select: HyperScriptHelperFn; 434 | small: HyperScriptHelperFn; 435 | source: HyperScriptHelperFn; 436 | span: HyperScriptHelperFn; 437 | strong: HyperScriptHelperFn; 438 | style: HyperScriptHelperFn; 439 | sub: HyperScriptHelperFn; 440 | sup: HyperScriptHelperFn; 441 | table: HyperScriptHelperFn; 442 | tbody: HyperScriptHelperFn; 443 | td: HyperScriptHelperFn; 444 | textarea: HyperScriptHelperFn; 445 | tfoot: HyperScriptHelperFn; 446 | th: HyperScriptHelperFn; 447 | thead: HyperScriptHelperFn; 448 | time: HyperScriptHelperFn; 449 | title: HyperScriptHelperFn; 450 | tr: HyperScriptHelperFn; 451 | u: HyperScriptHelperFn; 452 | ul: HyperScriptHelperFn; 453 | video: HyperScriptHelperFn; 454 | } 455 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {Stream} from 'xstream'; 2 | import {ReactElement, createElement} from 'react'; 3 | import {render} from 'react-dom'; 4 | import {ReactSource, makeCycleReactComponent} from '@cycle/react'; 5 | 6 | export function makeDOMDriver(container: any) { 7 | return function domDriver(sink: Stream>) { 8 | const source = new ReactSource(); 9 | const Root = makeCycleReactComponent(() => ({source, sink})); 10 | render(createElement(Root), container); 11 | return source; 12 | }; 13 | } 14 | 15 | import hh, {HyperScriptHelperFn, SVGHelperFn} from './hyperscript-helpers'; 16 | 17 | export const svg: SVGHelperFn = hh.svg; 18 | export const a: HyperScriptHelperFn = hh.a; 19 | export const abbr: HyperScriptHelperFn = hh.abbr; 20 | export const address: HyperScriptHelperFn = hh.address; 21 | export const area: HyperScriptHelperFn = hh.area; 22 | export const article: HyperScriptHelperFn = hh.article; 23 | export const aside: HyperScriptHelperFn = hh.aside; 24 | export const audio: HyperScriptHelperFn = hh.audio; 25 | export const b: HyperScriptHelperFn = hh.b; 26 | export const base: HyperScriptHelperFn = hh.base; 27 | export const bdi: HyperScriptHelperFn = hh.bdi; 28 | export const bdo: HyperScriptHelperFn = hh.bdo; 29 | export const blockquote: HyperScriptHelperFn = hh.blockquote; 30 | export const body: HyperScriptHelperFn = hh.body; 31 | export const br: HyperScriptHelperFn = hh.br; 32 | export const button: HyperScriptHelperFn = hh.button; 33 | export const canvas: HyperScriptHelperFn = hh.canvas; 34 | export const caption: HyperScriptHelperFn = hh.caption; 35 | export const cite: HyperScriptHelperFn = hh.cite; 36 | export const code: HyperScriptHelperFn = hh.code; 37 | export const col: HyperScriptHelperFn = hh.col; 38 | export const colgroup: HyperScriptHelperFn = hh.colgroup; 39 | export const dd: HyperScriptHelperFn = hh.dd; 40 | export const del: HyperScriptHelperFn = hh.del; 41 | export const dfn: HyperScriptHelperFn = hh.dfn; 42 | export const dir: HyperScriptHelperFn = hh.dir; 43 | export const div: HyperScriptHelperFn = hh.div; 44 | export const dl: HyperScriptHelperFn = hh.dl; 45 | export const dt: HyperScriptHelperFn = hh.dt; 46 | export const em: HyperScriptHelperFn = hh.em; 47 | export const embed: HyperScriptHelperFn = hh.embed; 48 | export const fieldset: HyperScriptHelperFn = hh.fieldset; 49 | export const figcaption: HyperScriptHelperFn = hh.figcaption; 50 | export const figure: HyperScriptHelperFn = hh.figure; 51 | export const footer: HyperScriptHelperFn = hh.footer; 52 | export const form: HyperScriptHelperFn = hh.form; 53 | export const h1: HyperScriptHelperFn = hh.h1; 54 | export const h2: HyperScriptHelperFn = hh.h2; 55 | export const h3: HyperScriptHelperFn = hh.h3; 56 | export const h4: HyperScriptHelperFn = hh.h4; 57 | export const h5: HyperScriptHelperFn = hh.h5; 58 | export const h6: HyperScriptHelperFn = hh.h6; 59 | export const head: HyperScriptHelperFn = hh.head; 60 | export const header: HyperScriptHelperFn = hh.header; 61 | export const hgroup: HyperScriptHelperFn = hh.hgroup; 62 | export const hr: HyperScriptHelperFn = hh.hr; 63 | export const html: HyperScriptHelperFn = hh.html; 64 | export const i: HyperScriptHelperFn = hh.i; 65 | export const iframe: HyperScriptHelperFn = hh.iframe; 66 | export const img: HyperScriptHelperFn = hh.img; 67 | export const input: HyperScriptHelperFn = hh.input; 68 | export const ins: HyperScriptHelperFn = hh.ins; 69 | export const kbd: HyperScriptHelperFn = hh.kbd; 70 | export const keygen: HyperScriptHelperFn = hh.keygen; 71 | export const label: HyperScriptHelperFn = hh.label; 72 | export const legend: HyperScriptHelperFn = hh.legend; 73 | export const li: HyperScriptHelperFn = hh.li; 74 | export const link: HyperScriptHelperFn = hh.link; 75 | export const main: HyperScriptHelperFn = hh.main; 76 | export const map: HyperScriptHelperFn = hh.map; 77 | export const mark: HyperScriptHelperFn = hh.mark; 78 | export const menu: HyperScriptHelperFn = hh.menu; 79 | export const meta: HyperScriptHelperFn = hh.meta; 80 | export const nav: HyperScriptHelperFn = hh.nav; 81 | export const noscript: HyperScriptHelperFn = hh.noscript; 82 | export const object: HyperScriptHelperFn = hh.object; 83 | export const ol: HyperScriptHelperFn = hh.ol; 84 | export const optgroup: HyperScriptHelperFn = hh.optgroup; 85 | export const option: HyperScriptHelperFn = hh.option; 86 | export const p: HyperScriptHelperFn = hh.p; 87 | export const param: HyperScriptHelperFn = hh.param; 88 | export const pre: HyperScriptHelperFn = hh.pre; 89 | export const progress: HyperScriptHelperFn = hh.progress; 90 | export const q: HyperScriptHelperFn = hh.q; 91 | export const rp: HyperScriptHelperFn = hh.rp; 92 | export const rt: HyperScriptHelperFn = hh.rt; 93 | export const ruby: HyperScriptHelperFn = hh.ruby; 94 | export const s: HyperScriptHelperFn = hh.s; 95 | export const samp: HyperScriptHelperFn = hh.samp; 96 | export const script: HyperScriptHelperFn = hh.script; 97 | export const section: HyperScriptHelperFn = hh.section; 98 | export const select: HyperScriptHelperFn = hh.select; 99 | export const small: HyperScriptHelperFn = hh.small; 100 | export const source: HyperScriptHelperFn = hh.source; 101 | export const span: HyperScriptHelperFn = hh.span; 102 | export const strong: HyperScriptHelperFn = hh.strong; 103 | export const style: HyperScriptHelperFn = hh.style; 104 | export const sub: HyperScriptHelperFn = hh.sub; 105 | export const sup: HyperScriptHelperFn = hh.sup; 106 | export const table: HyperScriptHelperFn = hh.table; 107 | export const tbody: HyperScriptHelperFn = hh.tbody; 108 | export const td: HyperScriptHelperFn = hh.td; 109 | export const textarea: HyperScriptHelperFn = hh.textarea; 110 | export const tfoot: HyperScriptHelperFn = hh.tfoot; 111 | export const th: HyperScriptHelperFn = hh.th; 112 | export const thead: HyperScriptHelperFn = hh.thead; 113 | export const title: HyperScriptHelperFn = hh.title; 114 | export const tr: HyperScriptHelperFn = hh.tr; 115 | export const u: HyperScriptHelperFn = hh.u; 116 | export const ul: HyperScriptHelperFn = hh.ul; 117 | export const video: HyperScriptHelperFn = hh.video; 118 | 119 | export { default as jsxFactory } from './jsx-factory'; 120 | -------------------------------------------------------------------------------- /src/jsx-factory.ts: -------------------------------------------------------------------------------- 1 | import { createElement, ReactElement, ReactType } from 'react'; 2 | import { incorporate } from '@cycle/react'; 3 | export { Attributes } from 'react'; 4 | 5 | declare global { 6 | namespace JSX { 7 | interface IntrinsicAttributes { 8 | sel?: string | symbol; 9 | } 10 | } 11 | namespace React { 12 | interface ClassAttributes extends Attributes { 13 | sel?: string | symbol; 14 | } 15 | } 16 | } 17 | 18 | type PropsExtensions = { 19 | sel?: string | symbol; 20 | } 21 | 22 | function createIncorporatedElement

( 23 | type: ReactType

, 24 | props: P & PropsExtensions | null, 25 | ...children: Array> 26 | ): ReactElement

{ 27 | if (!props || !props.sel) { 28 | return createElement(type, props, ...children); 29 | } else { 30 | return createElement(incorporate(type), props, ...children); 31 | } 32 | } 33 | 34 | export default { 35 | createElement: createIncorporatedElement 36 | } 37 | -------------------------------------------------------------------------------- /test/hyperscript-helpers.ts: -------------------------------------------------------------------------------- 1 | import xs from 'xstream'; 2 | import {h, ReactSource} from '@cycle/react'; 3 | import { 4 | makeDOMDriver, 5 | section, 6 | h1, 7 | h2, 8 | h3, 9 | div, 10 | button, 11 | span, 12 | } from '../src/index'; 13 | import {run} from '@cycle/run'; 14 | const assert = require('assert'); 15 | 16 | function createRenderTarget(id: string | null = null) { 17 | const element = document.createElement('div'); 18 | element.className = 'cycletest'; 19 | if (id) { 20 | element.id = id; 21 | } 22 | document.body.appendChild(element); 23 | return element; 24 | } 25 | 26 | describe('hyperscript helpers', function() { 27 | it('w/ nothing', done => { 28 | function main(sources: {react: ReactSource}) { 29 | return { 30 | react: xs.of(h1()), 31 | }; 32 | } 33 | 34 | const target = createRenderTarget(); 35 | run(main, { 36 | react: makeDOMDriver(target), 37 | }); 38 | 39 | setTimeout(() => { 40 | const h1 = target.querySelector('h1') as HTMLElement; 41 | assert.strictEqual(!!h1, true); 42 | assert.strictEqual(h1.tagName, 'H1'); 43 | done(); 44 | }, 100); 45 | }); 46 | 47 | it('w/ text child', done => { 48 | function main(sources: {react: ReactSource}) { 49 | return { 50 | react: xs.of(h1('heading 1')), 51 | }; 52 | } 53 | 54 | const target = createRenderTarget(); 55 | run(main, { 56 | react: makeDOMDriver(target), 57 | }); 58 | 59 | setTimeout(() => { 60 | const h1 = target.querySelector('h1') as HTMLElement; 61 | assert.strictEqual(!!h1, true); 62 | assert.strictEqual(h1.innerHTML, 'heading 1'); 63 | done(); 64 | }, 100); 65 | }); 66 | 67 | it('w/ children array', done => { 68 | function main(sources: {react: ReactSource}) { 69 | return { 70 | react: xs.of( 71 | section([h1('heading 1'), h2('heading 2'), h3('heading 3')]), 72 | ), 73 | }; 74 | } 75 | 76 | const target = createRenderTarget(); 77 | run(main, { 78 | react: makeDOMDriver(target), 79 | }); 80 | 81 | setTimeout(() => { 82 | const section = target.querySelector('section') as HTMLElement; 83 | assert.strictEqual(!!section, true); 84 | assert.strictEqual(section.children.length, 3); 85 | done(); 86 | }, 100); 87 | }); 88 | 89 | it('w/ props', done => { 90 | function main(sources: {react: ReactSource}) { 91 | return { 92 | react: xs.of(section({['data-foo']: 'bar'})), 93 | }; 94 | } 95 | 96 | const target = createRenderTarget(); 97 | run(main, { 98 | react: makeDOMDriver(target), 99 | }); 100 | 101 | setTimeout(() => { 102 | const section = target.querySelector('section') as HTMLElement; 103 | assert.strictEqual(!!section, true); 104 | assert.strictEqual(section.dataset.foo, 'bar'); 105 | done(); 106 | }, 100); 107 | }); 108 | 109 | it('w/ props and children', done => { 110 | function main(sources: {react: ReactSource}) { 111 | return { 112 | react: xs.of( 113 | section({['data-foo']: 'bar'}, [ 114 | h1('heading 1'), 115 | h2('heading 2'), 116 | h3('heading 3'), 117 | ]), 118 | ), 119 | }; 120 | } 121 | 122 | const target = createRenderTarget(); 123 | run(main, { 124 | react: makeDOMDriver(target), 125 | }); 126 | 127 | setTimeout(() => { 128 | const section = target.querySelector('section') as HTMLElement; 129 | assert.strictEqual(!!section, true); 130 | assert.strictEqual(section.dataset.foo, 'bar'); 131 | assert.strictEqual(section.children.length, 3); 132 | done(); 133 | }, 100); 134 | }); 135 | 136 | it('w/ className shortcut', done => { 137 | function main(sources: {react: ReactSource}) { 138 | return { 139 | react: xs.of(section('.foo')), 140 | }; 141 | } 142 | 143 | const target = createRenderTarget(); 144 | run(main, { 145 | react: makeDOMDriver(target), 146 | }); 147 | 148 | setTimeout(() => { 149 | const section = target.querySelector('section') as HTMLElement; 150 | assert.strictEqual(!!section, true); 151 | assert.strictEqual(section.className, 'foo'); 152 | done(); 153 | }, 100); 154 | }); 155 | 156 | it('w/ multi-className shortcut', done => { 157 | function main(sources: {react: ReactSource}) { 158 | return { 159 | react: xs.of(section('.foo.bar.baz')), 160 | }; 161 | } 162 | 163 | const target = createRenderTarget(); 164 | run(main, { 165 | react: makeDOMDriver(target), 166 | }); 167 | 168 | setTimeout(() => { 169 | const section = target.querySelector('section') as HTMLElement; 170 | assert.strictEqual(!!section, true); 171 | assert.strictEqual(section.classList.length, 3); 172 | assert.strictEqual(section.classList[0], 'foo'); 173 | assert.strictEqual(section.classList[1], 'bar'); 174 | assert.strictEqual(section.classList[2], 'baz'); 175 | done(); 176 | }, 100); 177 | }); 178 | 179 | it('w/ id shortcut', done => { 180 | function main(sources: {react: ReactSource}) { 181 | return { 182 | react: xs.of(section('#foo')), 183 | }; 184 | } 185 | 186 | const target = createRenderTarget(); 187 | run(main, { 188 | react: makeDOMDriver(target), 189 | }); 190 | 191 | setTimeout(() => { 192 | const section = target.querySelector('section') as HTMLElement; 193 | assert.strictEqual(!!section, true); 194 | assert.strictEqual(section.id, 'foo'); 195 | done(); 196 | }, 100); 197 | }); 198 | 199 | it('w/ className + id shortcut', done => { 200 | function main(sources: {react: ReactSource}) { 201 | return { 202 | react: xs.of(section('#foo.bar')), 203 | }; 204 | } 205 | 206 | const target = createRenderTarget(); 207 | run(main, { 208 | react: makeDOMDriver(target), 209 | }); 210 | 211 | setTimeout(() => { 212 | const section = target.querySelector('section') as HTMLElement; 213 | assert.strictEqual(!!section, true); 214 | assert.strictEqual(section.id, 'foo'); 215 | assert.strictEqual(section.className, 'bar'); 216 | done(); 217 | }, 100); 218 | }); 219 | 220 | it('w/ multi-className + id shortcut', done => { 221 | function main(sources: {react: ReactSource}) { 222 | return { 223 | react: xs.of(section('#foo.bar.baz')), 224 | }; 225 | } 226 | 227 | const target = createRenderTarget(); 228 | run(main, { 229 | react: makeDOMDriver(target), 230 | }); 231 | 232 | setTimeout(() => { 233 | const section = target.querySelector('section') as HTMLElement; 234 | assert.strictEqual(!!section, true); 235 | assert.strictEqual(section.id, 'foo'); 236 | assert.strictEqual(section.classList.length, 2); 237 | assert.strictEqual(section.classList[0], 'bar'); 238 | assert.strictEqual(section.classList[1], 'baz'); 239 | done(); 240 | }, 100); 241 | }); 242 | 243 | it('w/ symbol selector shortcut', done => { 244 | function main(sources: {react: ReactSource}) { 245 | const inc = Symbol(); 246 | const inc$ = sources.react.select(inc).events('click'); 247 | const count$ = inc$.fold((acc: number, x: any) => acc + 1, 0); 248 | const vdom$ = count$.map((i: number) => div([h1('' + i), button(inc)])); 249 | return {react: vdom$}; 250 | } 251 | 252 | const target = createRenderTarget(); 253 | run(main, { 254 | react: makeDOMDriver(target), 255 | }); 256 | 257 | setTimeout(() => { 258 | const button = target.querySelector('button') as HTMLElement; 259 | const h1 = target.querySelector('h1') as HTMLElement; 260 | assert.strictEqual(!!button, true); 261 | assert.strictEqual(!!h1, true); 262 | assert.strictEqual(h1.innerHTML, '0'); 263 | button.click(); 264 | setTimeout(() => { 265 | assert.strictEqual(h1.innerHTML, '1'); 266 | done(); 267 | }, 100); 268 | }, 100); 269 | }); 270 | 271 | it('w/ symbol selector shortcut and empty props', done => { 272 | function main(sources: {react: ReactSource}) { 273 | const inc = Symbol(); 274 | const inc$ = sources.react.select(inc).events('click'); 275 | const count$ = inc$.fold((acc: number, x: any) => acc + 1, 0); 276 | const vdom$ = count$.map((i: number) => 277 | div([h1('' + i), button(inc, {})]), 278 | ); 279 | return {react: vdom$}; 280 | } 281 | 282 | const target = createRenderTarget(); 283 | run(main, { 284 | react: makeDOMDriver(target), 285 | }); 286 | 287 | setTimeout(() => { 288 | const button = target.querySelector('button') as HTMLElement; 289 | const h1 = target.querySelector('h1') as HTMLElement; 290 | assert.strictEqual(!!button, true); 291 | assert.strictEqual(!!h1, true); 292 | assert.strictEqual(h1.innerHTML, '0'); 293 | button.click(); 294 | setTimeout(() => { 295 | assert.strictEqual(h1.innerHTML, '1'); 296 | done(); 297 | }, 100); 298 | }, 100); 299 | }); 300 | 301 | it('w/ string sel shortcut and empty props', done => { 302 | function main(sources: {react: ReactSource}) { 303 | const inc$ = sources.react.select('inc').events('click'); 304 | const count$ = inc$.fold((acc: number, x: any) => acc + 1, 0); 305 | const vdom$ = count$.map((i: number) => 306 | div([h1('' + i), button('inc', {})]), 307 | ); 308 | return {react: vdom$}; 309 | } 310 | 311 | const target = createRenderTarget(); 312 | run(main, { 313 | react: makeDOMDriver(target), 314 | }); 315 | 316 | setTimeout(() => { 317 | const button = target.querySelector('button') as HTMLElement; 318 | const h1 = target.querySelector('h1') as HTMLElement; 319 | assert.strictEqual(!!button, true); 320 | assert.strictEqual(!!h1, true); 321 | assert.strictEqual(h1.innerHTML, '0'); 322 | button.click(); 323 | setTimeout(() => { 324 | assert.strictEqual(h1.innerHTML, '1'); 325 | done(); 326 | }, 100); 327 | }, 100); 328 | }); 329 | 330 | it('w/ string sel shortcut and child text', done => { 331 | function main(sources: {react: ReactSource}) { 332 | const inc$ = sources.react.select('inc').events('click'); 333 | const count$ = inc$.fold((acc: number, x: any) => acc + 1, 0); 334 | const vdom$ = count$.map((i: number) => 335 | div([h1('' + i), button('inc', 'increment')]), 336 | ); 337 | return {react: vdom$}; 338 | } 339 | 340 | const target = createRenderTarget(); 341 | run(main, { 342 | react: makeDOMDriver(target), 343 | }); 344 | 345 | setTimeout(() => { 346 | const button = target.querySelector('button') as HTMLElement; 347 | const h1 = target.querySelector('h1') as HTMLElement; 348 | assert.strictEqual(!!button, true); 349 | assert.strictEqual(!!h1, true); 350 | assert.strictEqual(h1.innerHTML, '0'); 351 | button.click(); 352 | setTimeout(() => { 353 | assert.strictEqual(h1.innerHTML, '1'); 354 | done(); 355 | }, 100); 356 | }, 100); 357 | }); 358 | 359 | it('w/ string sel shortcut and children array', done => { 360 | function main(sources: {react: ReactSource}) { 361 | const inc$ = sources.react.select('inc').events('click'); 362 | const count$ = inc$.fold((acc: number, x: any) => acc + 1, 0); 363 | const vdom$ = count$.map((i: number) => 364 | div([h1('' + i), button('inc', [span('hello'), span('hi')])]), 365 | ); 366 | return {react: vdom$}; 367 | } 368 | 369 | const target = createRenderTarget(); 370 | run(main, { 371 | react: makeDOMDriver(target), 372 | }); 373 | 374 | setTimeout(() => { 375 | const button = target.querySelector('button') as HTMLElement; 376 | const h1 = target.querySelector('h1') as HTMLElement; 377 | assert.strictEqual(!!button, true); 378 | assert.strictEqual(!!h1, true); 379 | assert.strictEqual(h1.innerHTML, '0'); 380 | button.click(); 381 | setTimeout(() => { 382 | assert.strictEqual(h1.innerHTML, '1'); 383 | done(); 384 | }, 100); 385 | }, 100); 386 | }); 387 | 388 | it('w/ string sel shortcut and props and children array', done => { 389 | function main(sources: {react: ReactSource}) { 390 | const inc$ = sources.react.select('inc').events('click'); 391 | const count$ = inc$.fold((acc: number, x: any) => acc + 1, 0); 392 | const vdom$ = count$.map((i: number) => 393 | div([ 394 | h1('' + i), 395 | button('inc', {['data-foo']: 'bar'}, [(span('hello'), span('hi'))]), 396 | ]), 397 | ); 398 | return {react: vdom$}; 399 | } 400 | 401 | const target = createRenderTarget(); 402 | run(main, { 403 | react: makeDOMDriver(target), 404 | }); 405 | 406 | setTimeout(() => { 407 | const button = target.querySelector('button') as HTMLElement; 408 | const h1 = target.querySelector('h1') as HTMLElement; 409 | assert.strictEqual(!!button, true); 410 | assert.strictEqual(button.dataset.foo, 'bar'); 411 | assert.strictEqual(!!h1, true); 412 | assert.strictEqual(h1.innerHTML, '0'); 413 | button.click(); 414 | setTimeout(() => { 415 | assert.strictEqual(h1.innerHTML, '1'); 416 | done(); 417 | }, 100); 418 | }, 100); 419 | }); 420 | 421 | it('w/ string sel + class shortcut and props', done => { 422 | function main(sources: {react: ReactSource}) { 423 | const inc$ = sources.react.select('inc').events('click'); 424 | const count$ = inc$.fold((acc: number, x: any) => acc + 1, 0); 425 | const vdom$ = count$.map((i: number) => 426 | div([h1('' + i), button('inc.red', {['data-foo']: 'bar'})]), 427 | ); 428 | return {react: vdom$}; 429 | } 430 | 431 | const target = createRenderTarget(); 432 | run(main, { 433 | react: makeDOMDriver(target), 434 | }); 435 | 436 | setTimeout(() => { 437 | const button = target.querySelector('button') as HTMLElement; 438 | const h1 = target.querySelector('h1') as HTMLElement; 439 | assert.strictEqual(!!button, true); 440 | assert.strictEqual(button.dataset.foo, 'bar'); 441 | assert.strictEqual(button.className, 'red'); 442 | assert.strictEqual(!!h1, true); 443 | assert.strictEqual(h1.innerHTML, '0'); 444 | button.click(); 445 | setTimeout(() => { 446 | assert.strictEqual(h1.innerHTML, '1'); 447 | done(); 448 | }, 100); 449 | }, 100); 450 | }); 451 | 452 | it('w/ string sel + class shortcut and children array', done => { 453 | function main(sources: {react: ReactSource}) { 454 | const inc$ = sources.react.select('inc').events('click'); 455 | const count$ = inc$.fold((acc: number, x: any) => acc + 1, 0); 456 | const vdom$ = count$.map((i: number) => 457 | div([h1('' + i), button('inc.red', [span('hello'), span('hi')])]), 458 | ); 459 | return {react: vdom$}; 460 | } 461 | 462 | const target = createRenderTarget(); 463 | run(main, { 464 | react: makeDOMDriver(target), 465 | }); 466 | 467 | setTimeout(() => { 468 | const button = target.querySelector('button') as HTMLElement; 469 | const h1 = target.querySelector('h1') as HTMLElement; 470 | assert.strictEqual(!!button, true); 471 | assert.strictEqual(button.className, 'red'); 472 | assert.strictEqual(button.children.length, 2); 473 | assert.strictEqual(!!h1, true); 474 | assert.strictEqual(h1.innerHTML, '0'); 475 | button.click(); 476 | setTimeout(() => { 477 | assert.strictEqual(h1.innerHTML, '1'); 478 | done(); 479 | }, 100); 480 | }, 100); 481 | }); 482 | 483 | it('w/ str sel + class shortcut and props and children array', done => { 484 | function main(sources: {react: ReactSource}) { 485 | const inc$ = sources.react.select('inc').events('click'); 486 | const count$ = inc$.fold((acc: number, x: any) => acc + 1, 0); 487 | const vdom$ = count$.map((i: number) => 488 | div([ 489 | h1('' + i), 490 | button('inc.red', {['data-foo']: 'bar'}, [span('hello'), span('hi')]), 491 | ]), 492 | ); 493 | return {react: vdom$}; 494 | } 495 | 496 | const target = createRenderTarget(); 497 | run(main, { 498 | react: makeDOMDriver(target), 499 | }); 500 | 501 | setTimeout(() => { 502 | const button = target.querySelector('button') as HTMLElement; 503 | const h1 = target.querySelector('h1') as HTMLElement; 504 | assert.strictEqual(!!button, true); 505 | assert.strictEqual(button.dataset.foo, 'bar'); 506 | assert.strictEqual(button.className, 'red'); 507 | assert.strictEqual(button.children.length, 2); 508 | assert.strictEqual(!!h1, true); 509 | assert.strictEqual(h1.innerHTML, '0'); 510 | button.click(); 511 | setTimeout(() => { 512 | assert.strictEqual(h1.innerHTML, '1'); 513 | done(); 514 | }, 100); 515 | }, 100); 516 | }); 517 | }); 518 | -------------------------------------------------------------------------------- /test/jsx-factory.tsx: -------------------------------------------------------------------------------- 1 | import { createElement, Attributes, ReactElement, ReactType } from 'react'; 2 | const assert = require('assert'); 3 | import {ReactSource} from '@cycle/react'; 4 | import {makeDOMDriver, jsxFactory} from '../src/index'; 5 | import {run} from '@cycle/run'; 6 | import xs from 'xstream'; 7 | 8 | function createRenderTarget(id: string | null = null) { 9 | const element = document.createElement('div'); 10 | element.className = 'cycletest'; 11 | if (id) { 12 | element.id = id; 13 | } 14 | document.body.appendChild(element); 15 | return element; 16 | } 17 | 18 | describe('jsx-factory', function() { 19 | it('w/ nothing', done => { 20 | function main(sources: {react: ReactSource}) { 21 | return { 22 | react: xs.of(

), 23 | }; 24 | } 25 | 26 | const target = createRenderTarget(); 27 | run(main, { 28 | react: makeDOMDriver(target), 29 | }); 30 | 31 | setTimeout(() => { 32 | const h1 = target.querySelector('h1') as HTMLElement; 33 | assert.strictEqual(!!h1, true); 34 | assert.strictEqual(h1.tagName, 'H1'); 35 | done(); 36 | }, 100); 37 | }); 38 | 39 | it('w/ text child', done => { 40 | function main(sources: {react: ReactSource}) { 41 | return { 42 | react: xs.of(

heading 1

), 43 | }; 44 | } 45 | 46 | const target = createRenderTarget(); 47 | run(main, { 48 | react: makeDOMDriver(target), 49 | }); 50 | 51 | setTimeout(() => { 52 | const h1 = target.querySelector('h1') as HTMLElement; 53 | assert.strictEqual(!!h1, true); 54 | assert.strictEqual(h1.innerHTML, 'heading 1'); 55 | done(); 56 | }, 100); 57 | }); 58 | 59 | it('w/ children array', done => { 60 | function main(sources: {react: ReactSource}) { 61 | return { 62 | react: xs.of( 63 |
64 |

heading 1

65 |

heading 2

66 |

heading 3

67 |
68 | ), 69 | }; 70 | } 71 | 72 | const target = createRenderTarget(); 73 | run(main, { 74 | react: makeDOMDriver(target), 75 | }); 76 | 77 | setTimeout(() => { 78 | const section = target.querySelector('section') as HTMLElement; 79 | assert.strictEqual(!!section, true); 80 | assert.strictEqual(section.children.length, 3); 81 | done(); 82 | }, 100); 83 | }); 84 | 85 | it('w/ props', done => { 86 | function main(sources: {react: ReactSource}) { 87 | return { 88 | react: xs.of(
), 89 | }; 90 | } 91 | 92 | const target = createRenderTarget(); 93 | run(main, { 94 | react: makeDOMDriver(target), 95 | }); 96 | 97 | setTimeout(() => { 98 | const section = target.querySelector('section') as HTMLElement; 99 | assert.strictEqual(!!section, true); 100 | assert.strictEqual(section.dataset.foo, 'bar'); 101 | done(); 102 | }, 100); 103 | }); 104 | 105 | it('w/ props and children', done => { 106 | function main(sources: {react: ReactSource}) { 107 | return { 108 | react: xs.of( 109 |
110 |

heading 1

111 |

heading 2

112 |

heading 3

113 |
114 | ), 115 | }; 116 | } 117 | 118 | const target = createRenderTarget(); 119 | run(main, { 120 | react: makeDOMDriver(target), 121 | }); 122 | 123 | setTimeout(() => { 124 | const section = target.querySelector('section') as HTMLElement; 125 | assert.strictEqual(!!section, true); 126 | assert.strictEqual(section.dataset.foo, 'bar'); 127 | assert.strictEqual(section.children.length, 3); 128 | done(); 129 | }, 100); 130 | }); 131 | 132 | it('w/ className', done => { 133 | function main(sources: {react: ReactSource}) { 134 | return { 135 | react: xs.of(
), 136 | }; 137 | } 138 | 139 | const target = createRenderTarget(); 140 | run(main, { 141 | react: makeDOMDriver(target), 142 | }); 143 | 144 | setTimeout(() => { 145 | const section = target.querySelector('section') as HTMLElement; 146 | assert.strictEqual(!!section, true); 147 | assert.strictEqual(section.className, 'foo'); 148 | done(); 149 | }, 100); 150 | }); 151 | 152 | it('w/ symbol selector', done => { 153 | function main(sources: {react: ReactSource}) { 154 | 155 | const inc = Symbol(); 156 | const inc$ = sources.react.select(inc).events('click'); 157 | const count$ = inc$.fold((acc: number, x: any) => acc + 1, 0); 158 | const vdom$ = count$.map((i: number) => ( 159 |
160 |

{'' + i}

161 |
163 | )); 164 | 165 | return {react: vdom$}; 166 | } 167 | 168 | const target = createRenderTarget(); 169 | run(main, { 170 | react: makeDOMDriver(target), 171 | }); 172 | 173 | setTimeout(() => { 174 | const button = target.querySelector('button') as HTMLElement; 175 | const h1 = target.querySelector('h1') as HTMLElement; 176 | assert.strictEqual(!!button, true); 177 | assert.strictEqual(!!h1, true); 178 | assert.strictEqual(h1.innerHTML, '0'); 179 | button.click(); 180 | setTimeout(() => { 181 | assert.strictEqual(h1.innerHTML, '1'); 182 | done(); 183 | }, 100); 184 | }, 100); 185 | }); 186 | 187 | it('renders functional component', done => { 188 | const Test = () => (

Functional

); 189 | 190 | function main() { 191 | const vdom$ = xs.of(); 192 | return {react: vdom$}; 193 | } 194 | 195 | const target = createRenderTarget(); 196 | run(main, { 197 | react: makeDOMDriver(target), 198 | }); 199 | 200 | setTimeout(() => { 201 | const h1 = target.querySelector('h1') as HTMLElement; 202 | assert.strictEqual(!!h1, true); 203 | done(); 204 | }, 100); 205 | }); 206 | 207 | it('renders class component', done => { 208 | class Test extends React.Component { 209 | render() { 210 | return (

Class

); 211 | } 212 | } 213 | 214 | function main() { 215 | const vdom$ = xs.of(); 216 | return {react: vdom$}; 217 | } 218 | 219 | const target = createRenderTarget(); 220 | run(main, { 221 | react: makeDOMDriver(target), 222 | }); 223 | 224 | setTimeout(() => { 225 | const h1 = target.querySelector('h1') as HTMLElement; 226 | assert.strictEqual(!!h1, true); 227 | done(); 228 | }, 100); 229 | }); 230 | 231 | }); 232 | -------------------------------------------------------------------------------- /test/render.ts: -------------------------------------------------------------------------------- 1 | import xs from 'xstream'; 2 | import {h, ReactSource} from '@cycle/react'; 3 | import {makeDOMDriver, h1, h2, h3, h4} from '../src/index'; 4 | import {run} from '@cycle/run'; 5 | const assert = require('assert'); 6 | 7 | function createRenderTarget(id: string | null = null) { 8 | const element = document.createElement('div'); 9 | element.className = 'cycletest'; 10 | if (id) { 11 | element.id = id; 12 | } 13 | document.body.appendChild(element); 14 | return element; 15 | } 16 | 17 | describe('rendering', function() { 18 | it('makeDOMDriver renders hello world Cycle.js app on the DOM', done => { 19 | function main(sources: {react: ReactSource}) { 20 | return { 21 | react: xs.of( 22 | h('section', [h('div', {}, [h('h1', {}, 'Hello world')])]), 23 | ), 24 | }; 25 | } 26 | 27 | const target = createRenderTarget(); 28 | run(main, { 29 | react: makeDOMDriver(target), 30 | }); 31 | 32 | setTimeout(() => { 33 | const h1 = target.querySelector('h1'); 34 | if (!h1) { 35 | return done('No H1 element found'); 36 | } 37 | assert.strictEqual(h1.innerHTML, 'Hello world'); 38 | done(); 39 | }, 100); 40 | }); 41 | 42 | it('makeDOMDriver renders counter Cycle.js app on the DOM', done => { 43 | function main(sources: {react: ReactSource}) { 44 | const inc$ = sources.react.select('inc').events('click'); 45 | const count$ = inc$.fold((acc: number, x: any) => acc + 1, 0); 46 | const vdom$ = count$.map((i: number) => 47 | h('div', {}, [ 48 | h('h1', {}, '' + i), 49 | h('button', {sel: 'inc'}, 'increment'), 50 | ]), 51 | ); 52 | return {react: vdom$}; 53 | } 54 | 55 | const target = createRenderTarget(); 56 | run(main, { 57 | react: makeDOMDriver(target), 58 | }); 59 | 60 | setTimeout(() => { 61 | const button = target.querySelector('button') as HTMLElement; 62 | const h1 = target.querySelector('h1') as HTMLElement; 63 | assert.strictEqual(!!button, true); 64 | assert.strictEqual(!!h1, true); 65 | assert.strictEqual(h1.innerHTML, '0'); 66 | button.click(); 67 | setTimeout(() => { 68 | assert.strictEqual(h1.innerHTML, '1'); 69 | done(); 70 | }, 100); 71 | }, 100); 72 | }); 73 | 74 | it('hyperscript helper without class/id works', done => { 75 | function main(sources: {react: ReactSource}) { 76 | return { 77 | react: xs.of(h1('heading 1')), 78 | }; 79 | } 80 | 81 | const target = createRenderTarget(); 82 | run(main, { 83 | react: makeDOMDriver(target), 84 | }); 85 | 86 | setTimeout(() => { 87 | const h1 = target.querySelector('h1') as HTMLElement; 88 | assert.strictEqual(!!h1, true); 89 | assert.strictEqual(h1.innerHTML, 'heading 1'); 90 | done(); 91 | }, 100); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "removeComments": false, 4 | "preserveConstEnums": true, 5 | "strictNullChecks": true, 6 | "declaration": true, 7 | "sourceMap": true, 8 | "suppressImplicitAnyIndexErrors": true, 9 | "module": "commonjs", 10 | "target": "ES2015", 11 | "rootDir": "./", 12 | "outDir": "test/out", 13 | "skipLibCheck": true, 14 | "lib": ["dom", "es5", "scripthost", "es2015"], 15 | "jsx": "react", 16 | "jsxFactory": "jsxFactory.createElement" 17 | }, 18 | "include": ["../src/*", "./**/*"] 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "removeComments": false, 4 | "preserveConstEnums": true, 5 | "strictNullChecks": true, 6 | "declaration": true, 7 | "sourceMap": true, 8 | "suppressImplicitAnyIndexErrors": true, 9 | "module": "commonjs", 10 | "target": "ES2015", 11 | "rootDir": "src/", 12 | "outDir": "lib/cjs/", 13 | "skipLibCheck": true, 14 | "lib": ["dom", "es5", "scripthost", "es2015"] 15 | }, 16 | "files": ["src/index.ts"] 17 | } 18 | --------------------------------------------------------------------------------