├── .npmignore ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .babelrc ├── .editorconfig ├── test ├── browser │ ├── index.js │ ├── fixtures │ │ └── issue-89.js │ ├── dom-driver.js │ ├── select.js │ ├── render.js │ ├── transposition.js │ ├── events.js │ └── isolation.js └── node │ ├── mock-dom-source.js │ └── html-render.js ├── perf ├── basic │ ├── index.html │ ├── .testem.json │ ├── cyclejs_logo.svg │ └── main.js └── dbmon │ ├── .testem.json │ ├── index.html │ ├── main.js │ └── resources │ ├── monitor.js │ ├── memory-stats.js │ └── ENV.js ├── .travis.yml ├── src ├── fromEvent.js ├── ScopeChecker.js ├── modules │ └── index.js ├── isolate │ ├── isolation.js │ └── module.js ├── makeHTMLDriver.js ├── transposition.js ├── mockDOMSource.js ├── hyperscript │ ├── thunk.js │ ├── h.js │ └── helpers.js ├── ElementFinder.js ├── VNodeWrapper.js ├── index.js ├── makeDOMDriver.js ├── util.js ├── EventDelegator.js └── DOMSource.js ├── .testem.json ├── LICENSE.md ├── package.json ├── README.md ├── CHANGELOG.md └── CONTRIBUTING.md /.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | wallaby.js 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-standard" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.sublime* 3 | 4 | node_modules 5 | npm-debug.log 6 | 7 | test/browser/page/*.js 8 | lib/ 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ], 5 | "plugins": [ 6 | "syntax-jsx", 7 | ["transform-react-jsx", {"pragma": "html"}] 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | trim_trailing_whitespace = true 7 | end_of_line = lf 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /test/browser/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('./dom-driver.js'); 3 | require('./render.js'); 4 | require('./transposition.js'); 5 | require('./select.js'); 6 | require('./events.js'); 7 | require('./isolation.js'); 8 | -------------------------------------------------------------------------------- /perf/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /perf/basic/.testem.json: -------------------------------------------------------------------------------- 1 | { 2 | "before_tests": "browserify perf/basic/main.js -t babelify -o perf/basic/tests-bundle.js", 3 | "test_page": "perf/basic/index.html", 4 | "after_tests": "rm perf/basic/tests-bundle.js", 5 | "launch_in_dev": ["chrome"] 6 | } 7 | -------------------------------------------------------------------------------- /perf/dbmon/.testem.json: -------------------------------------------------------------------------------- 1 | { 2 | "before_tests": "browserify perf/dbmon/main.js -t babelify -o perf/dbmon/tests-bundle.js", 3 | "test_page": "perf/dbmon/index.html", 4 | "after_tests": "rm perf/dbmon/tests-bundle.js", 5 | "launch_in_dev": ["chrome"] 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | addons: 3 | firefox: "latest" 4 | node_js: 5 | - "6" 6 | - "4" 7 | before_script: 8 | - "export DISPLAY=:99.0" 9 | - "sh -e /etc/init.d/xvfb start" 10 | - sleep 3 # give xvfb some time to start 11 | script: npm run test-ci 12 | -------------------------------------------------------------------------------- /src/fromEvent.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | 3 | export function fromEvent (eventType, node, useCapture) { 4 | return Observable.create((observer) => { 5 | const listener = ev => observer.next(ev) 6 | 7 | node.addEventListener(eventType, listener, useCapture) 8 | 9 | return () => node.removeEventListener(eventType, listener, useCapture) 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /.testem.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": "mocha", 3 | "src_files": [ 4 | "test/index.js" 5 | ], 6 | "before_tests": "browserify test/browser/index.js -t babelify -o test/bundle.js", 7 | "serve_files": [ 8 | "test/bundle.js" 9 | ], 10 | "after_tests": "rm test/bundle.js", 11 | "launch_in_ci": [ 12 | "firefox" 13 | ], 14 | "launch_in_dev": [ 15 | "chrome" 16 | ], 17 | "ignore_missing_launchers": true 18 | } 19 | -------------------------------------------------------------------------------- /src/ScopeChecker.js: -------------------------------------------------------------------------------- 1 | export class ScopeChecker { 2 | constructor (scope, isolateModule) { 3 | this.scope = scope 4 | this.isolateModule = isolateModule 5 | } 6 | 7 | isStrictlyInRootScope (leaf) { // eslint-disable-line complexity 8 | for (let el = leaf; el; el = el.parentElement) { 9 | const scope = this.isolateModule.isIsolatedElement(el) 10 | if (scope && scope !== this.scope) { 11 | return false 12 | } 13 | if (scope) { 14 | return true 15 | } 16 | } 17 | return true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/index.js: -------------------------------------------------------------------------------- 1 | import ClassModule from 'snabbdom/modules/class' 2 | import PropsModule from 'snabbdom/modules/props' 3 | import AttrsModule from 'snabbdom/modules/attributes' 4 | import EventsModule from 'snabbdom/modules/eventlisteners' 5 | import StyleModule from 'snabbdom/modules/style' 6 | import HeroModule from 'snabbdom/modules/hero' 7 | import DatasetModule from 'snabbdom/modules/dataset' 8 | 9 | export default [StyleModule, ClassModule, PropsModule, AttrsModule] 10 | 11 | export { 12 | StyleModule, ClassModule, 13 | PropsModule, AttrsModule, 14 | HeroModule, EventsModule, 15 | DatasetModule 16 | } 17 | -------------------------------------------------------------------------------- /perf/dbmon/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/isolate/isolation.js: -------------------------------------------------------------------------------- 1 | import {SCOPE_PREFIX} from '../util' 2 | 3 | export function isolateSource (source, scope) { 4 | return source.select(SCOPE_PREFIX + scope) 5 | } 6 | 7 | export function isolateSink (sink, scope) { 8 | return sink.do(vTree => { 9 | if (vTree.data.isolate) { 10 | const existingScope = 11 | parseInt(vTree.data.isolate.split(SCOPE_PREFIX + 'cycle')[1]) 12 | 13 | const _scope = parseInt(scope.split('cycle')[1]) 14 | 15 | if (Number.isNaN(existingScope) || 16 | Number.isNaN(_scope) || 17 | existingScope > _scope 18 | ) { 19 | return vTree 20 | } 21 | } 22 | vTree.data.isolate = SCOPE_PREFIX + scope 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/makeHTMLDriver.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | import toHtml from 'snabbdom-to-html' 3 | 4 | import {transposeVNode} from './transposition' 5 | 6 | class HTMLSource { 7 | constructor (vNode$) { 8 | this._html$ = vNode$.last().map(toHtml) 9 | } 10 | 11 | get elements () { 12 | return this._html$ 13 | } 14 | 15 | select () { 16 | return new HTMLSource(Observable.empty()) 17 | } 18 | 19 | events () { 20 | return Observable.empty() 21 | } 22 | } 23 | 24 | export function makeHTMLDriver (options = {}) { 25 | const transposition = options.transposition || false 26 | return function htmlDriver (vNode$) { 27 | const preprocessedVNode$ = transposition 28 | ? vNode$.map(transposeVNode).switch() 29 | : vNode$ 30 | return new HTMLSource(preprocessedVNode$) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/browser/fixtures/issue-89.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | let CycleDOM = require('../../../src/index') 3 | let Observable = require('rx').Observable 4 | let {h} = CycleDOM 5 | 6 | function myElement (content) { 7 | return Observable.of(content).map(content => 8 | h('h3.myelementclass', content) 9 | ) 10 | } 11 | 12 | function makeModelNumber$ () { 13 | return Observable.merge( 14 | Observable.of(123).delay(50), 15 | Observable.of(456).delay(100) 16 | ) 17 | } 18 | 19 | function viewWithContainerFn (number$) { 20 | return number$.map(number => 21 | h('div', [ 22 | myElement(String(number)) 23 | ]) 24 | ) 25 | } 26 | 27 | function viewWithoutContainerFn (number$) { 28 | return number$.map(number => 29 | myElement(String(number)) 30 | ) 31 | } 32 | 33 | module.exports = { 34 | myElement, 35 | makeModelNumber$, 36 | viewWithContainerFn, 37 | viewWithoutContainerFn 38 | } 39 | -------------------------------------------------------------------------------- /src/transposition.js: -------------------------------------------------------------------------------- 1 | import {Observable as $} from 'rx' 2 | 3 | function createVTree (vNode, children) { 4 | return { 5 | sel: vNode.sel, 6 | data: vNode.data, 7 | text: vNode.text, 8 | elm: vNode.elm, 9 | key: vNode.key, 10 | children 11 | } 12 | } 13 | 14 | export function transposeVNode (vNode) { // eslint-disable-line complexity 15 | if (vNode && vNode.data && vNode.data.static) { 16 | return $.just(vNode) 17 | } else if (typeof vNode.subscribe === 'function') { 18 | return vNode.map(transposeVNode).switch() 19 | } else if (vNode !== null && typeof vNode === 'object') { 20 | if (!vNode.children || vNode.children.length === 0) { 21 | return $.just(vNode) 22 | } 23 | 24 | return $.combineLatest( 25 | vNode.children.map(transposeVNode), 26 | (...children) => createVTree(vNode, children) 27 | ) 28 | } else { 29 | throw new TypeError('transposition: Unhandled vNode type') 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Tylor Steinberger 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 | -------------------------------------------------------------------------------- /src/mockDOMSource.js: -------------------------------------------------------------------------------- 1 | import {Observable} from 'rx' 2 | 3 | export class MockedDOMSource { 4 | constructor (_mockConfig) { 5 | this._mockConfig = _mockConfig 6 | if (_mockConfig['elements']) { 7 | this.elements = _mockConfig['elements'] 8 | } else { 9 | this.elements = Observable.empty() 10 | } 11 | } 12 | 13 | events (eventType) { 14 | const mockConfig = this._mockConfig 15 | const keys = Object.keys(mockConfig) 16 | const keysLen = keys.length 17 | for (let i = 0; i < keysLen; i++) { 18 | const key = keys[i] 19 | if (key === eventType) { 20 | return mockConfig[key] 21 | } 22 | } 23 | return Observable.empty() 24 | } 25 | 26 | select (selector) { 27 | const mockConfig = this._mockConfig 28 | const keys = Object.keys(mockConfig) 29 | const keysLen = keys.length 30 | for (let i = 0; i < keysLen; i++) { 31 | const key = keys[i] 32 | if (key === selector) { 33 | return new MockedDOMSource(mockConfig[key]) 34 | } 35 | } 36 | return new MockedDOMSource({}) 37 | } 38 | } 39 | 40 | export function mockDOMSource (mockConfig) { 41 | return new MockedDOMSource(mockConfig) 42 | } 43 | -------------------------------------------------------------------------------- /src/hyperscript/thunk.js: -------------------------------------------------------------------------------- 1 | import {h} from './h' 2 | 3 | function copyToThunk (vnode, thunk) { 4 | thunk.elm = vnode.elm 5 | vnode.data.fn = thunk.data.fn 6 | vnode.data.args = thunk.data.args 7 | thunk.data = vnode.data 8 | thunk.children = vnode.children 9 | thunk.text = vnode.text 10 | thunk.elm = vnode.elm 11 | } 12 | 13 | function init (thunk) { 14 | var cur = thunk.data 15 | var vnode = cur.fn.apply(void 0, cur.args) 16 | copyToThunk(vnode, thunk) 17 | } 18 | 19 | function prepatch (oldVnode, thunk) { 20 | let old = oldVnode.data 21 | let cur = thunk.data 22 | let oldArgs = old.args 23 | let args = cur.args 24 | if (old.fn !== cur.fn || oldArgs.length !== args.length) { 25 | copyToThunk(cur.fn.apply(void 0, args), thunk) 26 | } 27 | for (let i = 0; i < args.length; ++i) { 28 | if (oldArgs[i] !== args[i]) { 29 | copyToThunk(cur.fn.apply(void 0, args), thunk) 30 | return 31 | } 32 | } 33 | copyToThunk(oldVnode, thunk) 34 | } 35 | 36 | export function thunk (sel, key, fn, args) { 37 | if (args === void 0) { 38 | args = fn 39 | fn = key 40 | key = void 0 41 | } 42 | return h(sel, { 43 | key: key, 44 | hook: {init: init, prepatch: prepatch}, 45 | fn: fn, 46 | args: args 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /src/ElementFinder.js: -------------------------------------------------------------------------------- 1 | import {ScopeChecker} from './ScopeChecker' 2 | import {getScope, getSelectors, matchesSelector} from './util' 3 | 4 | function toElementArray (nodeList) { 5 | const length = nodeList.length 6 | const arr = Array(length) 7 | 8 | for (let i = 0; i < length; ++i) { 9 | arr[i] = nodeList[i] 10 | } 11 | 12 | return arr 13 | } 14 | 15 | export class ElementFinder { 16 | constructor (namespace, isolateModule) { 17 | this.namespace = namespace 18 | this.isolateModule = isolateModule 19 | } 20 | 21 | call (rootElement) { // eslint-disable-line complexity 22 | const namespace = this.namespace 23 | if (namespace.join('') === '') { return rootElement } 24 | 25 | const scope = getScope(namespace) 26 | const selector = getSelectors(namespace) 27 | const scopeChecker = new ScopeChecker(scope, this.isolateModule) 28 | 29 | let topNode = rootElement 30 | let topNodeMatches = [] 31 | 32 | if (scope.length > 0) { 33 | topNode = this.isolateModule.getIsolatedElement(scope) || rootElement 34 | if (selector && matchesSelector(topNode, selector)) { 35 | topNodeMatches[0] = topNode 36 | } 37 | } 38 | 39 | return toElementArray(topNode.querySelectorAll(selector)) 40 | .filter(scopeChecker.isStrictlyInRootScope, scopeChecker) 41 | .concat(topNodeMatches) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/VNodeWrapper.js: -------------------------------------------------------------------------------- 1 | import {h} from './hyperscript/h' 2 | import classNameFromVNode from 'snabbdom-selector/lib/classNameFromVNode' 3 | import selectorParser from 'snabbdom-selector/lib/selectorParser' 4 | 5 | export class VNodeWrapper { 6 | constructor (rootElement) { 7 | this.rootElement = rootElement 8 | } 9 | 10 | call (vnode) { // eslint-disable-line complexity 11 | const {tagName: selectorTagName, id: selectorId} = selectorParser(vnode.sel) 12 | const vNodeClassName = classNameFromVNode(vnode) 13 | const vNodeData = vnode.data || {} 14 | const vNodeDataProps = vNodeData.props || {} 15 | const {id: vNodeId = selectorId} = vNodeDataProps 16 | 17 | const isVNodeAndRootElementIdentical = 18 | vNodeId.toLowerCase() === this.rootElement.id.toLowerCase() && 19 | selectorTagName.toLowerCase() === this.rootElement.tagName.toLowerCase() && 20 | vNodeClassName.toLowerCase() === this.rootElement.className.toLowerCase() 21 | 22 | if (isVNodeAndRootElementIdentical) { return vnode } 23 | 24 | const {tagName, id, className} = this.rootElement 25 | 26 | const elementId = id 27 | ? `#${id}` 28 | : '' 29 | 30 | const elementClassName = className 31 | ? `.${className.split(' ').join('.')}` 32 | : '' 33 | 34 | return h(`${tagName}${elementId}${elementClassName}`, {}, [vnode]) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /perf/basic/cyclejs_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | -------------------------------------------------------------------------------- /perf/dbmon/main.js: -------------------------------------------------------------------------------- 1 | /* global ENV, Monitoring */ 2 | import {subject} from 'most-subject' 3 | import {run} from '@motorcycle/core' 4 | import {makeDOMDriver, h} from '../../src' 5 | 6 | function map (arr, fn) { 7 | const l = arr.length 8 | const mappedArr = Array(l) 9 | for (let i = 0; i < l; ++i) { 10 | mappedArr[i] = fn(arr[i], i) 11 | } 12 | return mappedArr 13 | } 14 | 15 | function dbMap (q) { 16 | return h('td.' + q.elapsedClassName, [ 17 | h('span.foo', [q.formatElapsed]), 18 | h('div.popover.left', [ 19 | h('div.popover-content', [ 20 | q.query 21 | ]), 22 | h('div.arrow') 23 | ]) 24 | ]) 25 | } 26 | 27 | function databasesMap (db) { 28 | return h('tr', [ 29 | h('td.dbname', [db.dbname]), 30 | h('td.query-count', [ 31 | h('span.' + db.lastSample.countClassName, [ 32 | db.lastSample.nbQueries 33 | ]) 34 | ]) 35 | ].concat(map(db.lastSample.topFiveQueries, dbMap))) 36 | } 37 | 38 | function mainMap (databases) { 39 | return h('div', {static: true}, [ 40 | h('table.table.table-striped.latest-data', {}, [ 41 | h('tbody', map(databases, databasesMap)) 42 | ]) 43 | ]) 44 | } 45 | 46 | function main (sources) { 47 | return { 48 | DOM: sources.databases.map(mainMap) 49 | } 50 | } 51 | 52 | function load (stream) { 53 | stream.next(ENV.generateData().toArray()) 54 | Monitoring.renderRate.ping() 55 | setTimeout(function () { load(stream) }, ENV.timeout) 56 | } 57 | 58 | run(main, { 59 | DOM: makeDOMDriver('#test-container'), 60 | databases: function () { 61 | const stream = subject() 62 | load(stream) 63 | return stream 64 | } 65 | }) 66 | -------------------------------------------------------------------------------- /src/hyperscript/h.js: -------------------------------------------------------------------------------- 1 | import is from 'snabbdom/is' 2 | const vnode = require('snabbdom/vnode') 3 | 4 | function isStream (stream) { 5 | return typeof stream.subscribe === 'function' 6 | } 7 | 8 | function mutateStreamWithNS (vNode) { 9 | addNS(vNode.data, vNode.children) 10 | return vNode 11 | } 12 | 13 | function addNS (data, children) { // eslint-disable-line complexity 14 | data.ns = 'http://www.w3.org/2000/svg' 15 | if (typeof children !== 'undefined' && is.array(children)) { 16 | for (let i = 0; i < children.length; ++i) { 17 | if (isStream(children[i])) { 18 | children[i] = children[i].map(mutateStreamWithNS) 19 | } else { 20 | addNS(children[i].data, children[i].children) 21 | } 22 | } 23 | } 24 | } 25 | 26 | export function h (sel, b, c) { // eslint-disable-line complexity 27 | let data = {} 28 | let children 29 | let text 30 | let i 31 | if (arguments.length === 3) { 32 | data = b 33 | if (is.array(c)) { 34 | children = c 35 | } else if (is.primitive(c)) { 36 | text = c 37 | } 38 | } else if (arguments.length === 2) { 39 | if (is.array(b)) { 40 | children = b 41 | } else if (is.primitive(b)) { 42 | text = b 43 | } else { 44 | data = b 45 | } 46 | } 47 | if (is.array(children)) { 48 | children = children.filter(x => x) // handle null/undef children 49 | for (i = 0; i < children.length; ++i) { 50 | if (is.primitive(children[i])) { 51 | children[i] = vnode(undefined, undefined, undefined, children[i]) 52 | } 53 | } 54 | } 55 | if (sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g') { 56 | addNS(data, children) 57 | } 58 | return vnode(sel, data, children, text, undefined) 59 | }; 60 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export {h} from './hyperscript/h' 2 | export {thunk} from './hyperscript/thunk' 3 | 4 | import hh from './hyperscript/helpers' 5 | 6 | const {svg, 7 | a, abbr, address, area, article, aside, audio, b, base, 8 | bdi, bdo, blockquote, body, br, button, canvas, caption, 9 | cite, code, col, colgroup, dd, del, dfn, dir, div, dl, 10 | dt, em, embed, fieldset, figcaption, figure, footer, form, 11 | h1, h2, h3, h4, h5, h6, head, header, hgroup, hr, html, 12 | i, iframe, img, input, ins, kbd, keygen, label, legend, 13 | li, link, main, map, mark, menu, meta, nav, noscript, 14 | object, ol, optgroup, option, p, param, pre, q, rp, rt, 15 | ruby, s, samp, script, section, select, small, source, span, 16 | strong, style, sub, sup, table, tbody, td, textarea, 17 | tfoot, th, thead, title, tr, u, ul, video, progress 18 | } = hh 19 | 20 | export {svg, 21 | a, abbr, address, area, article, aside, audio, b, base, 22 | bdi, bdo, blockquote, body, br, button, canvas, caption, 23 | cite, code, col, colgroup, dd, del, dfn, dir, div, dl, 24 | dt, em, embed, fieldset, figcaption, figure, footer, form, 25 | h1, h2, h3, h4, h5, h6, head, header, hgroup, hr, html, 26 | i, iframe, img, input, ins, kbd, keygen, label, legend, 27 | li, link, main, map, mark, menu, meta, nav, noscript, 28 | object, ol, optgroup, option, p, param, pre, q, rp, rt, 29 | ruby, s, samp, script, section, select, small, source, span, 30 | strong, style, sub, sup, table, tbody, td, textarea, 31 | tfoot, th, thead, title, tr, u, ul, video, progress 32 | } 33 | 34 | import * as modules from './modules' 35 | export {modules} 36 | 37 | export {makeDOMDriver} from './makeDOMDriver' 38 | 39 | export {mockDOMSource} from './mockDOMSource' 40 | 41 | export {makeHTMLDriver} from './makeHTMLDriver' 42 | -------------------------------------------------------------------------------- /src/makeDOMDriver.js: -------------------------------------------------------------------------------- 1 | import {init} from 'snabbdom' 2 | 3 | import {DOMSource} from './DOMSource' 4 | import {VNodeWrapper} from './VNodeWrapper' 5 | import {IsolateModule} from './isolate/module' 6 | import defaultModules from './modules' 7 | import {transposeVNode} from './transposition' 8 | import {getElement} from './util' 9 | 10 | function makeDOMDriverInputGuard (modules) { 11 | if (!Array.isArray(modules)) { 12 | throw new Error('Optional modules option must be ' + 13 | 'an array for snabbdom modules') 14 | } 15 | } 16 | 17 | function domDriverInputGuard (view$) { // eslint-disable-line complexity 18 | if (!view$ || typeof view$.subscribe !== 'function') // eslint-disable-line brace-style 19 | { 20 | throw new Error('The DOM driver function expects as input an Observable ' + 21 | 'of virtual DOM elements') 22 | } 23 | } 24 | 25 | export function makeDOMDriver (container, options = {}) { 26 | const transposition = options.transposition || false 27 | const modules = options.modules || defaultModules 28 | const isolateModule = new IsolateModule(new Map([])) 29 | const patch = init([isolateModule.createModule()].concat(modules)) 30 | const rootElement = getElement(container) 31 | const vNodeWrapper = new VNodeWrapper(rootElement) 32 | const delegators = new Map([]) 33 | makeDOMDriverInputGuard(modules) 34 | 35 | return function DOMDriver (vNode$) { 36 | domDriverInputGuard(vNode$) 37 | const preprocessedVNode$ = transposition 38 | ? vNode$.map(transposeVNode).switch() 39 | : vNode$ 40 | 41 | const rootElement$ = preprocessedVNode$ 42 | .map(vNode => vNodeWrapper.call(vNode)) 43 | .scan(patch, rootElement) 44 | .startWith(rootElement) 45 | .map(vNode => vNode.elm || vNode) 46 | .replay(null, 1) 47 | 48 | rootElement$.connect() 49 | 50 | return new DOMSource(rootElement$, [], isolateModule, delegators) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | let matchesSelector 2 | try { 3 | matchesSelector = require('matches-selector') 4 | } catch (e) { 5 | matchesSelector = Function.prototype 6 | } 7 | 8 | export {matchesSelector} 9 | 10 | function isElement (obj) { // eslint-disable-line complexity 11 | return typeof HTMLElement === 'object' 12 | ? obj instanceof HTMLElement || obj instanceof DocumentFragment // eslint-disable-line 13 | : obj && typeof obj === 'object' && obj !== null && 14 | (obj.nodeType === 1 || obj.nodeType === 11) && 15 | typeof obj.nodeName === 'string' 16 | } 17 | 18 | export const SCOPE_PREFIX = '$$CYCLEDOM$$-' 19 | 20 | export function getElement (selectors) { // eslint-disable-line complexity 21 | const domElement = typeof selectors === 'string' 22 | ? document.querySelector(selectors) 23 | : selectors 24 | 25 | if (typeof selectors === 'string' && domElement === null) { 26 | throw new Error(`Cannot render into unknown element '${selectors}'`) 27 | } else if (!isElement(domElement)) { 28 | throw new Error('Given container is not a DOM element neither a ' + 29 | 'selector string.') 30 | } 31 | return domElement 32 | } 33 | 34 | export function getScope (namespace) { 35 | return namespace 36 | .filter(c => c.indexOf(SCOPE_PREFIX) > -1) 37 | .slice(-1) // only need the latest, most specific, isolated boundary 38 | .join('') 39 | } 40 | 41 | export function getSelectors (namespace) { 42 | return namespace.filter(c => c.indexOf(SCOPE_PREFIX) === -1).join(' ') 43 | } 44 | 45 | export const eventTypesThatDontBubble = [ 46 | 'load', 47 | 'unload', 48 | 'focus', 49 | 'blur', 50 | 'mouseenter', 51 | 'mouseleave', 52 | 'submit', 53 | 'change', 54 | 'reset', 55 | 'timeupdate', 56 | 'playing', 57 | 'waiting', 58 | 'seeking', 59 | 'seeked', 60 | 'ended', 61 | 'loadedmetadata', 62 | 'loadeddata', 63 | 'canplay', 64 | 'canplaythrough', 65 | 'durationchange', 66 | 'play', 67 | 'pause', 68 | 'ratechange', 69 | 'volumechange', 70 | 'suspend', 71 | 'emptied', 72 | 'stalled', 73 | 'scroll' 74 | ] 75 | -------------------------------------------------------------------------------- /perf/dbmon/resources/monitor.js: -------------------------------------------------------------------------------- 1 | var Monitoring = Monitoring || (function() { 2 | 3 | var stats = new MemoryStats(); 4 | stats.domElement.style.position = 'fixed'; 5 | stats.domElement.style.right = '0px'; 6 | stats.domElement.style.bottom = '0px'; 7 | document.body.appendChild( stats.domElement ); 8 | requestAnimationFrame(function rAFloop(){ 9 | stats.update(); 10 | requestAnimationFrame(rAFloop); 11 | }); 12 | 13 | var RenderRate = function () { 14 | var container = document.createElement( 'div' ); 15 | container.id = 'stats'; 16 | container.style.cssText = 'width:150px;opacity:0.9;cursor:pointer;position:fixed;right:80px;bottom:0px;'; 17 | 18 | var msDiv = document.createElement( 'div' ); 19 | msDiv.id = 'ms'; 20 | msDiv.style.cssText = 'padding:0 0 3px 3px;text-align:left;background-color:#020;'; 21 | container.appendChild( msDiv ); 22 | 23 | var msText = document.createElement( 'div' ); 24 | msText.id = 'msText'; 25 | msText.style.cssText = 'color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px'; 26 | msText.innerHTML= 'Repaint rate: 0/sec'; 27 | msDiv.appendChild( msText ); 28 | 29 | var bucketSize = 20; 30 | var bucket = []; 31 | var lastTime = Date.now(); 32 | return { 33 | domElement: container, 34 | ping: function () { 35 | var start = lastTime; 36 | var stop = Date.now(); 37 | var rate = 1000 / (stop - start); 38 | bucket.push(rate); 39 | if (bucket.length > bucketSize) { 40 | bucket.shift(); 41 | } 42 | var sum = 0; 43 | for (var i = 0; i < bucket.length; i++) { 44 | sum = sum + bucket[i]; 45 | } 46 | msText.textContent = "Repaint rate: " + (sum / bucket.length).toFixed(2) + "/sec"; 47 | lastTime = stop; 48 | } 49 | } 50 | }; 51 | 52 | var renderRate = new RenderRate(); 53 | document.body.appendChild( renderRate.domElement ); 54 | 55 | return { 56 | memoryStats: stats, 57 | renderRate: renderRate 58 | }; 59 | 60 | })(); 61 | -------------------------------------------------------------------------------- /perf/basic/main.js: -------------------------------------------------------------------------------- 1 | import {run} from '@motorcycle/core' 2 | import {makeDOMDriver, h, modules} from '../../src' 3 | const {PropsModule, StyleModule} = modules 4 | import {zip} from 'most' 5 | 6 | function map (arr, fn) { 7 | const l = arr.length 8 | const mappedArr = Array(l) 9 | for (let i = 0; i < l; ++i) { 10 | mappedArr[i] = fn(arr[i], i) 11 | } 12 | return mappedArr 13 | } 14 | 15 | function InputCount (dom, initialValue) { 16 | const id = '.component-count' 17 | const value$ = dom.select(id) 18 | .events('input') 19 | .map(ev => ev.target.value) 20 | .startWith(initialValue) 21 | .multicast() 22 | 23 | return { 24 | dom: value$.map(value => h(`input${id}`, { 25 | key: 1000, 26 | props: { 27 | type: 'range', 28 | max: 250, 29 | min: 1, 30 | value 31 | }, 32 | style: { 33 | width: '100%' 34 | } 35 | })), 36 | value$ 37 | } 38 | } 39 | 40 | function CycleJSLogo (id) { 41 | return h('div', { 42 | key: id, 43 | style: { 44 | alignItems: 'center', 45 | background: 'url(./cyclejs_logo.svg)', 46 | boxSizing: 'border-box', 47 | display: 'inline-flex', 48 | fontFamily: 'sans-serif', 49 | fontWeight: '700', 50 | fontSize: '8px', 51 | height: '32px', 52 | justifyContent: 'center', 53 | margin: '8px', 54 | width: '32px' 55 | } 56 | }, `${id}`) 57 | } 58 | 59 | function view (value, inputCountVTree, componentDOMs) { 60 | return h('div', {static: true}, [ 61 | h('h2', [`# of Components: ${value}`]), 62 | inputCountVTree, 63 | h('div', componentDOMs) 64 | ]) 65 | } 66 | 67 | function main (sources) { 68 | const initialValue = 100 69 | const inputCount = InputCount(sources.dom, initialValue) 70 | 71 | const components$ = inputCount.value$ 72 | .map(value => map(Array(parseInt(value)), (v, i) => CycleJSLogo(i + 1))) 73 | 74 | return { 75 | dom: zip(view, inputCount.value$, inputCount.dom, components$) 76 | } 77 | } 78 | 79 | run(main, { dom: makeDOMDriver('#test-container', {modules: [ 80 | PropsModule, StyleModule 81 | ]}) }) 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cycle-snabbdom", 3 | "version": "3.0.0", 4 | "description": "Snabbdom driver for Cycle.js.", 5 | "main": "lib/index.js", 6 | "directories": { 7 | "lib": "./lib" 8 | }, 9 | "files": [ 10 | "lib/" 11 | ], 12 | "scripts": { 13 | "eslint": "eslint src/", 14 | "test-node": "mocha --compilers js:babel-core/register test/node", 15 | "test-browser": "testem", 16 | "test": "npm run eslint && npm run test-node && npm run test-browser", 17 | "test-perf-basic": "testem -f perf/basic/.testem.json", 18 | "test-perf-dbmon": "testem -f perf/dbmon/.testem.json", 19 | "test-ci": "npm run test-node && testem ci", 20 | "start": "npm install && npm prune && validate-commit-msg", 21 | "lib": "rimraf lib/ && mkdirp lib && babel -d lib/ src/", 22 | "dist": "rimraf dist && mkdirp dist && browserify -t babelify -t browserify-shim --standalone CycleSnabbdom --exclude rx src/index.js -o dist/cycle-snabbdom.js", 23 | "postdist": "uglifyjs dist/cycle-snabbdom.js -o dist/cycle-snabbdom.min.js", 24 | "prepublish": "npm run lib", 25 | "release": "npm run release-patch", 26 | "release-patch": "git checkout master && release patch && npm publish --access=public", 27 | "release-minor": "git checkout master && release minor && npm publish --access=public", 28 | "release-major": "git checkout master && release major && npm publish --access=public" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/TylorS/cycle-snabbdom" 33 | }, 34 | "keywords": [ 35 | "Cyclejs", 36 | "Cycle", 37 | "Reactive", 38 | "Framework" 39 | ], 40 | "author": "Tylor Steinberger