├── .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 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 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 ", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/TylorS/cycle-snabbdom/issues" 44 | }, 45 | "homepage": "https://github.com/TylorS/cycle-snabbdom#readme", 46 | "browserify-shim": { 47 | "rx": "global:Rx" 48 | }, 49 | "dependencies": { 50 | "matches-selector": "^1.0.0", 51 | "rx": "^4.1.0", 52 | "snabbdom": "^0.5.0", 53 | "snabbdom-selector": "^0.4.0", 54 | "snabbdom-to-html": "^2.1.3" 55 | }, 56 | "devDependencies": { 57 | "@cycle/core": "^6.0.3", 58 | "@cycle/isolate": "^1.3.2", 59 | "assert": "^1.4.1", 60 | "babel-cli": "^6.9.0", 61 | "babel-core": "^6.9.1", 62 | "babel-plugin-syntax-jsx": "^6.8.0", 63 | "babel-plugin-transform-react-jsx": "^6.8.0", 64 | "babel-preset-es2015": "^6.9.0", 65 | "babelify": "^7.3.0", 66 | "browserify": "^13.0.1", 67 | "browserify-shim": "^3.8.12", 68 | "cli-release": "^1.0.4", 69 | "eslint": "^2.11.1", 70 | "eslint-config-standard": "^5.3.1", 71 | "mkdirp": "^0.5.1", 72 | "mocha": "^2.5.3", 73 | "rimraf": "^2.5.2", 74 | "rx": "^4.1.0", 75 | "snabbdom-jsx": "^0.3.0", 76 | "testem": "^1.8.1", 77 | "uglify-js": "^2.6.2", 78 | "validate-commit-message": "^3.0.1" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cycle-snabbdom [![Build Status](https://travis-ci.org/TylorS/cycle-snabbdom.svg?branch=master)](https://travis-ci.org/TylorS/cycle-snabbdom) 2 | 3 | # Deprecation 4 | 5 | **It's been great, and thank you for using this library :) 6 | @cycle/dom is likely reached it's last release candidate, and will be released soon! This library is at 100% parity with it, 7 | and is actually a slight step ahead as it has updated to the latest snabbdom 0.5.0 in the process. However, this will be the very last update this library will see. 8 | I strongly urge you to start updating your application to Cycle.js Diversity as soon as you can, and to stop using Rx ;)** 9 | 10 | Alternative DOM driver utilizing the [snabbdom](https://github.com/paldepind/snabbdom) library 11 | 12 | 13 | # Install 14 | ```js 15 | $ npm install cycle-snabbdom 16 | ``` 17 | ## API 18 | 19 | ##### makeDOMDriver(container: string|Element, {modules?: Array}) 20 | 21 | ```js 22 | import {makeDOMDriver} from 'cycle-snabbdom' 23 | ``` 24 | 25 | ##### makeHTMLDriver() 26 | ```js 27 | import {makeHTMLDriver} from 'cycle-snabbdom' 28 | ``` 29 | ##### h - thunk - hyperscript-helpers 30 | Shorcuts to `snabbdom/h`, `snabbdom/thunk` and `hyperscript-helpers` 31 | ```js 32 | import {h, thunk, div, span, h4} from 'cycle-snabbdom' 33 | ``` 34 | 35 | ##### modules : Array 36 | 37 | Shortcut to snabbdom modules. 38 | 39 | ```js 40 | import Cycle from '@cycle/core' 41 | import {modules, makeDOMDriver} from 'cycle-snabbdom' 42 | const { 43 | StyleModule, PropsModule, 44 | AttrsModule, ClassModule, 45 | HeroModule, EventsModule, 46 | } = modules 47 | ... 48 | 49 | Cycle.run(main, { 50 | DOM: makeDOMDriver('#app', {modules: [ 51 | StyleModule, PropsModule, 52 | AttrsModule, ClassModule, 53 | HeroModule, EventsModule 54 | ]}) 55 | }) 56 | 57 | ``` 58 | 59 | ##### mockDOMSource() 60 | A testing utility which aids in creating a queryable collection of Observables. Call mockDOMSource giving it an object specifying selectors, eventTypes and their Observables, and get as output an object following the same format as the DOM Driver's source. 61 | 62 | Example: 63 | ```js 64 | const userEvents = mockDOMSource({ 65 | '.foo': { 66 | 'click': Rx.Observable.just({target: {}}), 67 | 'mouseover': Rx.Observable.just({target: {}}) 68 | }, 69 | '.bar': { 70 | 'scroll': Rx.Observable.just({target: {}}) 71 | } 72 | }); 73 | 74 | // Usage 75 | const click$ = userEvents.select('.foo').events('click'); 76 | ``` 77 | Arguments: 78 | 79 | mockedSelectors :: Object an object where keys are selector strings and values are objects. Those nested objects have eventType strings as keys and values are Observables you created. 80 | Return: 81 | 82 | (Object) fake DOM source object, containing a function select() which can be used just like the DOM Driver's source. Call select(selector).events(eventType) on the source object to get the Observable you defined in the input of mockDOMSource. 83 | -------------------------------------------------------------------------------- /src/EventDelegator.js: -------------------------------------------------------------------------------- 1 | import {ScopeChecker} from './ScopeChecker' 2 | import {getScope, getSelectors, matchesSelector} from './util' 3 | 4 | function patchEvent (event) { 5 | const pEvent = event 6 | pEvent.propagationHasBeenStopped = false 7 | const oldStopPropagation = pEvent.stopPropagation 8 | pEvent.stopPropagation = function stopPropagation () { 9 | oldStopPropagation.call(this) 10 | this.propagationHasBeenStopped = true 11 | } 12 | return pEvent 13 | } 14 | 15 | function mutateEventCurrentTarget (event, currentTargetElement) { 16 | try { 17 | Object.defineProperty(event, 'currentTarget', { 18 | value: currentTargetElement, 19 | configurable: true 20 | }) 21 | } catch (err) { 22 | console.log('please use event.ownerTarget') 23 | } 24 | event.ownerTarget = currentTargetElement 25 | } 26 | 27 | export class EventDelegator { 28 | constructor (topElement, eventType, useCapture, isolateModule) { 29 | this.topElement = topElement 30 | this.eventType = eventType 31 | this.useCapture = useCapture 32 | this.isolateModule = isolateModule 33 | this.roof = topElement.parentElement 34 | this.destinations = [] 35 | 36 | if (useCapture) { 37 | this.domListener = ev => this.capture(ev) 38 | } else { 39 | this.domListener = ev => this.bubble(ev) 40 | } 41 | 42 | topElement.addEventListener(eventType, this.domListener, useCapture) 43 | } 44 | 45 | bubble (rawEvent) { 46 | if (!document.body.contains(rawEvent.currentTarget)) { return } 47 | const ev = patchEvent(rawEvent) 48 | 49 | for (let el = ev.target; el && el !== this.roof; el = el.parentElement) { 50 | if (ev.propagationHasBeenStopped) { return } 51 | this.matchEventAgainstDestinations(el, ev) 52 | } 53 | } 54 | 55 | matchEventAgainstDestinations (el, ev) { 56 | for (let i = 0, n = this.destinations.length; i < n; ++i) { 57 | const dest = this.destinations[i] 58 | if (!dest.scopeChecker.isStrictlyInRootScope(el)) { continue } 59 | if (matchesSelector(el, dest.selector)) { 60 | mutateEventCurrentTarget(ev, el) 61 | dest.subject.onNext(ev) 62 | } 63 | } 64 | } 65 | 66 | capture (ev) { 67 | for (let i = 0, n = this.destinations.length; i < n; i++) { 68 | const dest = this.destinations[i] 69 | if (matchesSelector(ev.target, dest.selector)) { 70 | dest.subject.onNext(ev) 71 | } 72 | } 73 | } 74 | 75 | addDestination (subject, namespace) { 76 | const scope = getScope(namespace) 77 | const selector = getSelectors(namespace) 78 | const scopeChecker = new ScopeChecker(scope, this.isolateModule) 79 | this.destinations.push({subject, scopeChecker, selector}) 80 | } 81 | 82 | updateTopElement (newTopElement) { 83 | const eventType = this.eventType 84 | const domListener = this.domListener 85 | const useCapture = this.useCapture 86 | this.topElement.removeEventListener(eventType, domListener, useCapture) 87 | newTopElement.addEventListener(eventType, domListener, useCapture) 88 | this.topElement = newTopElement 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/isolate/module.js: -------------------------------------------------------------------------------- 1 | export class IsolateModule { 2 | constructor (isolatedElements) { 3 | this.isolatedElements = isolatedElements 4 | this.eventDelegators = new Map([]) 5 | } 6 | 7 | setScope (elm, scope) { 8 | this.isolatedElements.set(scope, elm) 9 | } 10 | 11 | removeScope (scope) { 12 | this.isolatedElements.delete(scope) 13 | } 14 | 15 | getIsolatedElement (scope) { 16 | return this.isolatedElements.get(scope) 17 | } 18 | 19 | isIsolatedElement (elm) { 20 | const elements = Array.from(this.isolatedElements.entries()) 21 | 22 | for (let i = 0; i < elements.length; ++i) { 23 | if (elm === elements[i][1]) { 24 | return elements[i][0] 25 | } 26 | } 27 | return false 28 | } 29 | 30 | addEventDelegator (scope, eventDelegator) { 31 | let delegators = this.eventDelegators.get(scope) 32 | if (!delegators) { 33 | delegators = [] 34 | this.eventDelegators.set(scope, delegators) 35 | } 36 | delegators[delegators.length] = eventDelegator 37 | } 38 | 39 | reset () { 40 | this.isolatedElements.clear() 41 | } 42 | 43 | createModule () { 44 | const self = this 45 | return { 46 | create (oldVNode, vNode) { // eslint-disable-line complexity 47 | const {data: oldData = {}} = oldVNode 48 | const {elm, data = {}} = vNode 49 | const oldScope = oldData.isolate || '' 50 | const scope = data.isolate || '' 51 | if (scope) { 52 | if (oldScope) { self.removeScope(oldScope) } 53 | self.setScope(elm, scope) 54 | const delegators = self.eventDelegators.get(scope) 55 | if (delegators) { 56 | for (let i = 0, len = delegators.length; i < len; ++i) { 57 | delegators[i].updateTopElement(elm) 58 | } 59 | } else if (delegators === void 0) { 60 | self.eventDelegators.set(scope, []) 61 | } 62 | } 63 | if (oldScope && !scope) { 64 | self.removeScope(scope) 65 | } 66 | }, 67 | 68 | update (oldVNode, vNode) { // eslint-disable-line complexity 69 | const {data: oldData = {}} = oldVNode 70 | const {elm, data = {}} = vNode 71 | const oldScope = oldData.isolate || '' 72 | const scope = data.isolate || '' 73 | if (scope) { 74 | if (oldScope) { self.removeScope(oldScope) } 75 | self.setScope(elm, scope) 76 | } 77 | if (oldScope && !scope) { 78 | self.removeScope(scope) 79 | } 80 | }, 81 | 82 | remove ({data = {}}, cb) { 83 | const scope = data.isolate 84 | if (scope) { 85 | self.removeScope(scope) 86 | if (self.eventDelegators.get(scope)) { 87 | self.eventDelegators.set(scope, []) 88 | } 89 | } 90 | cb() 91 | }, 92 | 93 | destroy ({data = {}}) { 94 | const scope = data.isolate 95 | if (scope) { 96 | self.removeScope(scope) 97 | if (self.eventDelegators.get(scope)) { 98 | self.eventDelegators.set(scope, []) 99 | } 100 | } 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/hyperscript/helpers.js: -------------------------------------------------------------------------------- 1 | import {h} from './h' 2 | 3 | function isValidString (param) { 4 | return typeof param === 'string' && param.length > 0 5 | } 6 | 7 | function isSelector (param) { 8 | return isValidString(param) && (param[0] === '.' || param[0] === '#') 9 | } 10 | 11 | function createTagFunction (tagName) { 12 | return function hyperscript (first, b, c) { // eslint-disable-line complexity 13 | if (isSelector(first)) { 14 | if (!!b && !!c) { 15 | return h(tagName + first, b, c) 16 | } else if (!!b) { // eslint-disable-line no-extra-boolean-cast 17 | return h(tagName + first, b) 18 | } else { 19 | return h(tagName + first, {}) 20 | } 21 | } else if (!!b) { // eslint-disable-line no-extra-boolean-cast 22 | return h(tagName, first, b) 23 | } else if (!!first) { // eslint-disable-line no-extra-boolean-cast 24 | return h(tagName, first) 25 | } else { 26 | return h(tagName, {}) 27 | } 28 | } 29 | } 30 | 31 | const SVG_TAG_NAMES = [ 32 | 'a', 'altGlyph', 'altGlyphDef', 'altGlyphItem', 'animate', 'animateColor', 33 | 'animateMotion', 'animateTransform', 'animateTransform', 'circle', 'clipPath', 34 | 'colorProfile', 'cursor', 'defs', 'desc', 'ellipse', 'feBlend', 'feColorMatrix', 35 | 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 36 | 'feDisplacementMap', 'feDistantLight', 'feFlood', 'feFuncA', 'feFuncB', 37 | 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 38 | 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotlight', 39 | 'feTile', 'feTurbulence', 'filter', 'font', 'fontFace', 'fontFaceFormat', 40 | 'fontFaceName', 'fontFaceSrc', 'fontFaceUri', 'foreignObject', 'g', 41 | 'glyph', 'glyphRef', 'hkern', 'image', 'line', 'linearGradient', 'marker', 42 | 'mask', 'metadata', 'missingGlyph', 'mpath', 'path', 'pattern', 'polygon', 43 | 'polyling', 'radialGradient', 'rect', 'script', 'set', 'stop', 'style', 44 | 'switch', 'symbol', 'text', 'textPath', 'title', 'tref', 'tspan', 'use', 45 | 'view', 'vkern' 46 | ] 47 | 48 | const svg = createTagFunction('svg') 49 | 50 | SVG_TAG_NAMES.forEach(tag => { 51 | svg[tag] = createTagFunction(tag) 52 | }) 53 | 54 | const TAG_NAMES = [ 55 | 'a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 56 | 'bdi', 'bdo', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 57 | 'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', 58 | 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 59 | 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 60 | 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 61 | 'li', 'link', 'main', 'map', 'mark', 'menu', 'meta', 'nav', 'noscript', 62 | 'object', 'ol', 'optgroup', 'option', 'p', 'param', 'pre', 'q', 'rp', 'rt', 63 | 'ruby', 's', 'samp', 'script', 'section', 'select', 'small', 'source', 'span', 64 | 'strong', 'style', 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 65 | 'tfoot', 'th', 'thead', 'title', 'tr', 'u', 'ul', 'video', 'progress' 66 | ] 67 | 68 | const exported = { 69 | SVG_TAG_NAMES, 70 | TAG_NAMES, 71 | svg, 72 | isSelector, 73 | createTagFunction 74 | } 75 | 76 | for (let i = 0; i < TAG_NAMES.length; ++i) { 77 | exported[TAG_NAMES[i]] = createTagFunction(TAG_NAMES[i]) 78 | } 79 | 80 | export default exported 81 | -------------------------------------------------------------------------------- /perf/dbmon/resources/memory-stats.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author mrdoob / http://mrdoob.com/ 3 | * @author jetienne / http://jetienne.com/ 4 | * @author paulirish / http://paulirish.com/ 5 | */ 6 | var MemoryStats = function (){ 7 | 8 | var msMin = 100; 9 | var msMax = 0; 10 | 11 | var container = document.createElement( 'div' ); 12 | container.id = 'stats'; 13 | container.style.cssText = 'width:80px;opacity:0.9;cursor:pointer'; 14 | 15 | var msDiv = document.createElement( 'div' ); 16 | msDiv.id = 'ms'; 17 | msDiv.style.cssText = 'padding:0 0 3px 3px;text-align:left;background-color:#020;'; 18 | container.appendChild( msDiv ); 19 | 20 | var msText = document.createElement( 'div' ); 21 | msText.id = 'msText'; 22 | msText.style.cssText = 'color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px'; 23 | msText.innerHTML= 'Memory'; 24 | msDiv.appendChild( msText ); 25 | 26 | var msGraph = document.createElement( 'div' ); 27 | msGraph.id = 'msGraph'; 28 | msGraph.style.cssText = 'position:relative;width:74px;height:30px;background-color:#0f0'; 29 | msDiv.appendChild( msGraph ); 30 | 31 | while ( msGraph.children.length < 74 ) { 32 | 33 | var bar = document.createElement( 'span' ); 34 | bar.style.cssText = 'width:1px;height:30px;float:left;background-color:#131'; 35 | msGraph.appendChild( bar ); 36 | 37 | } 38 | 39 | var updateGraph = function ( dom, height, color ) { 40 | 41 | var child = dom.appendChild( dom.firstChild ); 42 | child.style.height = height + 'px'; 43 | if( color ) child.style.backgroundColor = color; 44 | 45 | } 46 | 47 | var perf = window.performance || {}; 48 | // polyfill usedJSHeapSize 49 | if (!perf && !perf.memory){ 50 | perf.memory = { usedJSHeapSize : 0 }; 51 | } 52 | if (perf && !perf.memory){ 53 | perf.memory = { usedJSHeapSize : 0 }; 54 | } 55 | 56 | // support of the API? 57 | if( perf.memory.totalJSHeapSize === 0 ){ 58 | console.warn('totalJSHeapSize === 0... performance.memory is only available in Chrome .') 59 | } 60 | 61 | // TODO, add a sanity check to see if values are bucketed. 62 | // If so, reminde user to adopt the --enable-precise-memory-info flag. 63 | // open -a "/Applications/Google Chrome.app" --args --enable-precise-memory-info 64 | 65 | var lastTime = Date.now(); 66 | var lastUsedHeap= perf.memory.usedJSHeapSize; 67 | return { 68 | domElement: container, 69 | 70 | update: function () { 71 | 72 | // refresh only 30time per second 73 | if( Date.now() - lastTime < 1000/30 ) return; 74 | lastTime = Date.now() 75 | 76 | var delta = perf.memory.usedJSHeapSize - lastUsedHeap; 77 | lastUsedHeap = perf.memory.usedJSHeapSize; 78 | var color = delta < 0 ? '#830' : '#131'; 79 | 80 | var ms = perf.memory.usedJSHeapSize; 81 | msMin = Math.min( msMin, ms ); 82 | msMax = Math.max( msMax, ms ); 83 | msText.textContent = "Mem: " + bytesToSize(ms, 2); 84 | 85 | var normValue = ms / (30*1024*1024); 86 | var height = Math.min( 30, 30 - normValue * 30 ); 87 | updateGraph( msGraph, height, color); 88 | 89 | function bytesToSize( bytes, nFractDigit ){ 90 | var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; 91 | if (bytes == 0) return 'n/a'; 92 | nFractDigit = nFractDigit !== undefined ? nFractDigit : 0; 93 | var precision = Math.pow(10, nFractDigit); 94 | var i = Math.floor(Math.log(bytes) / Math.log(1024)); 95 | return Math.round(bytes*precision / Math.pow(1024, i))/precision + ' ' + sizes[i]; 96 | }; 97 | } 98 | 99 | } 100 | 101 | }; 102 | -------------------------------------------------------------------------------- /test/browser/dom-driver.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* global describe, it */ 3 | let assert = require('assert') 4 | let Cycle = require('@cycle/core') 5 | let CycleDOM = require('../../src/index') 6 | let Observable = require('rx').Observable 7 | let {div, h3, makeDOMDriver} = CycleDOM 8 | 9 | function createRenderTarget (id = null) { 10 | let element = document.createElement('div') 11 | element.className = 'cycletest' 12 | if (id) { 13 | element.id = id 14 | } 15 | document.body.appendChild(element) 16 | return element 17 | } 18 | 19 | describe('makeDOMDriver', function () { 20 | it('should accept a DOM element as input', function () { 21 | const element = createRenderTarget() 22 | assert.doesNotThrow(function () { 23 | makeDOMDriver(element) 24 | }) 25 | }) 26 | 27 | it('should accept a DocumentFragment as input', function () { 28 | const element = document.createDocumentFragment() 29 | assert.doesNotThrow(function () { 30 | makeDOMDriver(element) 31 | }) 32 | }) 33 | 34 | it('should accept a string selector to an existing element as input', function () { 35 | const id = 'testShouldAcceptSelectorToExisting' 36 | const element = createRenderTarget() 37 | element.id = id 38 | assert.doesNotThrow(function () { 39 | makeDOMDriver('#' + id) 40 | }) 41 | }) 42 | 43 | it('should not accept a selector to an unknown element as input', function () { 44 | assert.throws(function () { 45 | makeDOMDriver('#nonsenseIdToNothing') 46 | }, /Cannot render into unknown element/) 47 | }) 48 | 49 | it('should not accept a number as input', function () { 50 | assert.throws(function () { 51 | makeDOMDriver(123) 52 | }, /Given container is not a DOM element neither a selector string/) 53 | }) 54 | }) 55 | 56 | describe('DOM Driver', function () { 57 | it('should throw if input is not an Observable', function () { 58 | const domDriver = makeDOMDriver(createRenderTarget()) 59 | assert.throws(function () { 60 | domDriver({}) 61 | }, /The DOM driver function expects as input an Observable of virtual/) 62 | }) 63 | 64 | it('should have isolateSource() and isolateSink() in source', function (done) { 65 | function app () { 66 | return { 67 | DOM: Observable.of(div([])) 68 | } 69 | } 70 | 71 | const {sinks, sources} = Cycle.run(app, { 72 | DOM: makeDOMDriver(createRenderTarget()) 73 | }) 74 | assert.strictEqual(typeof sources.DOM.isolateSource, 'function') 75 | assert.strictEqual(typeof sources.DOM.isolateSink, 'function') 76 | sinks.dispose() 77 | sources.dispose() 78 | done() 79 | }) 80 | 81 | it('should not work after has been disposed', function (done) { 82 | const number$ = Observable.from([1, 2, 3]) 83 | .concatMap(x => Observable.of(x).delay(50)) 84 | 85 | function app () { 86 | return { 87 | DOM: number$.map(number => 88 | h3('.target', String(number)) 89 | ) 90 | } 91 | } 92 | 93 | const {sinks, sources} = Cycle.run(app, { 94 | DOM: makeDOMDriver(createRenderTarget()) 95 | }) 96 | 97 | sources.DOM.select(':root').elements.skip(1).subscribe(function (root) { 98 | const selectEl = root.querySelector('.target') 99 | assert.notStrictEqual(selectEl, null) 100 | assert.notStrictEqual(typeof selectEl, 'undefined') 101 | assert.strictEqual(selectEl.tagName, 'H3') 102 | assert.notStrictEqual(selectEl.textContent, '3') 103 | if (selectEl.textContent === '2') { 104 | sinks.dispose() 105 | sources.dispose() 106 | setTimeout(() => { 107 | done() 108 | }, 100) 109 | } 110 | }) 111 | }) 112 | }) 113 | -------------------------------------------------------------------------------- /test/node/mock-dom-source.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* global describe, it */ 3 | let assert = require('assert') 4 | let {Observable} = require('rx') 5 | let CycleDOM = require('../../src/index') 6 | let mockDOMSource = CycleDOM.mockDOMSource 7 | 8 | describe('mockDOMSource', function () { 9 | it('should be in accessible in the API', function () { 10 | assert.strictEqual(typeof CycleDOM.mockDOMSource, 'function') 11 | }) 12 | 13 | it('should make an Observable for clicks on `.foo`', function (done) { 14 | const userEvents = mockDOMSource({ 15 | '.foo': { 16 | 'click': Observable.of(135) 17 | } 18 | }) 19 | userEvents.select('.foo').events('click') 20 | .subscribe(ev => { 21 | assert.strictEqual(ev, 135) 22 | done() 23 | }) 24 | }) 25 | 26 | it('should make multiple user event Observables', function (done) { 27 | const userEvents = mockDOMSource({ 28 | '.foo': { 29 | 'click': Observable.of(135) 30 | }, 31 | '.bar': { 32 | 'scroll': Observable.of(2) 33 | } 34 | }) 35 | Observable.combineLatest( 36 | userEvents.select('.foo').events('click'), 37 | userEvents.select('.bar').events('scroll'), 38 | (a, b) => a * b 39 | ).subscribe(ev => { 40 | assert.strictEqual(ev, 270) 41 | done() 42 | }, err => done(err)) 43 | }) 44 | 45 | it('should make multiple user event Observables on the same selector', function (done) { 46 | const userEvents = mockDOMSource({ 47 | '.foo': { 48 | 'click': Observable.of(135), 49 | 'scroll': Observable.of(3) 50 | } 51 | }) 52 | Observable.combineLatest( 53 | userEvents.select('.foo').events('click'), 54 | userEvents.select('.foo').events('scroll'), 55 | (a, b) => a * b 56 | ).subscribe(ev => { 57 | assert.strictEqual(ev, 405) 58 | done() 59 | }, done) 60 | }) 61 | 62 | it('should return an empty Observable if query does not match', function (done) { 63 | const userEvents = mockDOMSource({ 64 | '.foo': { 65 | 'click': Observable.of(135) 66 | } 67 | }) 68 | userEvents.select('.impossible').events('scroll') 69 | .subscribe(done, done, () => done()) 70 | }) 71 | 72 | it('should return empty Observable for select().elements and none is defined', function (done) { 73 | const userEvents = mockDOMSource({ 74 | '.foo': { 75 | 'click': Observable.of(135) 76 | } 77 | }) 78 | userEvents.select('.foo').elements 79 | .subscribe(done, done, () => done()) 80 | }) 81 | 82 | it('should return defined Observable for select().elements', function (done) { 83 | const mockedDOMSource = mockDOMSource({ 84 | '.foo': { 85 | elements: Observable.of(135) 86 | } 87 | }) 88 | mockedDOMSource.select('.foo').elements 89 | .subscribe(ev => { 90 | assert.strictEqual(ev, 135) 91 | done() 92 | }, done) 93 | }) 94 | it('should return defined Observable when chaining .select()', function (done) { 95 | const mockedDOMSource = mockDOMSource({ 96 | '.bar': { 97 | '.foo': { 98 | '.baz': { 99 | elements: Observable.of(135) 100 | } 101 | } 102 | } 103 | }) 104 | mockedDOMSource.select('.bar').select('.foo').select('.baz').elements 105 | .subscribe(ev => { 106 | assert.strictEqual(ev, 135) 107 | done() 108 | }, done) 109 | }) 110 | 111 | it('multiple .select()s should not throw when given empty mockedSelectors', () => { 112 | assert.doesNotThrow(() => { 113 | const DOM = mockDOMSource({}) 114 | DOM.select('.something').select('.other').events('click') 115 | }) 116 | }) 117 | 118 | it('multiple .select()s should return empty observable if not defined', () => { 119 | const DOM = mockDOMSource({}) 120 | const selector = DOM.select('.something').select('.other') 121 | assert.strictEqual(selector.events('click') instanceof Observable, true) 122 | assert.strictEqual(selector.elements instanceof Observable, true) 123 | }) 124 | }) 125 | -------------------------------------------------------------------------------- /src/DOMSource.js: -------------------------------------------------------------------------------- 1 | import {Subject} from 'rx' 2 | 3 | import {fromEvent} from './fromEvent' 4 | import {isolateSource, isolateSink} from './isolate/isolation' 5 | import {EventDelegator} from './EventDelegator' 6 | import {ElementFinder} from './ElementFinder' 7 | import {getScope, eventTypesThatDontBubble} from './util' 8 | 9 | function determineUseCapture (eventType, options) { 10 | let result = false 11 | if (typeof options.useCapture === 'boolean') { 12 | result = options.useCapture 13 | } 14 | 15 | if (eventTypesThatDontBubble.indexOf(eventType) !== -1) { 16 | result = true 17 | } 18 | 19 | return result 20 | } 21 | 22 | export class DOMSource { 23 | constructor (rootElement$, namespace, isolateModule, delegators) { 24 | this._rootElement$ = rootElement$ 25 | this._namespace = namespace 26 | this._isolateModule = isolateModule 27 | this._delegators = delegators 28 | } 29 | 30 | get elements () { 31 | if (this._namespace.length === 0) { return this._rootElement$ } 32 | 33 | const elementFinder = 34 | new ElementFinder(this._namespace, this._isolateModule) 35 | 36 | return this._rootElement$.map(el => elementFinder.call(el)) 37 | } 38 | 39 | get namespace () { 40 | return this._namespace 41 | } 42 | 43 | select (selector) { 44 | if (typeof selector !== 'string') { 45 | throw new Error('DOM driver\'s select() expects the argument to be a ' + 46 | 'string as a CSS selector') 47 | } 48 | 49 | const trimmedSelector = selector.trim() 50 | const childNamespace = trimmedSelector === ':root' 51 | ? this._namespace 52 | : this._namespace.concat(trimmedSelector) 53 | 54 | return new DOMSource( 55 | this._rootElement$, childNamespace, 56 | this._isolateModule, this._delegators) 57 | } 58 | 59 | events (eventType, options = {}) { // eslint-disable-line complexity 60 | if (typeof eventType !== 'string') { 61 | throw new Error('DOM driver\'s events() expects argument to be a ' + 62 | 'string representing the event type to listen for.') 63 | } 64 | const useCapture = determineUseCapture(eventType, options) 65 | 66 | const namespace = this._namespace 67 | const scope = getScope(namespace) 68 | const key = scope 69 | ? `${eventType}~${useCapture}~${scope}` 70 | : `${eventType}~${useCapture}` 71 | 72 | const domSource = this 73 | 74 | let rootElement$ 75 | if (scope) { 76 | let hadIsolatedMutable = false 77 | rootElement$ = this._rootElement$ 78 | .filter(function filterScopedElements (rootElement) { 79 | const hasIsolated = 80 | !!domSource._isolateModule.getIsolatedElement(scope) 81 | const shouldPass = hasIsolated && !hadIsolatedMutable 82 | hadIsolatedMutable = hasIsolated 83 | return shouldPass 84 | }) 85 | } else { 86 | rootElement$ = this._rootElement$.take(2) 87 | } 88 | 89 | return rootElement$ 90 | .flatMapLatest(function setupEventDelegators (rootElement) { 91 | if (!namespace || namespace.length === 0) { 92 | return fromEvent(eventType, rootElement, useCapture) 93 | } 94 | 95 | const delegators = domSource._delegators 96 | 97 | const top = scope 98 | ? domSource._isolateModule.getIsolatedElement(scope) 99 | : rootElement 100 | 101 | let delegator 102 | if (delegators.has(key)) { 103 | delegator = delegators.get(key) 104 | delegator.updateTopElement(top) 105 | } else { 106 | delegator = new EventDelegator( 107 | top, eventType, useCapture, domSource._isolateModule 108 | ) 109 | delegators.set(key, delegator) 110 | } 111 | const stream = new Subject() 112 | 113 | if (scope) { 114 | domSource._isolateModule.addEventDelegator(scope, delegator) 115 | } 116 | 117 | delegator.addDestination(stream, namespace) 118 | 119 | return stream 120 | }) 121 | .share() 122 | } 123 | 124 | dispose () { 125 | this._isolateModule.reset() 126 | } 127 | 128 | isolateSource (source, scope) { 129 | return isolateSource(source, scope) 130 | } 131 | 132 | isolateSink (sink, scope) { 133 | return isolateSink(sink, scope) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /test/browser/select.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* global describe, it */ 3 | let assert = require('assert') 4 | let Cycle = require('@cycle/core') 5 | let CycleDOM = require('../../src/index') 6 | let Observable = require('rx').Observable 7 | let {svg, div, p, span, h2, h3, h4, makeDOMDriver} = CycleDOM 8 | 9 | function createRenderTarget (id = null) { 10 | let element = document.createElement('div') 11 | element.className = 'cycletest' 12 | if (id) { 13 | element.id = id 14 | } 15 | document.body.appendChild(element) 16 | return element 17 | } 18 | 19 | describe('DOMSource.select()', function () { 20 | it('should have Observable `:root` in DOM source', function (done) { 21 | function app () { 22 | return { 23 | DOM: Observable.of( 24 | div('.top-most', [ 25 | p('Foo'), 26 | span('Bar') 27 | ]) 28 | ) 29 | } 30 | } 31 | 32 | const {sinks, sources} = Cycle.run(app, { 33 | DOM: makeDOMDriver(createRenderTarget()) 34 | }) 35 | 36 | sources.DOM.select(':root').elements.skip(1).take(1).subscribe(root => { 37 | const classNameRegex = /top\-most/ 38 | assert.strictEqual(root.tagName, 'DIV') 39 | const child = root.children[0] 40 | assert.notStrictEqual(classNameRegex.exec(child.className), null) 41 | assert.strictEqual(classNameRegex.exec(child.className)[0], 'top-most') 42 | setTimeout(() => { 43 | sinks.dispose() 44 | done() 45 | }) 46 | }) 47 | }) 48 | 49 | it('should return an object with observable and events()', function (done) { 50 | function app () { 51 | return { 52 | DOM: Observable.of(h3('.myelementclass', 'Foobar')) 53 | } 54 | } 55 | 56 | const {sinks, sources} = Cycle.run(app, { 57 | DOM: makeDOMDriver(createRenderTarget()) 58 | }) 59 | 60 | // Make assertions 61 | const selection = sources.DOM.select('.myelementclass') 62 | assert.strictEqual(typeof selection, 'object') 63 | assert.strictEqual(typeof selection.elements, 'object') 64 | assert.strictEqual(typeof selection.elements.subscribe, 'function') 65 | assert.strictEqual(typeof selection.events, 'function') 66 | sinks.dispose() 67 | done() 68 | }) 69 | 70 | it('should have an observable of DOM elements', function (done) { 71 | function app () { 72 | return { 73 | DOM: Observable.of(h3('.myelementclass', 'Foobar')) 74 | } 75 | } 76 | 77 | const {sinks, sources} = Cycle.run(app, { 78 | DOM: makeDOMDriver(createRenderTarget()) 79 | }) 80 | 81 | // Make assertions 82 | sources.DOM.select('.myelementclass').elements.skip(1).take(1) 83 | .subscribe(elements => { 84 | assert.notStrictEqual(elements, null) 85 | assert.notStrictEqual(typeof elements, 'undefined') 86 | // Is an Array 87 | assert.strictEqual(Array.isArray(elements), true) 88 | assert.strictEqual(elements.length, 1) 89 | // Array with the H3 element 90 | assert.strictEqual(elements[0].tagName, 'H3') 91 | assert.strictEqual(elements[0].textContent, 'Foobar') 92 | setTimeout(() => { 93 | sinks.dispose() 94 | done() 95 | }) 96 | }) 97 | }) 98 | 99 | it('should not select element outside the given scope', function (done) { 100 | function app () { 101 | return { 102 | DOM: Observable.of( 103 | h3('.top-most', [ 104 | h2('.bar', 'Wrong'), 105 | div('.foo', [ 106 | h4('.bar', 'Correct') 107 | ]) 108 | ]) 109 | ) 110 | } 111 | } 112 | 113 | const {sinks, sources} = Cycle.run(app, { 114 | DOM: makeDOMDriver(createRenderTarget()) 115 | }) 116 | 117 | // Make assertions 118 | sources.DOM.select('.foo').select('.bar').elements.skip(1).take(1) 119 | .subscribe(elements => { 120 | assert.strictEqual(elements.length, 1) 121 | const element = elements[0] 122 | assert.notStrictEqual(element, null) 123 | assert.notStrictEqual(typeof element, 'undefined') 124 | assert.strictEqual(element.tagName, 'H4') 125 | assert.strictEqual(element.textContent, 'Correct') 126 | setTimeout(() => { 127 | sinks.dispose() 128 | done() 129 | }) 130 | }) 131 | }) 132 | 133 | it('should select svg element', function (done) { 134 | function app () { 135 | return { 136 | DOM: Observable.of( 137 | svg({width: 150, height: 150}, [ 138 | svg.polygon({ 139 | attrs: { 140 | class: 'triangle', 141 | points: '20 0 20 150 150 20' 142 | } 143 | }) 144 | ]) 145 | ) 146 | } 147 | } 148 | 149 | const {sinks, sources} = Cycle.run(app, { 150 | DOM: makeDOMDriver(createRenderTarget()) 151 | }) 152 | 153 | // Make assertions 154 | sources.DOM.select('.triangle').elements.skip(1).take(1) 155 | .subscribe(elements => { 156 | assert.strictEqual(elements.length, 1) 157 | const triangleElement = elements[0] 158 | assert.notStrictEqual(triangleElement, null) 159 | assert.notStrictEqual(typeof triangleElement, 'undefined') 160 | assert.strictEqual(triangleElement.tagName, 'polygon') 161 | sinks.dispose() 162 | done() 163 | }) 164 | }) 165 | }) 166 | -------------------------------------------------------------------------------- /test/browser/render.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* global describe, it */ 3 | let assert = require('assert') 4 | let Cycle = require('@cycle/core') 5 | let CycleDOM = require('../../src/index') 6 | let Observable = require('rx').Observable 7 | let {html} = require('snabbdom-jsx') // eslint-disable-line no-unused-vars 8 | let {div, h2, h4, select, option, thunk, makeDOMDriver} = CycleDOM 9 | 10 | function createRenderTarget (id = null) { 11 | let element = document.createElement('div') 12 | element.className = 'cycletest' 13 | if (id) { 14 | element.id = id 15 | } 16 | document.body.appendChild(element) 17 | return element 18 | } 19 | 20 | describe('DOM Rendering', function () { 21 | it('should render DOM elements even when DOMSource is not utilized', function (done) { 22 | function main () { 23 | return { 24 | DOM: Observable.of( 25 | div('.my-render-only-container', [ 26 | h2('Cycle.js framework') 27 | ]) 28 | ) 29 | } 30 | } 31 | 32 | Cycle.run(main, { 33 | DOM: makeDOMDriver(createRenderTarget()) 34 | }) 35 | 36 | setTimeout(() => { 37 | const myContainer = document.querySelector('.my-render-only-container') 38 | assert.notStrictEqual(myContainer, null) 39 | assert.notStrictEqual(typeof myContainer, 'undefined') 40 | assert.strictEqual(myContainer.tagName, 'DIV') 41 | const header = myContainer.querySelector('h2') 42 | assert.notStrictEqual(header, null) 43 | assert.notStrictEqual(typeof header, 'undefined') 44 | assert.strictEqual(header.textContent, 'Cycle.js framework') 45 | done() 46 | }, 150) 47 | }) 48 | 49 | it('should convert a simple virtual-dom (JSX) to DOM element', function (done) { 78 | function app () { 79 | return { 80 | DOM: Observable.of( 81 | 86 | ) 87 | } 88 | } 89 | 90 | const {sinks, sources} = Cycle.run(app, { 91 | DOM: makeDOMDriver(createRenderTarget(), {transposition: true}) 92 | }) 93 | 94 | sources.DOM.select(':root').elements.skip(1).take(1).subscribe(function (root) { 95 | const selectEl = root.querySelector('.my-class') 96 | assert.notStrictEqual(selectEl, null) 97 | assert.notStrictEqual(typeof selectEl, 'undefined') 98 | assert.strictEqual(selectEl.tagName, 'SELECT') 99 | setTimeout(() => { 100 | sinks.dispose() 101 | sources.dispose() 102 | done() 103 | }) 104 | }) 105 | }) 106 | 107 | it('should allow snabbdom Thunks in the VTree', function (done) { 108 | function renderThunk (greeting) { 109 | return h4('Constantly ' + greeting) 110 | } 111 | 112 | // The Cycle.js app 113 | function app () { 114 | return { 115 | DOM: Observable.interval(10).take(5).map(i => 116 | div([ 117 | thunk('h4', renderThunk, ['hello' + 0]) 118 | ]) 119 | ) 120 | } 121 | } 122 | 123 | // Run it 124 | const {sinks, sources} = Cycle.run(app, { 125 | DOM: makeDOMDriver(createRenderTarget()) 126 | }) 127 | 128 | // Assert it 129 | sources.DOM.select(':root').elements.skip(1).take(1).subscribe(function (root) { 130 | const selectEl = root.querySelector('h4') 131 | assert.notStrictEqual(selectEl, null) 132 | assert.notStrictEqual(typeof selectEl, 'undefined') 133 | assert.strictEqual(selectEl.tagName, 'H4') 134 | assert.strictEqual(selectEl.textContent, 'Constantly hello0') 135 | sources.dispose() 136 | sinks.dispose() 137 | done() 138 | }) 139 | }) 140 | 141 | it('should filter out null/undefined children', function (done) { 142 | // The Cycle.js app 143 | function app () { 144 | return { 145 | DOM: Observable.interval(10).take(5).map(i => 146 | div('.parent', [ 147 | 'Child 1', 148 | null, 149 | h4('.child3', [ 150 | null, 151 | 'Grandchild 31', 152 | div('.grandchild32', [ 153 | null, 154 | 'Great grandchild 322' 155 | ]) 156 | ]), 157 | undefined 158 | ]) 159 | ) 160 | } 161 | } 162 | 163 | // Run it 164 | const {sinks, sources} = Cycle.run(app, { 165 | DOM: makeDOMDriver(createRenderTarget()) 166 | }) 167 | 168 | // Assert it 169 | sources.DOM.select(':root').elements.skip(1).take(1).subscribe(function (root) { 170 | assert.strictEqual(root.querySelector('div.parent').childNodes.length, 2) 171 | assert.strictEqual(root.querySelector('h4.child3').childNodes.length, 2) 172 | assert.strictEqual(root.querySelector('div.grandchild32').childNodes.length, 1) 173 | sinks.dispose() 174 | sources.dispose() 175 | done() 176 | }) 177 | }) 178 | }) 179 | -------------------------------------------------------------------------------- /test/node/html-render.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* global describe, it*/ 3 | let assert = require('assert') 4 | let Cycle = require('@cycle/core') 5 | let CycleDOM = require('../../src/index') 6 | let Observable = require('rx').Observable 7 | let {div, h, makeHTMLDriver} = CycleDOM 8 | 9 | describe('HTML Driver', function () { 10 | it('should output HTML when given a simple vtree stream', function (done) { 11 | function app () { 12 | return { 13 | html: Observable.of(div('.test-element', ['Foobar'])) 14 | } 15 | } 16 | let {sinks, sources} = Cycle.run(app, { 17 | html: makeHTMLDriver() 18 | }) 19 | sources.html.elements.subscribe(html => { 20 | assert.strictEqual(html, '
Foobar
') 21 | sinks.dispose() 22 | sources.dispose() 23 | done() 24 | }) 25 | }) 26 | 27 | it('should make bogus select().events() as sources', function (done) { 28 | function app ({html}) { 29 | assert.strictEqual(typeof html.select, 'function') 30 | assert.strictEqual(typeof html.select('whatever').elements.subscribe, 'function') 31 | assert.strictEqual(typeof html.select('whatever').events().subscribe, 'function') 32 | return { 33 | html: Observable.of(div('.test-element', ['Foobar'])) 34 | } 35 | } 36 | let {sinks, sources} = Cycle.run(app, { 37 | html: makeHTMLDriver() 38 | }) 39 | sources.html.elements.subscribe(html => { 40 | assert.strictEqual(html, '
Foobar
') 41 | sinks.dispose() 42 | sources.dispose() 43 | done() 44 | }) 45 | }) 46 | 47 | it('should output simple HTML Observable', function (done) { 48 | function app () { 49 | return { 50 | html: Observable.of(div('.test-element', ['Foobar'])) 51 | } 52 | } 53 | let {sinks, sources} = Cycle.run(app, { 54 | html: makeHTMLDriver() 55 | }) 56 | sources.html.elements.subscribe(html => { 57 | assert.strictEqual(html, '
Foobar
') 58 | sinks.dispose() 59 | sources.dispose() 60 | done() 61 | }) 62 | }) 63 | 64 | describe('with transposition=true', function () { 65 | it('should render a simple nested vtree$ as HTML', function (done) { 66 | function app () { 67 | return { 68 | DOM: Observable.of(h('div.test-element', [ 69 | Observable.of(h('h3.myelementclass')) 70 | ])) 71 | } 72 | } 73 | let {sinks, sources} = Cycle.run(app, { 74 | DOM: makeHTMLDriver({transposition: true}) 75 | }) 76 | sources.DOM.elements.subscribe(html => { 77 | assert.strictEqual(html, 78 | '
' + 79 | '

' + 80 | '
' 81 | ) 82 | sinks.dispose() 83 | sources.dispose() 84 | done() 85 | }) 86 | }) 87 | 88 | it('should render double nested vtree$ as HTML', function (done) { 89 | function app () { 90 | return { 91 | html: Observable.of(h('div.test-element', [ 92 | Observable.of(h('div.a-nice-element', [ 93 | String('foobar'), 94 | Observable.of(h('h3.myelementclass')) 95 | ])) 96 | ])) 97 | } 98 | } 99 | let {sinks, sources} = Cycle.run(app, { 100 | html: makeHTMLDriver({transposition: true}) 101 | }) 102 | 103 | sources.html.elements.subscribe(html => { 104 | assert.strictEqual(html, 105 | '
' + 106 | '
' + 107 | 'foobar

' + 108 | '
' + 109 | '
' 110 | ) 111 | sinks.dispose() 112 | sources.dispose() 113 | done() 114 | }) 115 | }) 116 | 117 | it('should HTML-render a nested vtree$ with props', function (done) { 118 | function myElement (foobar$) { 119 | return foobar$.map(foobar => 120 | h('h3.myelementclass', String(foobar).toUpperCase()) 121 | ) 122 | } 123 | function app () { 124 | return { 125 | DOM: Observable.of( 126 | h('div.test-element', [ 127 | myElement(Observable.of('yes')) 128 | ]) 129 | ) 130 | } 131 | } 132 | let {sinks, sources} = Cycle.run(app, { 133 | DOM: makeHTMLDriver({transposition: true}) 134 | }) 135 | 136 | sources.DOM.elements.subscribe(html => { 137 | assert.strictEqual(html, 138 | '
' + 139 | '

YES

' + 140 | '
' 141 | ) 142 | sinks.dispose() 143 | sources.dispose() 144 | done() 145 | }) 146 | }) 147 | 148 | it('should render a complex and nested vtree$ as HTML', function (done) { 149 | function app () { 150 | return { 151 | html: Observable.of( 152 | h('.test-element', [ 153 | h('div', [ 154 | h('h2.a', 'a'), 155 | h('h4.b', 'b'), 156 | Observable.of(h('h1.fooclass')) 157 | ]), 158 | h('div', [ 159 | h('h3.c', 'c'), 160 | h('div', [ 161 | h('p.d', 'd'), 162 | Observable.of(h('h2.barclass')) 163 | ]) 164 | ]) 165 | ]) 166 | ) 167 | } 168 | } 169 | let {sinks, sources} = Cycle.run(app, { 170 | html: makeHTMLDriver({transposition: true}) 171 | }) 172 | 173 | sources.html.elements.subscribe(html => { 174 | assert.strictEqual(html, 175 | '
' + 176 | '
' + 177 | '

a

' + 178 | '

b

' + 179 | '

' + 180 | '
' + 181 | '
' + 182 | '

c

' + 183 | '
' + 184 | '

d

' + 185 | '

' + 186 | '
' + 187 | '
' + 188 | '
' 189 | ) 190 | sinks.dispose() 191 | sources.dispose() 192 | done() 193 | }) 194 | }) 195 | }) 196 | }) 197 | -------------------------------------------------------------------------------- /perf/dbmon/resources/ENV.js: -------------------------------------------------------------------------------- 1 | var ENV = ENV || (function() { 2 | 3 | var first = true; 4 | var counter = 0; 5 | var data; 6 | var _base; 7 | (_base = String.prototype).lpad || (_base.lpad = function(padding, toLength) { 8 | return padding.repeat((toLength - this.length) / padding.length).concat(this); 9 | }); 10 | 11 | function formatElapsed(value) { 12 | var str = parseFloat(value).toFixed(2); 13 | if (value > 60) { 14 | minutes = Math.floor(value / 60); 15 | comps = (value % 60).toFixed(2).split('.'); 16 | seconds = comps[0].lpad('0', 2); 17 | ms = comps[1]; 18 | str = minutes + ":" + seconds + "." + ms; 19 | } 20 | return str; 21 | } 22 | 23 | function getElapsedClassName(elapsed) { 24 | var className = 'Query elapsed'; 25 | if (elapsed >= 10.0) { 26 | className += ' warn_long'; 27 | } 28 | else if (elapsed >= 1.0) { 29 | className += ' warn'; 30 | } 31 | else { 32 | className += ' short'; 33 | } 34 | return className; 35 | } 36 | 37 | function countClassName(queries) { 38 | var countClassName = "label"; 39 | if (queries >= 20) { 40 | countClassName += " label-important"; 41 | } 42 | else if (queries >= 10) { 43 | countClassName += " label-warning"; 44 | } 45 | else { 46 | countClassName += " label-success"; 47 | } 48 | return countClassName; 49 | } 50 | 51 | function updateQuery(object) { 52 | if (!object) { 53 | object = {}; 54 | } 55 | var elapsed = Math.random() * 15; 56 | object.elapsed = elapsed; 57 | object.formatElapsed = formatElapsed(elapsed); 58 | object.elapsedClassName = getElapsedClassName(elapsed); 59 | object.query = "SELECT blah FROM something"; 60 | object.waiting = Math.random() < 0.5; 61 | if (Math.random() < 0.2) { 62 | object.query = " in transaction"; 63 | } 64 | if (Math.random() < 0.1) { 65 | object.query = "vacuum"; 66 | } 67 | return object; 68 | } 69 | 70 | function cleanQuery(value) { 71 | if (value) { 72 | value.formatElapsed = ""; 73 | value.elapsedClassName = ""; 74 | value.query = ""; 75 | value.elapsed = null; 76 | value.waiting = null; 77 | } else { 78 | return { 79 | query: "***", 80 | formatElapsed: "", 81 | elapsedClassName: "" 82 | }; 83 | } 84 | } 85 | 86 | function generateRow(object, keepIdentity, counter) { 87 | var nbQueries = Math.floor((Math.random() * 10) + 1); 88 | if (!object) { 89 | object = {}; 90 | } 91 | object.lastMutationId = counter; 92 | object.nbQueries = nbQueries; 93 | if (!object.lastSample) { 94 | object.lastSample = {}; 95 | } 96 | if (!object.lastSample.topFiveQueries) { 97 | object.lastSample.topFiveQueries = []; 98 | } 99 | if (keepIdentity) { 100 | // for Angular optimization 101 | if (!object.lastSample.queries) { 102 | object.lastSample.queries = []; 103 | for (var l = 0; l < 12; l++) { 104 | object.lastSample.queries[l] = cleanQuery(); 105 | } 106 | } 107 | for (var j in object.lastSample.queries) { 108 | var value = object.lastSample.queries[j]; 109 | if (j <= nbQueries) { 110 | updateQuery(value); 111 | } else { 112 | cleanQuery(value); 113 | } 114 | } 115 | } else { 116 | object.lastSample.queries = []; 117 | for (var j = 0; j < 12; j++) { 118 | if (j < nbQueries) { 119 | var value = updateQuery(cleanQuery()); 120 | object.lastSample.queries.push(value); 121 | } else { 122 | object.lastSample.queries.push(cleanQuery()); 123 | } 124 | } 125 | } 126 | for (var i = 0; i < 5; i++) { 127 | var source = object.lastSample.queries[i]; 128 | object.lastSample.topFiveQueries[i] = source; 129 | } 130 | object.lastSample.nbQueries = nbQueries; 131 | object.lastSample.countClassName = countClassName(nbQueries); 132 | return object; 133 | } 134 | 135 | function getData(keepIdentity) { 136 | var oldData = data; 137 | if (!keepIdentity) { // reset for each tick when !keepIdentity 138 | data = []; 139 | for (var i = 1; i <= ENV.rows; i++) { 140 | data.push({ dbname: 'cluster' + i, query: "", formatElapsed: "", elapsedClassName: "" }); 141 | data.push({ dbname: 'cluster' + i + ' slave', query: "", formatElapsed: "", elapsedClassName: "" }); 142 | } 143 | } 144 | if (!data) { // first init when keepIdentity 145 | data = []; 146 | for (var i = 1; i <= ENV.rows; i++) { 147 | data.push({ dbname: 'cluster' + i }); 148 | data.push({ dbname: 'cluster' + i + ' slave' }); 149 | } 150 | oldData = data; 151 | } 152 | for (var i in data) { 153 | var row = data[i]; 154 | if (!keepIdentity && oldData && oldData[i]) { 155 | row.lastSample = oldData[i].lastSample; 156 | } 157 | if (!row.lastSample || Math.random() < ENV.mutations()) { 158 | counter = counter + 1; 159 | if (!keepIdentity) { 160 | row.lastSample = null; 161 | } 162 | generateRow(row, keepIdentity, counter); 163 | } else { 164 | data[i] = oldData[i]; 165 | } 166 | } 167 | first = false; 168 | return { 169 | toArray: function() { 170 | return data; 171 | } 172 | }; 173 | } 174 | 175 | var mutationsValue = 0.5; 176 | 177 | function mutations(value) { 178 | if (value) { 179 | mutationsValue = value; 180 | return mutationsValue; 181 | } else { 182 | return mutationsValue; 183 | } 184 | } 185 | 186 | var body = document.querySelector('body'); 187 | var theFirstChild = body.firstChild; 188 | 189 | var sliderContainer = document.createElement( 'div' ); 190 | sliderContainer.style.cssText = "display: flex"; 191 | var slider = document.createElement('input'); 192 | var text = document.createElement('label'); 193 | text.innerHTML = 'mutations : ' + (mutationsValue * 100).toFixed(0) + '%'; 194 | text.id = "ratioval"; 195 | slider.setAttribute("type", "range"); 196 | slider.style.cssText = 'margin-bottom: 10px; margin-top: 5px'; 197 | slider.addEventListener('change', function(e) { 198 | ENV.mutations(e.target.value / 100); 199 | document.querySelector('#ratioval').innerHTML = 'mutations : ' + (ENV.mutations() * 100).toFixed(0) + '%'; 200 | }); 201 | sliderContainer.appendChild( text ); 202 | sliderContainer.appendChild( slider ); 203 | body.insertBefore( sliderContainer, theFirstChild ); 204 | 205 | return { 206 | generateData: getData, 207 | rows: 50, 208 | timeout: 0, 209 | mutations: mutations 210 | }; 211 | })(); 212 | -------------------------------------------------------------------------------- /test/browser/transposition.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* global describe, it */ 3 | let assert = require('assert') 4 | let Cycle = require('@cycle/core') 5 | let CycleDOM = require('../../src/index') 6 | let Fixture89 = require('./fixtures/issue-89') 7 | let Observable = require('rx').Observable 8 | let {html} = require('snabbdom-jsx') // eslint-disable-line no-unused-vars 9 | let {svg, div, p, h3, h4, makeDOMDriver} = CycleDOM 10 | 11 | function createRenderTarget (id = null) { 12 | let element = document.createElement('div') 13 | element.className = 'cycletest' 14 | if (id) { 15 | element.id = id 16 | } 17 | document.body.appendChild(element) 18 | return element 19 | } 20 | 21 | describe('DOM rendering with transposition', function () { 22 | it('should accept a view wrapping a VTree$ (#89)', function (done) { 23 | function app () { 24 | const number$ = Fixture89.makeModelNumber$() 25 | return { 26 | DOM: Fixture89.viewWithContainerFn(number$) 27 | } 28 | } 29 | 30 | const {sinks, sources} = Cycle.run(app, { 31 | DOM: makeDOMDriver(createRenderTarget(), {transposition: true}) 32 | }) 33 | 34 | sources.DOM.select('.myelementclass').elements.skip(1).take(1) // 1st 35 | .subscribe(function (elements) { 36 | const myelement = elements[0] 37 | assert.notStrictEqual(myelement, null) 38 | assert.strictEqual(myelement.tagName, 'H3') 39 | assert.strictEqual(myelement.textContent, '123') 40 | }) 41 | 42 | sources.DOM.select('.myelementclass').elements.skip(2).take(1) // 2nd 43 | .subscribe(function (elements) { 44 | const myelement = elements[0] 45 | assert.notStrictEqual(myelement, null) 46 | assert.strictEqual(myelement.tagName, 'H3') 47 | assert.strictEqual(myelement.textContent, '456') 48 | setTimeout(() => { 49 | sinks.dispose() 50 | done() 51 | }) 52 | }) 53 | }) 54 | 55 | it('should accept a view with VTree$ as the root of VTree', function (done) { 56 | function app () { 57 | const number$ = Fixture89.makeModelNumber$() 58 | return { 59 | DOM: Fixture89.viewWithoutContainerFn(number$) 60 | } 61 | } 62 | 63 | const {sinks, sources} = Cycle.run(app, { 64 | DOM: makeDOMDriver(createRenderTarget(), {transposition: true}) 65 | }) 66 | 67 | sources.DOM.select('.myelementclass').elements.skip(1).take(1) // 1st 68 | .subscribe(function (elements) { 69 | const myelement = elements[0] 70 | assert.notStrictEqual(myelement, null) 71 | assert.strictEqual(myelement.tagName, 'H3') 72 | assert.strictEqual(myelement.textContent, '123') 73 | }) 74 | sources.DOM.select('.myelementclass').elements.skip(2).take(1) // 1st 75 | .subscribe(function (elements) { 76 | const myelement = elements[0] 77 | assert.notStrictEqual(myelement, null) 78 | assert.strictEqual(myelement.tagName, 'H3') 79 | assert.strictEqual(myelement.textContent, '456') 80 | setTimeout(() => { 81 | sinks.dispose() 82 | done() 83 | }) 84 | }) 85 | }) 86 | 87 | it('should render a VTree with a child Observable', function (done) { 88 | function app () { 89 | const child$ = Observable.of( 90 | h4('.child', {}, 'I am a kid') 91 | ).delay(80) 92 | return { 93 | DOM: Observable.of(div('.my-class', [ 94 | p({}, 'Ordinary paragraph'), 95 | child$ 96 | ])) 97 | } 98 | } 99 | 100 | const {sinks, sources} = Cycle.run(app, { 101 | DOM: makeDOMDriver(createRenderTarget(), {transposition: true}) 102 | }) 103 | 104 | sources.DOM.select(':root').elements.skip(1).take(1).subscribe(function (root) { 105 | const selectEl = root.querySelector('.child') 106 | assert.notStrictEqual(selectEl, null) 107 | assert.notStrictEqual(typeof selectEl, 'undefined') 108 | assert.strictEqual(selectEl.tagName, 'H4') 109 | assert.strictEqual(selectEl.textContent, 'I am a kid') 110 | setTimeout(() => { 111 | sinks.dispose() 112 | done() 113 | }) 114 | }) 115 | }) 116 | 117 | it('should render a VTree with a grandchild Observable', function (done) { 118 | function app () { 119 | const grandchild$ = Observable.of( 120 | h4('.grandchild', {}, [ 121 | 'I am a baby' 122 | ]) 123 | ).delay(20) 124 | const child$ = Observable.of( 125 | h3('.child', {}, [ 126 | 'I am a kid', 127 | grandchild$ 128 | ]) 129 | ).delay(80) 130 | return { 131 | DOM: Observable.of(div('.my-class', [ 132 | p({}, 'Ordinary paragraph'), 133 | child$ 134 | ])) 135 | } 136 | } 137 | 138 | const {sinks, sources} = Cycle.run(app, { 139 | DOM: makeDOMDriver(createRenderTarget(), {transposition: true}) 140 | }) 141 | 142 | sources.DOM.select(':root').elements.skip(1).take(1).subscribe(function (root) { 143 | const selectEl = root.querySelector('.grandchild') 144 | assert.notStrictEqual(selectEl, null) 145 | assert.notStrictEqual(typeof selectEl, 'undefined') 146 | assert.strictEqual(selectEl.tagName, 'H4') 147 | assert.strictEqual(selectEl.textContent, 'I am a baby') 148 | setTimeout(() => { 149 | sinks.dispose() 150 | done() 151 | }) 152 | }) 153 | }) 154 | 155 | it('should render a SVG VTree with a child Observable', function (done) { 156 | function app () { 157 | const child$ = Observable.of( 158 | svg.g({ 159 | attrs: {'class': 'child'} 160 | }) 161 | ).delay(80) 162 | return { 163 | DOM: Observable.of(svg([ 164 | svg.g(), 165 | child$ 166 | ])) 167 | } 168 | } 169 | 170 | const {sinks, sources} = Cycle.run(app, { 171 | DOM: makeDOMDriver(createRenderTarget(), {transposition: true}) 172 | }) 173 | 174 | sources.DOM.select(':root').elements.skip(1).take(1).subscribe(function (root) { 175 | const selectEl = root.querySelector('.child') 176 | assert.notStrictEqual(selectEl, null) 177 | assert.notStrictEqual(typeof selectEl, 'undefined') 178 | assert.strictEqual(selectEl.tagName, 'g') 179 | setTimeout(() => { 180 | sinks.dispose() 181 | done() 182 | }) 183 | }) 184 | }) 185 | 186 | it('should only be concerned with values from the most recent nested Observable', function (done) { 187 | function app () { 188 | return { 189 | DOM: Observable.of( 190 | div([ 191 | Observable.of(1).concat(Observable.of(2).delay(5)).map(outer => 192 | Observable.of(1).concat(Observable.of(2).delay(10)).map(inner => 193 | div('.target', outer + '/' + inner) 194 | ) 195 | ) 196 | ]) 197 | ) 198 | } 199 | } 200 | 201 | const {sources} = Cycle.run(app, { 202 | DOM: makeDOMDriver(createRenderTarget(), {transposition: true}) 203 | }) 204 | 205 | let expected = ['1/1', '2/1', '2/2'] 206 | 207 | sources.DOM.select('.target').elements 208 | .skip(1) 209 | .take(3) 210 | .map(els => els[0].innerHTML) 211 | .subscribe((x) => { 212 | assert.strictEqual(x, expected.shift()) 213 | }, done, () => done()) 214 | }) 215 | }) 216 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v3.0.0 (2016-06-04) 2 | 3 | 4 | ## Features 5 | 6 | - **rewrite:** rewrite to @cycle/dom@10.0.0-rc28 7 | ([6d19df1f](https://github.com/TylorS/cycle-snabbdom/commits/6d19df1fc86bc67d8b289922f23574217a3951c5)) 8 | 9 | 10 | # v2.0.0 (2016-05-17) 11 | 12 | 13 | # v1.4.0 (2016-03-30) 14 | 15 | 16 | ## Bug Fixes 17 | 18 | - dataset module has not yet been publised to npm 19 | ([07e4b47c](https://github.com/motorcyclejs/dom/commits/07e4b47cc999013c7a6f5cb0a2448e5a396ded6f)) 20 | - **issue-89:** hopefully help fix fiddly test 21 | ([2eb6afbf](https://github.com/motorcyclejs/dom/commits/2eb6afbf1c149339c720499502d3b343d3371ee9)) 22 | 23 | 24 | ## Features 25 | 26 | - **mockDOMSource:** update to allow for multiple .select()s 27 | ([9a47a30f](https://github.com/motorcyclejs/dom/commits/9a47a30ffb01eeb49af912a23b94780bb6ed876e)) 28 | - **modules:** remove local version of modules in favor of fixed snabbdom versions 29 | ([c1864b22](https://github.com/motorcyclejs/dom/commits/c1864b22ff7737a8fd0af9373f4ff5a8a111c903)) 30 | 31 | 32 | # v1.3.0 (2016-03-15) 33 | 34 | 35 | ## Features 36 | 37 | - add new event types that don't bubble 38 | ([e62092e3](https://github.com/motorcyclejs/dom/commits/e62092e3e9598f62ed6ff986224e5e112834b9cd)) 39 | - **makeDOMDriver:** add option to specify your own error handling function 40 | ([80717f8b](https://github.com/motorcyclejs/dom/commits/80717f8bf903cde9f170bf0a4b373abfe6e6478f)) 41 | 42 | 43 | # v1.2.1 (2016-02-23) 44 | 45 | 46 | ## Bug Fixes 47 | 48 | - **select:** adjust select() semantics to match more css selectors properly 49 | ([362cab6c](https://github.com/motorcyclejs/dom/commits/362cab6ca6f255e6ae693fabbd12fcf9becc7a0d)) 50 | 51 | 52 | # v1.2.0 (2016-02-19) 53 | 54 | 55 | ## Bug Fixes 56 | 57 | - fix all failing tests of new test suite 58 | ([7107cb8f](https://github.com/motorcyclejs/dom/commits/7107cb8f19109f000ff428a5a1acbe3ddfd59a01)) 59 | 60 | 61 | # v1.1.0 (2016-02-07) 62 | 63 | 64 | ## Features 65 | 66 | - update event-delegation model 67 | ([2543bea4](https://github.com/motorcyclejs/dom/commits/2543bea4dff32165b3211064c6cf53e5b340eb5d), 68 | [#68]([object Object]/68)) 69 | - **events:** use @most/dom-event instead of local fromEvent 70 | ([daec57db](https://github.com/motorcyclejs/dom/commits/daec57db5f3561eccc5cf569117fa59f855939b5), 71 | [#69]([object Object]/69)) 72 | 73 | 74 | # v1.0.3 (2015-12-30) 75 | 76 | 77 | # v1.0.2 (2015-12-30) 78 | 79 | 80 | ## Bug Fixes 81 | 82 | - polyfill raf for snabbom 83 | ([eb17a5db](https://github.com/motorcyclejs/dom/commits/eb17a5dbd07573f5c0ad849518c6c0588396a4dd)) 84 | 85 | 86 | # v1.0.1 (2015-12-30) 87 | 88 | 89 | # v1.0.0 (2015-12-30) 90 | 91 | 92 | ## Bug Fixes 93 | 94 | - fix makeDomDriver import 95 | ([1f6347c4](https://github.com/motorcyclejs/dom/commits/1f6347c4e0a98fb2f9c11bbd3a1a167b0c4ffae6)) 96 | - remove unneeded test 97 | ([aef055dd](https://github.com/motorcyclejs/dom/commits/aef055ddc7217db30f1ab8675fa3110e39975689)) 98 | - rename `sink.type` to `sink.event` 99 | ([34d9705e](https://github.com/motorcyclejs/dom/commits/34d9705e7a85684c830223d5f0fe4d5e82b425ea)) 100 | - **events:** use standard event.target 101 | ([5c8b2313](https://github.com/motorcyclejs/dom/commits/5c8b231356389a11f002bef08f17e2026d60cf78)) 102 | - **isolate:** update isolation semantics 103 | ([08b69f0f](https://github.com/motorcyclejs/dom/commits/08b69f0f7ff174709fbe71f7acc6adc24cc7031d)) 104 | - **select:** fix isolateSource and isolateSink 105 | ([06bb35d2](https://github.com/motorcyclejs/dom/commits/06bb35d21a6808af0dbceb433057844282891ca7)) 106 | - **test:** 107 | - fix usage errors 108 | ([45372050](https://github.com/motorcyclejs/dom/commits/453720500f0de1854364a3b70bac209be9efe7b6)) 109 | - remove unused sinon import 110 | ([7a349332](https://github.com/motorcyclejs/dom/commits/7a3493327fc9d3a5f88036207b165f9188bf3b7f)) 111 | - **thunks:** check for data.vnode 112 | ([21e5f572](https://github.com/motorcyclejs/dom/commits/21e5f5726182994f7bfb403dad423779f6ee6d93)) 113 | - **vTreeParser:** ignore previous child observable's value 114 | ([b788e889](https://github.com/motorcyclejs/dom/commits/b788e88913b76d4dff810cbcfbd2115f5d816dfd), 115 | [#46]([object Object]/46)) 116 | 117 | 118 | ## Features 119 | 120 | - **dom-driver:** reuse event listeners 121 | ([1a939735](https://github.com/motorcyclejs/dom/commits/1a9397357672d81fa7b295b3e2cf072ea9a534f8)) 122 | - **events:** 123 | - avoid recreating the same eventListener 124 | ([56cad782](https://github.com/motorcyclejs/dom/commits/56cad782233ca839f7a07bb6418efef73afcc6e9)) 125 | - Switch to event delegation 126 | ([4c9ff0ff](https://github.com/motorcyclejs/dom/commits/4c9ff0ffdb32f688a1f74eab11c8267826d3b153)) 127 | - **fromEvent:** handle single DOM Nodes 128 | ([a8bd6fa4](https://github.com/motorcyclejs/dom/commits/a8bd6fa4faa79b5345f9a47882574f05d07d9bc9)) 129 | - **isolate:** add multicast 130 | ([db6c6f49](https://github.com/motorcyclejs/dom/commits/db6c6f49db39f27fc2fc041afed90fc76f82f830)) 131 | - **makeDOMDriver:** 132 | - throw error if modules is not an array 133 | ([11f2e35b](https://github.com/motorcyclejs/dom/commits/11f2e35bcbcdb0102fef28faeea4cae18d0004ae)) 134 | - switch to options object 135 | ([33fc153f](https://github.com/motorcyclejs/dom/commits/33fc153faac90552b7e56ea3407d7278cd9000dd), 136 | [#57]([object Object]/57)) 137 | - pass a stream of the rootElem to makeElementSelector 138 | ([17cb9d94](https://github.com/motorcyclejs/dom/commits/17cb9d943e1762ec56281af9c5127986c75a7519)) 139 | - **select:** 140 | - use event delegation 141 | ([770541ed](https://github.com/motorcyclejs/dom/commits/770541ed9a2c8085c003727bae62692cd635fad3)) 142 | - rewrite DOM.select with snabbdom-selector 143 | ([8b231e41](https://github.com/motorcyclejs/dom/commits/8b231e4136a5a426d4741288c0a49bdd8d64a4bb)) 144 | - **thunk:** export thunk by default 145 | ([2e43834c](https://github.com/motorcyclejs/dom/commits/2e43834cfb0f20608de27d200a6f485872d5eb56)) 146 | - **vTreeParser:** Add support for a static vTree option 147 | ([89e2ba1c](https://github.com/motorcyclejs/dom/commits/89e2ba1cf059a48e6c3984b5de71f48a9e5bbfb9), 148 | [#59]([object Object]/59)) 149 | - **wrapVnode:** wrap top-evel vnode 150 | ([dbbca443](https://github.com/motorcyclejs/dom/commits/dbbca4435f0fd2867b5d5ea01e76b6d4e9894cbf), 151 | [#8]([object Object]/8)) 152 | 153 | 154 | ## Breaking Changes 155 | 156 | - due to [b30c209a](https://github.com/motorcyclejs/dom/commits/b30c209aef43e8fcc01e267990663034d571f69d), 157 | 158 | 159 | before: 160 | import {makeDomDriver} from '@motorcycle/dom' 161 | 162 | after: 163 | import {makeDOMDriver} from '@motorcyce/core' 164 | 165 | - **select:** due to [8b231e41](https://github.com/motorcyclejs/dom/commits/8b231e4136a5a426d4741288c0a49bdd8d64a4bb), 166 | 167 | Before: 168 | DOM.select(selector) used document.querySelector() under the hood 169 | for ease of use and for it's substanstially more robust css selector 170 | engine. 171 | 172 | After: 173 | DOM.selector(selector) now uses snabbdom-selector to match css selectors 174 | from the virtual DOM tree for the speed of avoiding the baggage of the DOM. 175 | 176 | References #4 177 | 178 | - **wrapVnode:** due to [dbbca443](https://github.com/motorcyclejs/dom/commits/dbbca4435f0fd2867b5d5ea01e76b6d4e9894cbf), 179 | 180 | 181 | Before: 182 | Patching: h('h1', {}, 'Hello') 183 | to:
184 | rendered:

Hello

185 | 186 | After: 187 | Patching: h('h1', {}, 'Hello') 188 | to:
189 | renders: