├── .babelrc ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── example ├── app.js ├── index.html ├── main.js ├── package.json └── rollup.config.js ├── northbrook.json ├── package.json ├── perf └── flow-dom │ ├── .babelrc │ ├── dist │ ├── index.html │ └── main.js │ ├── package.json │ ├── rollup.config.js │ └── src │ └── index.js └── src ├── hyperscript ├── VNode.js └── h.js ├── index.js ├── interfaces.js ├── module └── properties.js └── util.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-flow-strip-types", 4 | "transform-object-rest-spread", 5 | ["module-resolver", { 6 | }] 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "parser": "babel-eslint", 4 | "plugins": [ 5 | "flowtype", 6 | "import", 7 | "immutable" 8 | ], 9 | "rules": { 10 | "import/default": 2, 11 | "import/export": 2, 12 | "import/extensions": [2, { 13 | "js": "never", 14 | "json": "always" 15 | }], 16 | "import/imports-first": 2, 17 | "import/no-amd": 2, 18 | "import/no-deprecated": 1, 19 | "import/no-duplicates": 2, 20 | "import/no-extraneous-dependencies": [2, {"devDependencies": false, "optionalDependencies": false, "peerDependencies": false}], 21 | "import/no-mutable-exports": 2, 22 | "import/no-named-as-default-member": 2, 23 | "import/no-named-as-default": 2, 24 | "import/no-unresolved": 2, 25 | "flowtype/define-flow-type": 1, 26 | "flowtype/require-parameter-type": [1, { excludeArrowFunctions: true }], 27 | "flowtype/require-return-type": [ 28 | 1, 29 | "always", 30 | { 31 | "annotateUndefined": "never" 32 | } 33 | ], 34 | "flowtype/space-after-type-colon": [ 35 | 2, 36 | "always" 37 | ], 38 | "flowtype/space-before-type-colon": [ 39 | 2, 40 | "never" 41 | ], 42 | "flowtype/use-flow-type": 1, 43 | "flowtype/valid-syntax": 2, 44 | "immutable/no-let": 2, 45 | }, 46 | "settings": { 47 | "flowtype": { 48 | "onlyFilesWithFlowAnnotation": true 49 | }, 50 | "import/resolver": { 51 | "babel-module": { 52 | 53 | } 54 | }, 55 | "import/ignore": [ 56 | 'node_modules', 57 | '.(scss|sass|css)$' 58 | ] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | node_modules/ 3 | lib/ 4 | perf/ 5 | example/ 6 | 7 | [include] 8 | src/ 9 | 10 | [libs] 11 | 12 | [options] 13 | unsafe.enable_getters_and_setters=true 14 | suppress_comment=\\(.\\|\n\\)*\\$flow-ignore-line 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TylorS/flow-dom/10116b69b8ff58b369d1c88c7b588537f6b210e1/.npmignore -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.1.3 (2016-08-29) 2 | --- 3 | 4 | 5 | ## Bug Fixes 6 | 7 | - fix(flow-dom): fix all the things and make some perf test and an exampley [65206e9c](https://github.com/TylorS/stream-flow/commits/65206e9c03f51849adeb0f9bf1537b3a28ab4c90) 8 | 9 | 10 | # v0.1.2 (2016-08-28) 11 | --- 12 | 13 | 14 | ## Bug Fixes 15 | 16 | - fix(flow-dom): remove @most/prelude [7ffad2be](https://github.com/TylorS/stream-flow/commits/7ffad2beeff648ca03e7e1e8e331cf8b90546841) 17 | 18 | 19 | # v0.1.1 (2016-08-28) 20 | --- 21 | 22 | 23 | ## Bug Fixes 24 | 25 | - fix(flow-dom): add main field to package [01a46f26](https://github.com/TylorS/stream-flow/commits/01a46f26e93db12c476b0cb9308bf3f3d28c924b) 26 | 27 | 28 | # v0.1.0 (2016-08-28) 29 | --- 30 | 31 | 32 | ## Features 33 | 34 | - feat(flow-dom): add insert hook to types [4aff342d](https://github.com/TylorS/stream-flow/commits/4aff342d850635750aa016507753b67d22eb43df) 35 | 36 | ## Bug Fixes 37 | 38 | - fix(flow-dom): export h [2fd72665](https://github.com/TylorS/stream-flow/commits/2fd72665f0fd18894b7a57d219c8c3e2100c2344) 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 Tylor Steinberger 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flow DOM 2 | 3 | > An experiment to create an immutable, mutation-free, virtual-dom 4 | 5 | I succeed ;P 6 | 7 | Docs are a maybe lol 8 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Example app 4 | 5 | 6 |
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | const { init, h } = require('../src/index') 2 | const { fromEvent } = require('most') 3 | 4 | const patch = init([]) 5 | 6 | function view (clicks) { 7 | return h('div.flow', {}, [ 8 | h('button.btn', {}, 'Click me'), 9 | h('h1', {}, 'Clicked ' + String(clicks) + ' times!') 10 | ]) 11 | } 12 | 13 | const click$ = fromEvent('click', document.body) 14 | .tap((x) => console.log(x)) 15 | .filter(ev => ev.target.matches('.btn')) 16 | 17 | const state$ = click$.scan((x) => x + 1, 0).tap(x => console.log(x)) 18 | 19 | const view$ = state$.map(view) 20 | 21 | view$.scan(patch, document.querySelector('#app')).drain() 22 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "most.js", 6 | "scripts": { 7 | "build": "rm -rf app.js && rollup -c", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@most/dom-event": "^1.3.0", 14 | "most": "^1.0.1" 15 | }, 16 | "devDependencies": { 17 | "babel-plugin-object-rest-spread": "0.0.0", 18 | "babel-plugin-transform-flow-strip-types": "^6.14.0", 19 | "babel-preset-es2015": "^6.14.0", 20 | "rollup": "^0.34.10", 21 | "rollup-plugin-babel": "^2.6.1", 22 | "rollup-plugin-buble": "^0.13.0", 23 | "rollup-plugin-commonjs": "^3.3.1", 24 | "rollup-plugin-node-resolve": "^2.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import buble from 'rollup-plugin-buble' 3 | import babel from 'rollup-plugin-babel' 4 | import nodeResolve from 'rollup-plugin-node-resolve' 5 | import commonjs from 'rollup-plugin-commonjs' 6 | 7 | export default { 8 | entry: 'main.js', 9 | dest: 'app.js', 10 | format: 'iife', 11 | moduleName: 'FlowDom', 12 | plugins: [ 13 | babel(), 14 | buble(), 15 | nodeResolve({ 16 | jsnext: false 17 | }), 18 | commonjs() 19 | ], 20 | acorn: { 21 | allowReserved: true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /northbrook.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["."], 3 | "plugins": ["eslint"] 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flow-dom", 3 | "version": "0.1.3", 4 | "description": "An Immutable Virtual DOM", 5 | "main": "lib/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/TylorS/stream-flow.git" 9 | }, 10 | "author": "Tylor Steinberger (https://github.com/TylorS)", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/TylorS/stream-flow/issues" 14 | }, 15 | "homepage": "https://github.com/TylorS/stream-flow#readme", 16 | "config": { 17 | "ghooks": { 18 | "commit-msg": "node ./node_modules/.bin/validate-commit-msg" 19 | }, 20 | "validate-commit-msg": { 21 | "types": "@northbrook/commit-types" 22 | } 23 | }, 24 | "scripts": { 25 | "lint": "northbrook eslint", 26 | "build": "buba -i src -o lib", 27 | "commit": "northbrook commit", 28 | "preversion": "npm run build", 29 | "release": "northbrook release" 30 | }, 31 | "devDependencies": { 32 | "@northbrook/commit-types": "^1.1.0", 33 | "@northbrook/eslint": "^1.1.1", 34 | "babel-eslint": "^6.1.2", 35 | "babel-plugin-module-resolver": "^2.1.1", 36 | "babel-plugin-transform-flow-strip-types": "^6.14.0", 37 | "babel-plugin-transform-object-rest-spread": "^6.8.0", 38 | "babel-preset-es2015": "^6.14.0", 39 | "buba": "^2.0.3", 40 | "eslint": "^3.4.0", 41 | "eslint-import-resolver-babel-module": "^2.0.1", 42 | "eslint-plugin-flowtype": "^2.11.4", 43 | "eslint-plugin-immutable": "^1.0.0", 44 | "eslint-plugin-import": "^1.14.0", 45 | "flow-bin": "^0.31.1", 46 | "northbrook": "^2.2.6" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /perf/flow-dom/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-flow-strip-types", 4 | "transform-object-rest-spread", 5 | ["module-resolver", { 6 | }] 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /perf/flow-dom/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Virtual DOM Benchmark: Flow-DOM 7 | 8 | 9 |

Virtual DOM Benchmark: Flow-DOM

10 |

Source code

11 |
12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /perf/flow-dom/dist/main.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | function interopDefault(ex) { 5 | return ex && typeof ex === 'object' && 'default' in ex ? ex['default'] : ex; 6 | } 7 | 8 | function createCommonjsModule(fn, module) { 9 | return module = { exports: {} }, fn(module, module.exports), module.exports; 10 | } 11 | 12 | var executor = createCommonjsModule(function (module) { 13 | 'use strict'; 14 | 15 | function render(nodes) { 16 | var children = []; 17 | var j; 18 | var c; 19 | var i; 20 | var e; 21 | var n; 22 | 23 | for (i = 0; i < nodes.length; i++) { 24 | n = nodes[i]; 25 | if (n.children !== null) { 26 | e = document.createElement('div'); 27 | c = render(n.children); 28 | for (j = 0; j < c.length; j++) { 29 | e.appendChild(c[j]); 30 | } 31 | children.push(e); 32 | } else { 33 | e = document.createElement('span'); 34 | e.textContent = n.key.toString(); 35 | children.push(e); 36 | } 37 | } 38 | 39 | return children; 40 | } 41 | 42 | function testInnerHtml(testName, nodes, container) { 43 | var c = document.createElement('div'); 44 | var e = document.createElement('div'); 45 | var children = render(nodes); 46 | for (var i = 0; i < children.length; i++) { 47 | e.appendChild(children[i]); 48 | } 49 | c.appendChild(e); 50 | if (c.innerHTML !== container.innerHTML) { 51 | console.log('error in test: ' + testName); 52 | console.log('container.innerHTML:'); 53 | console.log(container.innerHTML); 54 | console.log('should be:'); 55 | console.log(c.innerHTML); 56 | } 57 | } 58 | 59 | function Executor(impl, container, tests, iterations, cb, iterCb, enableTests) { 60 | if (iterCb === void 0) iterCb = null; 61 | 62 | this.impl = impl; 63 | this.container = container; 64 | this.tests = tests; 65 | this.iterations = iterations; 66 | this.cb = cb; 67 | this.iterCb = iterCb; 68 | this.enableTests = enableTests; 69 | 70 | this._currentTest = 0; 71 | this._currentIter = 0; 72 | this._renderSamples = []; 73 | this._updateSamples = []; 74 | this._result = []; 75 | 76 | this._tasksCount = tests.length * iterations; 77 | 78 | this._iter = this.iter.bind(this); 79 | } 80 | 81 | Executor.prototype.start = function () { 82 | this._iter(); 83 | }; 84 | 85 | Executor.prototype.finished = function () { 86 | this.cb(this._result); 87 | }; 88 | 89 | Executor.prototype.progress = function () { 90 | if (this._currentTest === 0 && this._currentIter === 0) { 91 | return 0; 92 | } 93 | 94 | var tests = this.tests; 95 | return (this._currentTest * tests.length + this._currentIter) / (tests.length * this.iterataions); 96 | }; 97 | 98 | Executor.prototype.iter = function () { 99 | if (this.iterCb != null) { 100 | this.iterCb(this); 101 | } 102 | 103 | var tests = this.tests; 104 | 105 | if (this._currentTest < tests.length) { 106 | var test = tests[this._currentTest]; 107 | 108 | if (this._currentIter < this.iterations) { 109 | var e, t; 110 | var renderTime, updateTime; 111 | 112 | e = new this.impl(this.container, test.data.a, test.data.b); 113 | e.setUp(); 114 | 115 | t = window.performance.now(); 116 | e.render(); 117 | renderTime = window.performance.now() - t; 118 | 119 | if (this.enableTests) { 120 | testInnerHtml(test.name + 'render()', test.data.a, this.container); 121 | } 122 | 123 | t = window.performance.now(); 124 | e.update(); 125 | updateTime = window.performance.now() - t; 126 | 127 | if (this.enableTests) { 128 | testInnerHtml(test.name + 'update()', test.data.b, this.container); 129 | } 130 | 131 | e.tearDown(); 132 | 133 | this._renderSamples.push(renderTime); 134 | this._updateSamples.push(updateTime); 135 | 136 | this._currentIter++; 137 | } else { 138 | this._result.push({ 139 | name: test.name + ' ' + 'render()', 140 | data: this._renderSamples.slice(0) 141 | }); 142 | 143 | this._result.push({ 144 | name: test.name + ' ' + 'update()', 145 | data: this._updateSamples.slice(0) 146 | }); 147 | 148 | this._currentTest++; 149 | 150 | this._currentIter = 0; 151 | this._renderSamples = []; 152 | this._updateSamples = []; 153 | } 154 | 155 | setTimeout(this._iter, 0); 156 | } else { 157 | this.finished(); 158 | } 159 | }; 160 | 161 | module.exports = Executor; 162 | }); 163 | 164 | var executor$1 = interopDefault(executor); 165 | 166 | 167 | var require$$0$1 = Object.freeze({ 168 | default: executor$1 169 | }); 170 | 171 | var benchmark$1 = createCommonjsModule(function (module) { 172 | 'use strict'; 173 | 174 | var Executor = interopDefault(require$$0$1); 175 | 176 | function Benchmark() { 177 | this.running = false; 178 | this.impl = null; 179 | this.tests = null; 180 | this.reportCallback = null; 181 | this.enableTests = false; 182 | 183 | this.container = document.createElement('div'); 184 | 185 | this._runButton = document.getElementById('RunButton'); 186 | this._iterationsElement = document.getElementById('Iterations'); 187 | this._reportElement = document.createElement('pre'); 188 | 189 | document.body.appendChild(this.container); 190 | document.body.appendChild(this._reportElement); 191 | 192 | var self = this; 193 | 194 | this._runButton.addEventListener('click', function (e) { 195 | e.preventDefault(); 196 | 197 | if (!self.running) { 198 | var iterations = parseInt(self._iterationsElement.value); 199 | if (iterations <= 0) { 200 | iterations = 10; 201 | } 202 | 203 | self.run(iterations); 204 | } 205 | }, false); 206 | 207 | this.ready(true); 208 | } 209 | 210 | Benchmark.prototype.ready = function (v) { 211 | if (v) { 212 | this._runButton.disabled = ''; 213 | } else { 214 | this._runButton.disabled = 'true'; 215 | } 216 | }; 217 | 218 | Benchmark.prototype.run = function (iterations) { 219 | var self = this; 220 | this.running = true; 221 | this.ready(false); 222 | 223 | new Executor(self.impl, self.container, self.tests, 1, function () { 224 | // warmup 225 | new Executor(self.impl, self.container, self.tests, iterations, function (samples) { 226 | self._reportElement.textContent = JSON.stringify(samples, null, ' '); 227 | self.running = false; 228 | self.ready(true); 229 | if (self.reportCallback != null) { 230 | self.reportCallback(samples); 231 | } 232 | }, undefined, false).start(); 233 | }, undefined, this.enableTests).start(); 234 | }; 235 | 236 | module.exports = Benchmark; 237 | }); 238 | 239 | var benchmark$2 = interopDefault(benchmark$1); 240 | 241 | 242 | var require$$0 = Object.freeze({ 243 | default: benchmark$2 244 | }); 245 | 246 | var index = createCommonjsModule(function (module) { 247 | 'use strict'; 248 | 249 | var Benchmark = interopDefault(require$$0); 250 | var benchmark = new Benchmark(); 251 | 252 | function initFromScript(scriptUrl, impl) { 253 | var e = document.createElement('script'); 254 | e.src = scriptUrl; 255 | 256 | e.onload = function () { 257 | benchmark.tests = window.generateBenchmarkData().units; 258 | benchmark.ready(true); 259 | }; 260 | 261 | document.head.appendChild(e); 262 | } 263 | 264 | function initFromParentWindow(parent, name, version, id) { 265 | window.addEventListener('message', function (e) { 266 | var data = e.data; 267 | var type = data.type; 268 | 269 | if (type === 'tests') { 270 | benchmark.tests = data.data; 271 | benchmark.reportCallback = function (samples) { 272 | parent.postMessage({ 273 | type: 'report', 274 | data: { 275 | name: name, 276 | version: version, 277 | samples: samples 278 | }, 279 | id: id 280 | }, '*'); 281 | }; 282 | benchmark.ready(true); 283 | 284 | parent.postMessage({ 285 | type: 'ready', 286 | data: null, 287 | id: id 288 | }, '*'); 289 | } else if (type === 'run') { 290 | benchmark.run(data.data.iterations); 291 | } 292 | }, false); 293 | 294 | parent.postMessage({ 295 | type: 'init', 296 | data: null, 297 | id: id 298 | }, '*'); 299 | } 300 | 301 | function init(name, version, impl) { 302 | // Parse Query String. 303 | var qs = function (a) { 304 | if (a == "") return {}; 305 | var b = {}; 306 | for (var i = 0; i < a.length; ++i) { 307 | var p = a[i].split('=', 2); 308 | if (p.length == 1) { 309 | b[p[0]] = ""; 310 | } else { 311 | b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " ")); 312 | } 313 | } 314 | return b; 315 | }(window.location.search.substr(1).split('&')); 316 | 317 | if (qs['name'] !== void 0) { 318 | name = qs['name']; 319 | } 320 | 321 | if (qs['version'] !== void 0) { 322 | version = qs['version']; 323 | } 324 | 325 | var type = qs['type']; 326 | 327 | if (qs['test'] !== void 0) { 328 | benchmark.enableTests = true; 329 | console.log('tests enabled'); 330 | } 331 | 332 | var id; 333 | if (type === 'iframe') { 334 | id = qs['id']; 335 | if (id === void 0) id = null; 336 | initFromParentWindow(window.parent, name, version, id); 337 | } else if (type === 'window') { 338 | if (window.opener != null) { 339 | id = qs['id']; 340 | if (id === void 0) id = null; 341 | initFromParentWindow(window.opener, name, version, id); 342 | } else { 343 | console.log('Failed to initialize: opener window is NULL'); 344 | } 345 | } else { 346 | var testsUrl = qs['data']; // url to the script generating test data 347 | if (testsUrl !== void 0) { 348 | initFromScript(testsUrl); 349 | } else { 350 | console.log('Failed to initialize: cannot load tests data'); 351 | } 352 | } 353 | 354 | benchmark.impl = impl; 355 | } 356 | 357 | // performance.now() polyfill 358 | // https://gist.github.com/paulirish/5438650 359 | // prepare base perf object 360 | if (typeof window.performance === 'undefined') { 361 | window.performance = {}; 362 | } 363 | if (!window.performance.now) { 364 | var nowOffset = Date.now(); 365 | if (performance.timing && performance.timing.navigationStart) { 366 | nowOffset = performance.timing.navigationStart; 367 | } 368 | window.performance.now = function now() { 369 | return Date.now() - nowOffset; 370 | }; 371 | } 372 | 373 | module.exports = init; 374 | }); 375 | 376 | var benchmark = interopDefault(index); 377 | 378 | var FlowVNode = function FlowVNode(tagName, id, classList, data, children, text, element, key) { 379 | this._tagName = tagName; 380 | this._id = id; 381 | this._classList = classList; 382 | this._data = data; 383 | this._children = children; 384 | this._text = text; 385 | this._element = element; 386 | this._key = key; 387 | }; 388 | 389 | var prototypeAccessors = { tagName: {},id: {},classList: {},selector: {},data: {},children: {},text: {},element: {},key: {} }; 390 | 391 | prototypeAccessors.tagName.get = function () { 392 | return this._tagName; 393 | }; 394 | 395 | prototypeAccessors.id.get = function () { 396 | return this._id; 397 | }; 398 | 399 | prototypeAccessors.classList.get = function () { 400 | return this._classList; 401 | }; 402 | 403 | prototypeAccessors.selector.get = function () { 404 | return "" + (this._tagName) + (this._id && ("#" + (this._id)) || '') + "" + (this._classList && this._classList.length > 0 ? '.' + this._classList.sort().join('') : ''); 405 | }; 406 | 407 | prototypeAccessors.data.get = function () { 408 | return this._data; 409 | }; 410 | 411 | prototypeAccessors.children.get = function () { 412 | return this._children; 413 | }; 414 | 415 | prototypeAccessors.text.get = function () { 416 | return this._text; 417 | }; 418 | 419 | prototypeAccessors.element.get = function () { 420 | return this._element; 421 | }; 422 | 423 | prototypeAccessors.key.get = function () { 424 | return this._key; 425 | }; 426 | 427 | FlowVNode.prototype.setTagName = function setTagName (tagName) { 428 | return new FlowVNode(tagName, this._id, this._classList, this._data, this._children, this._text, this._element, this._key); 429 | }; 430 | 431 | FlowVNode.prototype.setId = function setId (id) { 432 | return new FlowVNode(this._tagName, id, this._classList, this._data, this._children, this._text, this._element, this._key); 433 | }; 434 | 435 | FlowVNode.prototype.setClassList = function setClassList (classList) { 436 | return new FlowVNode(this._tagName, this._id, classList, this._data, this._children, this._text, this._element, this._key); 437 | }; 438 | 439 | FlowVNode.prototype.setData = function setData (data) { 440 | return new FlowVNode(this._tagName, this._id, this._classList, data, this._children, this._text, this._element, this._key); 441 | }; 442 | 443 | FlowVNode.prototype.setChildren = function setChildren (children) { 444 | return new FlowVNode(this._tagName, this._id, this._classList, this._data, children, this._text, this._element, this._key); 445 | }; 446 | 447 | FlowVNode.prototype.setText = function setText (text) { 448 | return new FlowVNode(this._tagName, this._id, this._classList, this._data, this._children, text, this._element, this._key); 449 | }; 450 | 451 | FlowVNode.prototype.setElement = function setElement (element) { 452 | return new FlowVNode(this._tagName, this._id, this._classList, this._data, this._children, this._text, element, this._key); 453 | }; 454 | 455 | FlowVNode.prototype.setKey = function setKey (key) { 456 | return new FlowVNode(this._tagName, this._id, this._classList, this._data, this._children, this._text, this._element, key); 457 | }; 458 | 459 | Object.defineProperties( FlowVNode.prototype, prototypeAccessors ); 460 | 461 | // eslint-disable-line 462 | 463 | function isUndef(x) { 464 | return x === void 0; 465 | } 466 | 467 | function emptyVNodeAt(node) { 468 | return new FlowVNode(node.tagName.toLowerCase(), node.id || '', copy(node.classList), {}, [], '', node, null); 469 | } 470 | 471 | function sameVNode(a, b) { 472 | return a.selector === b.selector && a.key === b.key; 473 | } 474 | 475 | function forEach(f, arr) { 476 | var length = arr.length; 477 | for (var i = 0; i < length; ++i) { 478 | // eslint-disable-line immutable/no-let 479 | f(arr[i], i); 480 | } 481 | } 482 | 483 | // copy :: [a] -> [a] 484 | // duplicate a (shallow duplication) 485 | function copy(a) { 486 | var l = a.length; 487 | var b = new Array(l); 488 | for (var i = 0; i < l; ++i) { 489 | // eslint-disable-line immutable/no-let 490 | b[i] = a[i]; 491 | } 492 | return b; 493 | } 494 | 495 | // map :: (a -> b) -> [a] -> [b] 496 | // transform each element with f 497 | function map$1(f, a) { 498 | var l = a.length; 499 | var b = new Array(l); 500 | for (var i = 0; i < l; ++i) { 501 | // eslint-disable-line immutable/no-let 502 | b[i] = f(a[i], i); 503 | } 504 | return b; 505 | } 506 | 507 | // reduce :: (a -> b -> a) -> a -> [b] -> a 508 | // accumulate via left-fold 509 | function reduce(f, z, a) { 510 | var r = z; // eslint-disable-line immutable/no-let 511 | for (var i = 0, l = a.length; i < l; ++i) { 512 | // eslint-disable-line immutable/no-let 513 | r = f(r, a[i], i); 514 | } 515 | return r; 516 | } 517 | 518 | // replace :: a -> Int -> [a] 519 | // replace element at index 520 | function replace(x, i, a) { 521 | if (i < 0) { 522 | throw new TypeError('i must be >= 0'); 523 | } 524 | 525 | var l = a.length; 526 | var b = new Array(l); 527 | for (var j = 0; j < l; ++j) { 528 | // eslint-disable-line immutable/no-let 529 | b[j] = i === j ? x : a[j]; 530 | } 531 | return b; 532 | } 533 | 534 | // eslint-disable-line 535 | 536 | var assign = function (x, y) { return Object.assign({}, x, y); }; 537 | 538 | function setSVGNamespace(vNode) { 539 | var newVNode = vNode.setData(assign(vNode.data, { ns: 'http://www.w3.org/2000/svg' })); 540 | if (newVNode.children.length > 0 && newVNode.tagName !== 'foreignObject') { 541 | return newVNode.setChildren(addNS(newVNode.children)); 542 | } 543 | return newVNode; 544 | } 545 | 546 | function addNS(children) { 547 | return map$1(setSVGNamespace, children); 548 | } 549 | 550 | function convertText(children) { 551 | return map$1(function (child) { 552 | return typeof child === 'string' ? new FlowVNode('', '', [], {}, [], child, null, null) : child; 553 | }, children); 554 | } 555 | 556 | function h(selector, data, childrenOrText) { 557 | var ref = parseSelector(selector); 558 | var tagName = ref.tagName; 559 | var id = ref.id; 560 | var classList = ref.classList; 561 | 562 | var text = typeof childrenOrText === 'string' ? childrenOrText : ''; 563 | 564 | var children = Array.isArray(childrenOrText) ? childrenOrText : []; 565 | 566 | return new FlowVNode(tagName, id, classList, data, tagName === 'svg' ? addNS(children) : convertText(children), text, null, data && data.key || null); 567 | } 568 | 569 | var classIdSplit = /([\.#]?[a-zA-Z0-9\u007F-\uFFFF_:-]+)/; 570 | var notClassId = /^\.|#/; 571 | 572 | function parseSelector(selector) { 573 | var tagParts = selector.split(classIdSplit); 574 | 575 | if (selector === '') { 576 | return { 577 | tagName: 'div', 578 | id: '', 579 | classList: [] 580 | }; 581 | } 582 | 583 | var seed = notClassId.test(tagParts[1]) ? { tagName: 'div', id: '', classList: [] } : { tagName: '', id: '', classList: [] }; 584 | 585 | return reduce(function (output, part) { 586 | if (!part) return output; 587 | 588 | var type = part.charAt(0); 589 | 590 | if (!output.tagName) { 591 | output.tagName = part.trim(); 592 | } else if (type === '.') { 593 | output.classList.push(part.substring(1, part.length).trim()); 594 | } else if (type === '#') { 595 | output.id = part.substring(1, part.length).trim(); 596 | } 597 | 598 | return output; 599 | }, seed, tagParts); 600 | } 601 | 602 | var _extends = Object.assign || function (target) { 603 | var arguments$1 = arguments; 604 | for (var i = 1; i < arguments.length; i++) { var source = arguments$1[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 605 | 606 | // eslint-disable-line 607 | var emptyVNode = new FlowVNode('', '', [], {}, [], '', null, null); 608 | 609 | var id = function (x, y) { return y; }; 610 | 611 | function setTextContent(node, text) { 612 | node.textContent = text; 613 | } 614 | 615 | function insertBefore(parent, node, before) { 616 | parent.insertBefore(node, before); 617 | } 618 | 619 | function removeChild(parent, child) { 620 | parent.removeChild(child); 621 | } 622 | 623 | function nextSibling(element) { 624 | return element.nextSibling; 625 | } 626 | 627 | var appendChild = function (parent) { return function (vNode) { 628 | parent.appendChild(vNode.element); 629 | }; }; 630 | 631 | function createKeyToIndex(children, beginIdx, endIdx) { 632 | return reduce(function (map, child, i) { 633 | if (child.key) { 634 | map[child.key] = i; 635 | } 636 | return map; 637 | }, {}, children); 638 | } 639 | 640 | function callInsert(vNode) { 641 | var data = vNode.data; 642 | var children = vNode.children; 643 | 644 | var insertedChildren = children.length > 0 ? map$1(callInsert, children) : children; 645 | 646 | if (data.insert) { 647 | return data.insert(vNode); 648 | } 649 | 650 | return vNode.setChildren(insertedChildren); 651 | } 652 | 653 | function init(modules) { 654 | if ( modules === void 0 ) modules = []; 655 | 656 | // calls all update hooks 657 | function callUpdateHooks(oldVNode, vNode) { 658 | var ref = reduce(function (ref, module) { 659 | var oldVNode = ref[0]; 660 | var vNode = ref[1]; 661 | 662 | return [vNode, module.update(oldVNode, vNode)]; 663 | }, [oldVNode, vNode], modules); 664 | var updatedOldVNode = ref[0]; 665 | var updatedVNode = ref[1]; 666 | 667 | var update = !isUndef(updatedVNode.data) && updatedVNode.data.update || id; 668 | 669 | return update(updatedOldVNode, updatedVNode); 670 | } 671 | 672 | // calls all create hooks 673 | function callCreateHooks(vNode) { 674 | var updatedVNode = reduce(function (vNode, module) { 675 | return module.create(emptyVNode, vNode); 676 | }, vNode, modules); 677 | 678 | var create = vNode.data.insert || id; 679 | 680 | return create(emptyVNode, updatedVNode); 681 | } 682 | 683 | // recursively calls destroy hook on a VNode and all it's children 684 | function callDestroyHooks(vNode) { 685 | var data = vNode.data; 686 | var children = vNode.children; 687 | if (data.destroy) data.destroy(vNode); 688 | 689 | forEach(function (module) { 690 | module.destroy(vNode); 691 | }, modules); 692 | 693 | if (children.length > 0) { 694 | forEach(callDestroyHooks, children); 695 | } 696 | } 697 | 698 | // calls remove hooks 699 | function callRemoveHooks(vNode, remove) { 700 | var data = vNode.data; 701 | 702 | forEach(function (module) { 703 | module.remove(vNode, remove); 704 | }, modules); 705 | 706 | if (data.remove) { 707 | data.remove(vNode, remove); 708 | } else { 709 | remove(); 710 | } 711 | } 712 | 713 | // create a new VNode which contains an element 714 | function createElement(vNode) { 715 | var tagName = vNode.tagName; 716 | var id = vNode.id; 717 | var classList = vNode.classList; 718 | var VNodeChildren = vNode.children; 719 | var text = vNode.text; 720 | var data = vNode.data; 721 | 722 | var element = data.ns ? document.createElementNS(data.ns, tagName) : document.createElement(tagName); 723 | 724 | if (id) element.id = id; 725 | if (classList.length > 0) element.className = classList.join(' '); 726 | 727 | var children = map$1(createElement, VNodeChildren); 728 | 729 | if (children.length > 0) { 730 | forEach(appendChild(element), children); 731 | } else if (text && text.length > 0) { 732 | setTextContent(element, text); 733 | } 734 | 735 | var updatedVNode = vNode.setChildren(children).setElement(element); 736 | 737 | return callCreateHooks(updatedVNode); 738 | } 739 | 740 | function createRemoveCallback(childElement, listeners) { 741 | return function () { 742 | if (--listeners === 0) { 743 | var parent = childElement.parentNode; 744 | removeChild(parent, childElement); 745 | } 746 | }; 747 | } 748 | 749 | function removeVNodes(parent, vNodes, startIndex, endIndex) { 750 | for (; startIndex < endIndex; ++startIndex) { 751 | var listeners = modules.length + 1; 752 | var child = vNodes[startIndex]; 753 | if (child) { 754 | if (child.tagName !== '') { 755 | callDestroyHooks(child); 756 | var rm = createRemoveCallback(child.element, listeners); 757 | callRemoveHooks(child, rm); 758 | } else { 759 | // Text Node 760 | removeChild(parent, child.element); 761 | } 762 | } 763 | } 764 | } 765 | 766 | function addVNodes(parent, vNode, before, startIndex, endIndex) { 767 | var children = vNode.children; 768 | 769 | if (startIndex >= children.length) { 770 | return vNode; 771 | } 772 | 773 | var childrenWithElements = map$1(function (child, index) { 774 | if (index >= startIndex && index <= endIndex) { 775 | insertBefore(parent, child.element, before); 776 | return callInsert(child); 777 | } 778 | return child; 779 | }, map$1(createElement, children)); 780 | 781 | return vNode.setChildren(childrenWithElements); 782 | } 783 | 784 | // update children when they have changed 785 | function updateChildren(ref) { 786 | var parent = ref.parent; 787 | var oldChildren = ref.oldChildren; 788 | var vNode = ref.vNode; 789 | var oldKeyToIndex = ref.oldKeyToIndex; 790 | var oldStartIndex = ref.oldStartIndex; 791 | var oldEndIndex = ref.oldEndIndex; 792 | var newStartIndex = ref.newStartIndex; 793 | var newEndIndex = ref.newEndIndex; 794 | var oldStartVNode = ref.oldStartVNode; 795 | var oldEndVNode = ref.oldEndVNode; 796 | var newStartVNode = ref.newStartVNode; 797 | var newEndVNode = ref.newEndVNode; 798 | 799 | var previousInput = arguments[0]; 800 | var children = vNode.children; 801 | 802 | if (oldStartIndex > oldEndIndex) { 803 | var before = isUndef(children[newEndIndex + 1]) ? null : children[newEndIndex + 1].element; 804 | 805 | return addVNodes(parent, vNode, before, newStartIndex, newEndIndex); 806 | } 807 | 808 | if (newStartIndex > newEndIndex) { 809 | removeVNodes(parent, oldChildren, oldStartIndex, oldEndIndex); 810 | return vNode; 811 | } 812 | 813 | // VNode has moved left in child array 814 | if (isUndef(oldStartVNode)) { 815 | var _oldStartIndex = oldStartIndex + 1; 816 | return updateChildren(_extends({}, previousInput, { 817 | oldStartVNode: oldChildren[_oldStartIndex], 818 | oldStartIndex: _oldStartIndex 819 | })); 820 | } 821 | 822 | if (isUndef(oldEndVNode)) { 823 | var _oldEndIndex = oldEndIndex - 1; 824 | return updateChildren(_extends({}, previousInput, { 825 | oldEndVNode: oldChildren[_oldEndIndex], 826 | oldEndIndex: _oldEndIndex 827 | })); 828 | } 829 | 830 | if (sameVNode(oldStartVNode, newStartVNode)) { 831 | var _updatedVNode = patchVNode(oldStartVNode, newStartVNode); 832 | var newChildren$1 = replace(_updatedVNode, newStartIndex, children); 833 | 834 | var _oldStartIndex$1 = oldStartIndex + 1; 835 | var _newStartIndex$1 = newStartIndex + 1; 836 | 837 | return updateChildren(_extends({}, previousInput, { 838 | vNode: vNode.setChildren(newChildren$1), 839 | oldStartIndex: _oldStartIndex$1, 840 | newStartIndex: _newStartIndex$1, 841 | oldStartVNode: oldChildren[_oldStartIndex$1], 842 | newStartVNode: children[_newStartIndex$1] 843 | })); 844 | } 845 | 846 | if (sameVNode(oldEndVNode, newEndVNode)) { 847 | var updatedVNode$1 = patchVNode(oldEndVNode, newEndVNode); 848 | var newChildren$2 = replace(updatedVNode$1, newEndIndex, children); 849 | 850 | var _oldEndIndex$1 = oldEndIndex - 1; 851 | var _newEndIndex = newEndIndex - 1; 852 | 853 | return updateChildren(_extends({}, previousInput, { 854 | vNode: vNode.setChildren(newChildren$2), 855 | oldEndIndex: _oldEndIndex$1, 856 | newEndIndex: _newEndIndex, 857 | oldEndVNode: oldChildren[_oldEndIndex$1], 858 | newEndVNode: children[_newEndIndex] 859 | })); 860 | } 861 | 862 | // vNode has moved right in the array 863 | if (sameVNode(oldStartVNode, newEndVNode)) { 864 | var updatedVNode$2 = patchVNode(oldStartVNode, newEndVNode); 865 | var newChildren$3 = replace(updatedVNode$2, newEndIndex, children); 866 | 867 | insertBefore(parent, oldStartVNode.element, nextSibling(oldEndVNode.element)); 868 | 869 | var _oldStartIndex$2 = oldStartIndex + 1; 870 | var _newEndIndex$1 = newEndIndex - 1; 871 | 872 | return updateChildren(_extends({}, previousInput, { 873 | vNode: vNode.setChildren(newChildren$3), 874 | oldStartIndex: _oldStartIndex$2, 875 | newEndIndex: _newEndIndex$1, 876 | oldStartVNode: oldChildren[_oldStartIndex$2], 877 | newEndVNode: children[_newEndIndex$1] 878 | })); 879 | } 880 | 881 | // vNode moved left 882 | if (sameVNode(oldEndVNode, newStartVNode)) { 883 | var updatedVNode$3 = patchVNode(oldEndVNode, newStartVNode); 884 | var newChildren$4 = replace(updatedVNode$3, newStartIndex, children); 885 | 886 | insertBefore(parent, oldEndVNode.element, oldStartVNode.element); 887 | 888 | var _oldEndIndex$2 = oldEndIndex - 1; 889 | var _newStartIndex$2 = newStartIndex + 1; 890 | 891 | return updateChildren(_extends({}, previousInput, { 892 | vNode: vNode.setChildren(newChildren$4), 893 | oldEndIndex: _oldEndIndex$2, 894 | newStartIndex: _newStartIndex$2, 895 | oldEndVNode: oldChildren[_oldEndIndex$2], 896 | newStartVNode: children[_newStartIndex$2] 897 | })); 898 | } 899 | 900 | var _oldKeyToIndex = oldKeyToIndex === null ? createKeyToIndex(oldChildren, oldStartIndex, oldEndIndex) : oldKeyToIndex; 901 | 902 | var indexInOld = _oldKeyToIndex[newStartVNode.key]; 903 | 904 | if (isUndef(indexInOld)) { 905 | // new element 906 | var _vNode$1 = createElement(newStartVNode); 907 | insertBefore(parent, _vNode$1.element, oldStartVNode.element); 908 | 909 | var _newStartIndex$3 = newStartIndex + 1; 910 | var _newStartVNode$1 = children[_newStartIndex$3]; 911 | 912 | return updateChildren(_extends({}, previousInput, { 913 | oldKeyToIndex: _oldKeyToIndex, 914 | vNode: callInsert(_vNode$1), 915 | newStartIndex: _newStartIndex$3, 916 | newStartVNode: _newStartVNode$1 917 | })); 918 | } 919 | 920 | var elementToMove = oldChildren[indexInOld]; 921 | var updatedVNode = patchVNode(elementToMove, newStartVNode); 922 | var newChildren = replace(updatedVNode, newStartIndex, children); 923 | var _vNode = vNode.setChildren(newChildren); 924 | // $flow-ignore-line 925 | oldChildren[indexInOld] = undefined; 926 | insertBefore(parent, elementToMove.element, oldStartVNode.element); 927 | var _newStartIndex = newStartIndex + 1; 928 | var _newStartVNode = children[_newStartIndex]; 929 | return updateChildren(_extends({}, previousInput, { 930 | oldKeyToIndex: _oldKeyToIndex, 931 | vNode: _vNode, 932 | newStartIndex: _newStartIndex, 933 | newStartVNode: _newStartVNode 934 | })); 935 | } 936 | 937 | // updates the DOM and VNode with the current information it should have 938 | function patchVNode(oldVNode, _vNode) { 939 | // $flow-ignore-line 940 | var vNode = _vNode.setElement(oldVNode.element); 941 | // if the previous and current are equal do nothing 942 | if (oldVNode === vNode) return vNode; 943 | 944 | // if the vNode has changed drastically create a new one an replace the old 945 | if (!sameVNode(oldVNode, vNode)) { 946 | var parent = oldVNode.element && oldVNode.element.parentNode; 947 | var newVNode = createElement(vNode); 948 | insertBefore(parent, newVNode.element, oldVNode.element); 949 | var updatedVNode$1 = callInsert(newVNode); 950 | if (parent !== null && parent !== undefined) { 951 | removeVNodes(parent, [oldVNode], 0, 0); 952 | } 953 | return updatedVNode$1; 954 | } 955 | 956 | var updatedVNode = callUpdateHooks(oldVNode, vNode); 957 | 958 | var element = updatedVNode.element; 959 | var children = updatedVNode.children; 960 | 961 | // lets update the DOM 962 | if (updatedVNode.text === '') { 963 | if (oldVNode.children.length > 0 && children.length > 0) { 964 | // children have changed somehow 965 | if (oldVNode.children === updatedVNode.children) return updatedVNode; 966 | 967 | var oldEndIndex = oldVNode.children.length - 1; 968 | var newEndIndex = children.length - 1; 969 | 970 | var oldStartVNode = oldVNode.children[0]; 971 | var oldEndVNode = oldVNode.children[oldEndIndex]; 972 | var newStartVNode = children[0]; 973 | var newEndVNode = children[newEndIndex]; 974 | 975 | var input = { 976 | parent: oldVNode.element, 977 | oldChildren: oldVNode.children, 978 | vNode: updatedVNode, 979 | oldKeyToIndex: null, 980 | oldStartIndex: 0, 981 | oldEndIndex: oldEndIndex, 982 | newStartIndex: 0, 983 | newEndIndex: newEndIndex, 984 | oldStartVNode: oldStartVNode, 985 | oldEndVNode: oldEndVNode, 986 | newStartVNode: newStartVNode, 987 | newEndVNode: newEndVNode 988 | }; 989 | 990 | return updateChildren(input); 991 | } else if (children.length > 0) { 992 | // children have been added when there were none 993 | if (oldVNode.text !== '') setTextContent(element, ''); 994 | return addVNodes(element, updatedVNode, null, 0, updatedVNode.children.length - 1); 995 | } else if (oldVNode.children.length > 0) { 996 | // children have been completely removed 997 | if (updatedVNode.element) { 998 | removeVNodes(updatedVNode.element, oldVNode.children, 0, oldVNode.children.length - 1); 999 | } 1000 | return updatedVNode; 1001 | } else if (oldVNode.text !== '') { 1002 | // text has been removed 1003 | setTextContent(updatedVNode.element, ''); 1004 | return updatedVNode; 1005 | } 1006 | } else if (oldVNode.text !== updatedVNode.text) { 1007 | // update text if needed 1008 | setTextContent(element, updatedVNode.text); 1009 | } 1010 | 1011 | return updatedVNode; 1012 | } 1013 | 1014 | return function patch(oldVNode, vNode) { 1015 | if (oldVNode === vNode) return vNode; 1016 | 1017 | var previousVNode = oldVNode instanceof HTMLElement ? emptyVNodeAt(oldVNode) : oldVNode; 1018 | 1019 | if (sameVNode(previousVNode, vNode)) { 1020 | return patchVNode(previousVNode, vNode); 1021 | } 1022 | 1023 | var element = previousVNode.element; 1024 | var parent = element && element.parentNode; 1025 | 1026 | var newVNode = createElement(vNode); 1027 | 1028 | if (parent) { 1029 | insertBefore(parent, newVNode.element, element && element.nextSibling); 1030 | return callInsert(newVNode); 1031 | } 1032 | 1033 | return newVNode; 1034 | }; 1035 | } 1036 | 1037 | var patch = init([]); 1038 | 1039 | var NAME = 'flow-dom'; 1040 | var VERSION = '1.0.0'; 1041 | 1042 | function map(arr, f) { 1043 | var l = arr.length; 1044 | var x = new Array(l); 1045 | for (var i = 0; i < l; ++i) { 1046 | // eslint-disable-line immutable/no-let 1047 | x[i] = f(arr[i]); 1048 | } 1049 | return x; 1050 | } 1051 | 1052 | function convertToVnodes(nodes) { 1053 | return map(nodes, function (n) { 1054 | if (n.children !== null) { 1055 | return h('div', { key: n.key }, convertToVnodes(n.children)); 1056 | } 1057 | return h('div', { key: n.key }, n.key); 1058 | }); 1059 | } 1060 | 1061 | function BenchmarkImpl(container, a, b) { 1062 | this.container = container; 1063 | this.a = a; 1064 | this.b = b; 1065 | this.vnode = null; 1066 | } 1067 | 1068 | BenchmarkImpl.prototype.setUp = function () {}; 1069 | 1070 | BenchmarkImpl.prototype.tearDown = function () { 1071 | patch(this.vnode, h('div')); 1072 | }; 1073 | 1074 | BenchmarkImpl.prototype.render = function () { 1075 | var elm = document.createElement('div'); 1076 | this.vnode = patch(elm, h('div', convertToVnodes(this.a))); 1077 | this.container.appendChild(elm); 1078 | }; 1079 | 1080 | BenchmarkImpl.prototype.update = function () { 1081 | this.vnode = patch(this.vnode, h('div', convertToVnodes(this.b))); 1082 | }; 1083 | 1084 | document.addEventListener('DOMContentLoaded', function (e) { 1085 | benchmark(NAME, VERSION, BenchmarkImpl); 1086 | }, false); 1087 | 1088 | }()); -------------------------------------------------------------------------------- /perf/flow-dom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vdom-benchmark-flow-dom", 3 | "version": "1.0.0", 4 | "description": "Virtual DOM Benchmark: Flow-DOM", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "rollup -c", 8 | "serve": "pushstate-server dist/ 3000 /index.html", 9 | "start": "npm run build && npm run serve", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "Tylor Steinberger ", 13 | "license": "MIT", 14 | "dependencies": { 15 | "vdom-benchmark-base": "^0.2.4" 16 | }, 17 | "devDependencies": { 18 | "babel-plugin-module-resolver": "^2.2.0", 19 | "babel-plugin-transform-flow-strip-types": "^6.14.0", 20 | "babel-plugin-transform-object-rest-spread": "^6.8.0", 21 | "pushstate-server": "^1.11.0", 22 | "rollup": "^0.34.10", 23 | "rollup-plugin-babel": "^2.6.1", 24 | "rollup-plugin-buble": "^0.13.0", 25 | "rollup-plugin-commonjs": "^3.3.1", 26 | "rollup-plugin-node-resolve": "^2.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /perf/flow-dom/rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import buble from 'rollup-plugin-buble' 3 | import babel from 'rollup-plugin-babel' 4 | import nodeResolve from 'rollup-plugin-node-resolve' 5 | import commonjs from 'rollup-plugin-commonjs' 6 | 7 | export default { 8 | entry: 'src/index.js', 9 | dest: 'dist/main.js', 10 | format: 'iife', 11 | moduleName: 'VDOMBenchmarkFlowDom', 12 | plugins: [ 13 | babel(), 14 | buble(), 15 | nodeResolve({ 16 | jsnext: true 17 | }), 18 | commonjs() 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /perf/flow-dom/src/index.js: -------------------------------------------------------------------------------- 1 | import benchmark from 'vdom-benchmark-base' 2 | import { init } from '../../../src/index' 3 | import { h } from '../../../src/hyperscript/h' 4 | const patch = init([]) 5 | 6 | var NAME = 'flow-dom' 7 | var VERSION = '1.0.0' 8 | 9 | function map (arr, f) { 10 | const l = arr.length 11 | const x = new Array(l) 12 | for (let i = 0; i < l; ++i) { // eslint-disable-line immutable/no-let 13 | x[i] = f(arr[i]) 14 | } 15 | return x 16 | } 17 | 18 | function convertToVnodes (nodes) { 19 | return map(nodes, function (n) { 20 | if (n.children !== null) { 21 | return h('div', {key: n.key}, convertToVnodes(n.children)) 22 | } 23 | return h('div', {key: n.key}, n.key) 24 | }) 25 | } 26 | 27 | function BenchmarkImpl (container, a, b) { 28 | this.container = container 29 | this.a = a 30 | this.b = b 31 | this.vnode = null 32 | } 33 | 34 | BenchmarkImpl.prototype.setUp = function () { 35 | } 36 | 37 | BenchmarkImpl.prototype.tearDown = function () { 38 | patch(this.vnode, h('div')) 39 | } 40 | 41 | BenchmarkImpl.prototype.render = function () { 42 | const elm = document.createElement('div') 43 | this.vnode = patch(elm, h('div', convertToVnodes(this.a))) 44 | this.container.appendChild(elm) 45 | } 46 | 47 | BenchmarkImpl.prototype.update = function () { 48 | this.vnode = patch(this.vnode, h('div', convertToVnodes(this.b))) 49 | } 50 | 51 | document.addEventListener('DOMContentLoaded', function (e) { 52 | benchmark(NAME, VERSION, BenchmarkImpl) 53 | }, false) 54 | -------------------------------------------------------------------------------- /src/hyperscript/VNode.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export type VNode = { 4 | tagName: string, 5 | id: string, 6 | classList: string[], 7 | selector: string, 8 | data: Object, 9 | children: Array, 10 | text: string | null, 11 | element: Element | Text | null, 12 | key: string | number | null, 13 | 14 | setTagName(tagName: string): VNode, 15 | setId(id: string): VNode, 16 | setClassList(classList: string[]): VNode, 17 | setData(data: Object): VNode, 18 | setChildren(children: Array): VNode, 19 | setText(text: string): VNode, 20 | setElement(element: Element | Text): VNode, 21 | setKey(key: string | number): VNode 22 | } 23 | 24 | export class FlowVNode { 25 | _tagName: string 26 | _id: string 27 | _classList: string[] 28 | _data: Object 29 | _children: Array 30 | _text: string 31 | _element: Element | Text | null 32 | _key: string | number | null 33 | 34 | constructor (tagName: string, id: string, classList: string[], data: Object, children: Array, 35 | text: string, element: Element | Text | null, key: string | number | null) { 36 | this._tagName = tagName 37 | this._id = id 38 | this._classList = classList 39 | this._data = data 40 | this._children = children 41 | this._text = text 42 | this._element = element 43 | this._key = key 44 | } 45 | 46 | get tagName (): string { 47 | return this._tagName 48 | } 49 | 50 | get id (): string { 51 | return this._id 52 | } 53 | 54 | get classList (): string[] { 55 | return this._classList 56 | } 57 | 58 | get selector (): string { 59 | return `${this._tagName}${this._id && `#${this._id}` || ''}` + 60 | `${this._classList && this._classList.length > 0 ? '.' + this._classList.sort().join('') : ''}` 61 | } 62 | 63 | get data (): Object { 64 | return this._data 65 | } 66 | 67 | get children (): Array { 68 | return this._children 69 | } 70 | 71 | get text (): string | null { 72 | return this._text 73 | } 74 | 75 | get element (): Element | Text | null { 76 | return this._element 77 | } 78 | 79 | get key (): string | number | null { 80 | return this._key 81 | } 82 | 83 | setTagName (tagName: string): VNode { 84 | return new FlowVNode(tagName, this._id, this._classList, this._data, 85 | this._children, this._text, this._element, this._key) 86 | } 87 | 88 | setId (id: string): VNode { 89 | return new FlowVNode(this._tagName, id, this._classList, this._data, 90 | this._children, this._text, this._element, this._key) 91 | } 92 | 93 | setClassList (classList: Array): VNode { 94 | return new FlowVNode(this._tagName, this._id, classList, this._data, 95 | this._children, this._text, this._element, this._key) 96 | } 97 | 98 | setData (data: Object): VNode { 99 | return new FlowVNode(this._tagName, this._id, this._classList, data, 100 | this._children, this._text, this._element, this._key) 101 | } 102 | 103 | setChildren (children: Array): VNode { 104 | return new FlowVNode(this._tagName, this._id, this._classList, this._data, 105 | children, this._text, this._element, this._key) 106 | } 107 | 108 | setText (text: string): VNode { 109 | return new FlowVNode(this._tagName, this._id, this._classList, this._data, 110 | this._children, text, this._element, this._key) 111 | } 112 | 113 | setElement (element: Element | Text): VNode { 114 | return new FlowVNode(this._tagName, this._id, this._classList, this._data, 115 | this._children, this._text, element, this._key) 116 | } 117 | 118 | setKey (key: string | number): VNode { 119 | return new FlowVNode(this._tagName, this._id, this._classList, this._data, 120 | this._children, this._text, this._element, key) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/hyperscript/h.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { VNode } from './VNode' 3 | import { map, reduce } from '../util' 4 | import { FlowVNode } from './VNode' // eslint-disable-line 5 | 6 | const assign = (x: Object, y: Object): Object => Object.assign({}, x, y) 7 | 8 | function setSVGNamespace (vNode: VNode): VNode { 9 | const newVNode = vNode.setData(assign(vNode.data, { ns: 'http://www.w3.org/2000/svg' })) 10 | if (newVNode.children.length > 0 && newVNode.tagName !== 'foreignObject') { 11 | return newVNode.setChildren(addNS(newVNode.children)) 12 | } 13 | return newVNode 14 | } 15 | 16 | function addNS (children: any[]): VNode[] { 17 | return map(setSVGNamespace, children) 18 | } 19 | 20 | function convertText (children: any[]): VNode[] { 21 | return map(function (child: VNode | string): VNode { 22 | return typeof child === 'string' 23 | ? new FlowVNode('', '', [], {}, [], child, null, null) 24 | : (child: VNode) 25 | }, children) 26 | } 27 | 28 | export function h (selector: string, data: Object, childrenOrText: string | Array): VNode { 29 | const { tagName, id, classList } = parseSelector(selector) 30 | 31 | const text = typeof childrenOrText === 'string' 32 | ? childrenOrText 33 | : '' 34 | 35 | const children = Array.isArray(childrenOrText) 36 | ? childrenOrText 37 | : [] 38 | 39 | return new FlowVNode(tagName, id, classList, data, 40 | tagName === 'svg' ? addNS(children) : convertText(children), 41 | text, null, data && data.key || null) 42 | } 43 | 44 | const classIdSplit = /([\.#]?[a-zA-Z0-9\u007F-\uFFFF_:-]+)/ 45 | const notClassId = /^\.|#/ 46 | 47 | type selectorOuput = { tagName: string, id: string, classList: string[] } 48 | 49 | function parseSelector (selector: string): selectorOuput { 50 | const tagParts = selector.split(classIdSplit) 51 | 52 | if (selector === '') { 53 | return { 54 | tagName: 'div', 55 | id: '', 56 | classList: [] 57 | } 58 | } 59 | 60 | const seed = notClassId.test(tagParts[1]) 61 | ? { tagName: 'div', id: '', classList: [] } 62 | : { tagName: '', id: '', classList: [] } 63 | 64 | return reduce(function (output: selectorOuput, part: string): selectorOuput { 65 | if (!part) return output 66 | 67 | const type = part.charAt(0) 68 | 69 | if (!output.tagName) { 70 | output.tagName = part.trim() 71 | } else if (type === '.') { 72 | output.classList.push(part.substring(1, part.length).trim()) 73 | } else if (type === '#') { 74 | output.id = part.substring(1, part.length).trim() 75 | } 76 | 77 | return output 78 | }, seed, tagParts) 79 | } 80 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { Module } from './interfaces' 3 | import type { VNode } from './hyperscript/VNode' 4 | import { FlowVNode } from './hyperscript/VNode' // eslint-disable-line 5 | import { isUndef, emptyVNodeAt, sameVNode, forEach, reduce, map, replace } from './util' 6 | 7 | export { h } from './hyperscript/h' 8 | 9 | const emptyVNode = new FlowVNode('', '', [], {}, [], '', null, null) 10 | 11 | const id = (x, y): VNode => y 12 | 13 | function setTextContent (node: any, text: any) { 14 | node.textContent = text 15 | } 16 | 17 | function insertBefore (parent: any, node: any, before: any) { 18 | parent.insertBefore(node, before) 19 | } 20 | 21 | function removeChild (parent: any, child: any) { 22 | parent.removeChild(child) 23 | } 24 | 25 | function nextSibling (element: any): Element { 26 | return element.nextSibling 27 | } 28 | 29 | const appendChild = (parent: any): Function => (vNode: any) => { 30 | parent.appendChild(vNode.element) 31 | } 32 | 33 | function createKeyToIndex (children: VNode[], beginIdx: number, endIdx: number): Object { 34 | return reduce(function (map: Object, child: VNode, i: number): Object { 35 | if (child.key) { 36 | map[child.key] = i 37 | } 38 | return map 39 | }, {}, children) 40 | } 41 | 42 | function callInsert (vNode: VNode): VNode { 43 | const { data, children } = vNode 44 | 45 | const insertedChildren = children.length > 0 46 | ? map(callInsert, children) 47 | : children 48 | 49 | if (data.insert) { 50 | return data.insert(vNode) 51 | } 52 | 53 | return vNode.setChildren(insertedChildren) 54 | } 55 | 56 | export function init (modules: Module[] = []): (oldVNode: VNode | HTMLElement, vNode: VNode) => VNode { 57 | // calls all update hooks 58 | function callUpdateHooks (oldVNode: VNode, vNode: VNode): VNode { 59 | const [updatedOldVNode, updatedVNode] = reduce(function ([oldVNode, vNode]: [VNode, VNode], module: Module): [VNode, VNode] { 60 | return [vNode, module.update(oldVNode, vNode)] 61 | }, [oldVNode, vNode], modules) 62 | 63 | const update = !isUndef(updatedVNode.data) && updatedVNode.data.update || id 64 | 65 | return update(updatedOldVNode, updatedVNode) 66 | } 67 | 68 | // calls all create hooks 69 | function callCreateHooks (vNode: VNode): VNode { 70 | const updatedVNode = reduce(function (vNode: VNode, module: Module): VNode { 71 | return module.create(emptyVNode, vNode) 72 | }, vNode, modules) 73 | 74 | const create = vNode.data.insert || id 75 | 76 | return create(emptyVNode, updatedVNode) 77 | } 78 | 79 | // recursively calls destroy hook on a VNode and all it's children 80 | function callDestroyHooks (vNode: VNode) { 81 | const { data, children } = vNode 82 | if (data.destroy) data.destroy(vNode) 83 | 84 | forEach(function (module: Module) { 85 | module.destroy(vNode) 86 | }, modules) 87 | 88 | if (children.length > 0) { 89 | forEach(callDestroyHooks, children) 90 | } 91 | } 92 | 93 | // calls remove hooks 94 | function callRemoveHooks (vNode: VNode, remove: Function) { 95 | const { data } = vNode 96 | 97 | forEach(function (module: Module) { 98 | module.remove(vNode, remove) 99 | }, modules) 100 | 101 | if (data.remove) { 102 | data.remove(vNode, remove) 103 | } else { 104 | remove() 105 | } 106 | } 107 | 108 | // create a new VNode which contains an element 109 | function createElement (vNode: VNode): VNode { 110 | const { tagName, id, classList, children: VNodeChildren, text, data } = vNode 111 | 112 | const element = data.ns 113 | ? document.createElementNS(data.ns, tagName) 114 | : document.createElement(tagName) 115 | 116 | if (id) element.id = id 117 | if (classList.length > 0) element.className = classList.join(' ') 118 | 119 | const children = map(createElement, VNodeChildren) 120 | 121 | if (children.length > 0) { 122 | forEach(appendChild(element), children) 123 | } else if (text && text.length > 0) { 124 | setTextContent(element, text) 125 | } 126 | 127 | const updatedVNode = vNode.setChildren(children).setElement(element) 128 | 129 | return callCreateHooks(updatedVNode) 130 | } 131 | 132 | function createRemoveCallback (childElement: any, listeners: number): Function { 133 | return function () { 134 | if (--listeners === 0) { 135 | const parent = childElement.parentNode 136 | removeChild(parent, childElement) 137 | } 138 | } 139 | } 140 | 141 | function removeVNodes (parent: Node, vNodes: VNode[], startIndex: number, endIndex: number) { 142 | for (; startIndex < endIndex; ++startIndex) { 143 | const listeners = modules.length + 1 144 | const child = vNodes[startIndex] 145 | if (child) { 146 | if (child.tagName !== '') { 147 | callDestroyHooks(child) 148 | const rm = createRemoveCallback(child.element, listeners) 149 | callRemoveHooks(child, rm) 150 | } else { // Text Node 151 | removeChild(parent, child.element) 152 | } 153 | } 154 | } 155 | } 156 | 157 | function addVNodes (parent: any, vNode: VNode, before: any, startIndex: number, endIndex: number): VNode { 158 | const { children } = vNode 159 | 160 | if (startIndex >= children.length) { 161 | return vNode 162 | } 163 | 164 | const childrenWithElements = map(function (child: VNode, index: number): VNode { 165 | if (index >= startIndex && index <= endIndex) { 166 | insertBefore(parent, child.element, before) 167 | return callInsert(child) 168 | } 169 | return child 170 | }, map(createElement, children)) 171 | 172 | return vNode.setChildren(childrenWithElements) 173 | } 174 | 175 | type UpdateChildren = { 176 | parent: any, 177 | // $flow-ignore-line 178 | oldChildren: Array, 179 | vNode: VNode, 180 | oldKeyToIndex: Object | null, 181 | oldStartIndex: number, 182 | oldEndIndex: number, 183 | newStartIndex: number, 184 | newEndIndex: number, 185 | oldStartVNode: VNode, 186 | oldEndVNode: VNode, 187 | newStartVNode: VNode, 188 | newEndVNode: VNode 189 | } 190 | 191 | // update children when they have changed 192 | function updateChildren ({parent, oldChildren, vNode, oldKeyToIndex, 193 | oldStartIndex, oldEndIndex, newStartIndex, newEndIndex, 194 | oldStartVNode, oldEndVNode, newStartVNode, newEndVNode }: UpdateChildren): VNode { 195 | const previousInput = arguments[0] 196 | const { children } = vNode 197 | 198 | if (oldStartIndex > oldEndIndex) { 199 | const before = isUndef(children[newEndIndex + 1]) ? null : children[newEndIndex + 1].element 200 | 201 | return addVNodes(parent, vNode, before, newStartIndex, newEndIndex) 202 | } 203 | 204 | if (newStartIndex > newEndIndex) { 205 | removeVNodes(parent, oldChildren, oldStartIndex, oldEndIndex) 206 | return vNode 207 | } 208 | 209 | // VNode has moved left in child array 210 | if (isUndef(oldStartVNode)) { 211 | const _oldStartIndex = oldStartIndex + 1 212 | return updateChildren({ ...previousInput, 213 | oldStartVNode: oldChildren[_oldStartIndex], 214 | oldStartIndex: _oldStartIndex 215 | }) 216 | } 217 | 218 | if (isUndef(oldEndVNode)) { 219 | const _oldEndIndex = oldEndIndex - 1 220 | return updateChildren({...previousInput, 221 | oldEndVNode: oldChildren[_oldEndIndex], 222 | oldEndIndex: _oldEndIndex 223 | }) 224 | } 225 | 226 | if (sameVNode(oldStartVNode, newStartVNode)) { 227 | const _updatedVNode = patchVNode(oldStartVNode, newStartVNode) 228 | const newChildren = replace(_updatedVNode, newStartIndex, children) 229 | 230 | const _oldStartIndex = oldStartIndex + 1 231 | const _newStartIndex = newStartIndex + 1 232 | 233 | return updateChildren({...previousInput, 234 | vNode: vNode.setChildren(newChildren), 235 | oldStartIndex: _oldStartIndex, 236 | newStartIndex: _newStartIndex, 237 | oldStartVNode: oldChildren[_oldStartIndex], 238 | newStartVNode: children[_newStartIndex] 239 | }) 240 | } 241 | 242 | if (sameVNode(oldEndVNode, newEndVNode)) { 243 | const updatedVNode = patchVNode(oldEndVNode, newEndVNode) 244 | const newChildren = replace(updatedVNode, newEndIndex, children) 245 | 246 | const _oldEndIndex = oldEndIndex - 1 247 | const _newEndIndex = newEndIndex - 1 248 | 249 | return updateChildren({...previousInput, 250 | vNode: vNode.setChildren(newChildren), 251 | oldEndIndex: _oldEndIndex, 252 | newEndIndex: _newEndIndex, 253 | oldEndVNode: oldChildren[_oldEndIndex], 254 | newEndVNode: children[_newEndIndex] 255 | }) 256 | } 257 | 258 | // vNode has moved right in the array 259 | if (sameVNode(oldStartVNode, newEndVNode)) { 260 | const updatedVNode = patchVNode(oldStartVNode, newEndVNode) 261 | const newChildren = replace(updatedVNode, newEndIndex, children) 262 | 263 | insertBefore(parent, oldStartVNode.element, nextSibling(oldEndVNode.element)) 264 | 265 | const _oldStartIndex = oldStartIndex + 1 266 | const _newEndIndex = newEndIndex - 1 267 | 268 | return updateChildren({...previousInput, 269 | vNode: vNode.setChildren(newChildren), 270 | oldStartIndex: _oldStartIndex, 271 | newEndIndex: _newEndIndex, 272 | oldStartVNode: oldChildren[_oldStartIndex], 273 | newEndVNode: children[_newEndIndex] 274 | }) 275 | } 276 | 277 | // vNode moved left 278 | if (sameVNode(oldEndVNode, newStartVNode)) { 279 | const updatedVNode = patchVNode(oldEndVNode, newStartVNode) 280 | const newChildren = replace(updatedVNode, newStartIndex, children) 281 | 282 | insertBefore(parent, oldEndVNode.element, oldStartVNode.element) 283 | 284 | const _oldEndIndex = oldEndIndex - 1 285 | const _newStartIndex = newStartIndex + 1 286 | 287 | return updateChildren({...previousInput, 288 | vNode: vNode.setChildren(newChildren), 289 | oldEndIndex: _oldEndIndex, 290 | newStartIndex: _newStartIndex, 291 | oldEndVNode: oldChildren[_oldEndIndex], 292 | newStartVNode: children[_newStartIndex] 293 | }) 294 | } 295 | 296 | const _oldKeyToIndex = oldKeyToIndex === null 297 | ? createKeyToIndex(oldChildren, oldStartIndex, oldEndIndex) 298 | : oldKeyToIndex 299 | 300 | const indexInOld = _oldKeyToIndex[newStartVNode.key] 301 | 302 | if (isUndef(indexInOld)) { 303 | // new element 304 | const _vNode = createElement(newStartVNode) 305 | insertBefore(parent, _vNode.element, oldStartVNode.element) 306 | 307 | const _newStartIndex = newStartIndex + 1 308 | const _newStartVNode = children[_newStartIndex] 309 | 310 | return updateChildren({...previousInput, 311 | oldKeyToIndex: _oldKeyToIndex, 312 | vNode: callInsert(_vNode), 313 | newStartIndex: _newStartIndex, 314 | newStartVNode: _newStartVNode 315 | }) 316 | } 317 | 318 | const elementToMove = oldChildren[indexInOld] 319 | const updatedVNode = patchVNode(elementToMove, newStartVNode) 320 | const newChildren = replace(updatedVNode, newStartIndex, children) 321 | const _vNode = vNode.setChildren(newChildren) 322 | // $flow-ignore-line 323 | oldChildren[indexInOld] = undefined 324 | insertBefore(parent, elementToMove.element, oldStartVNode.element) 325 | const _newStartIndex = newStartIndex + 1 326 | const _newStartVNode = children[_newStartIndex] 327 | return updateChildren({...previousInput, 328 | oldKeyToIndex: _oldKeyToIndex, 329 | vNode: _vNode, 330 | newStartIndex: _newStartIndex, 331 | newStartVNode: _newStartVNode 332 | }) 333 | } 334 | 335 | // updates the DOM and VNode with the current information it should have 336 | function patchVNode (oldVNode: VNode, _vNode: VNode): VNode { 337 | // $flow-ignore-line 338 | const vNode = _vNode.setElement(oldVNode.element) 339 | // if the previous and current are equal do nothing 340 | if (oldVNode === vNode) return vNode 341 | 342 | // if the vNode has changed drastically create a new one an replace the old 343 | if (!sameVNode(oldVNode, vNode)) { 344 | const parent = oldVNode.element && oldVNode.element.parentNode 345 | const newVNode = createElement(vNode) 346 | insertBefore(parent, newVNode.element, oldVNode.element) 347 | const updatedVNode = callInsert(newVNode) 348 | if (parent !== null && parent !== undefined) { 349 | removeVNodes(parent, [oldVNode], 0, 0) 350 | } 351 | return updatedVNode 352 | } 353 | 354 | const updatedVNode = callUpdateHooks(oldVNode, vNode) 355 | 356 | const { element, children } = updatedVNode 357 | 358 | // lets update the DOM 359 | if (updatedVNode.text === '') { 360 | if (oldVNode.children.length > 0 && children.length > 0) { // children have changed somehow 361 | if (oldVNode.children === updatedVNode.children) return updatedVNode 362 | 363 | const oldEndIndex = oldVNode.children.length - 1 364 | const newEndIndex = children.length - 1 365 | 366 | const oldStartVNode = oldVNode.children[0] 367 | const oldEndVNode = oldVNode.children[oldEndIndex] 368 | const newStartVNode = children[0] 369 | const newEndVNode = children[newEndIndex] 370 | 371 | const input: UpdateChildren = { 372 | parent: oldVNode.element, 373 | oldChildren: oldVNode.children, 374 | vNode: updatedVNode, 375 | oldKeyToIndex: null, 376 | oldStartIndex: 0, 377 | oldEndIndex, 378 | newStartIndex: 0, 379 | newEndIndex, 380 | oldStartVNode, 381 | oldEndVNode, 382 | newStartVNode, 383 | newEndVNode 384 | } 385 | 386 | return updateChildren(input) 387 | } else if (children.length > 0) { // children have been added when there were none 388 | if (oldVNode.text !== '') setTextContent(element, '') 389 | return addVNodes(element, updatedVNode, null, 0, updatedVNode.children.length - 1) 390 | } else if (oldVNode.children.length > 0) { // children have been completely removed 391 | if (updatedVNode.element) { 392 | removeVNodes(updatedVNode.element, oldVNode.children, 0, oldVNode.children.length - 1) 393 | } 394 | return updatedVNode 395 | } else if (oldVNode.text !== '') { // text has been removed 396 | setTextContent(updatedVNode.element, '') 397 | return updatedVNode 398 | } 399 | } else if (oldVNode.text !== updatedVNode.text) { // update text if needed 400 | setTextContent(element, updatedVNode.text) 401 | } 402 | 403 | return updatedVNode 404 | } 405 | 406 | return function patch (oldVNode: VNode | HTMLElement, vNode: VNode): VNode { 407 | if (oldVNode === vNode) return vNode 408 | 409 | const previousVNode = oldVNode instanceof HTMLElement 410 | ? emptyVNodeAt(oldVNode) 411 | : oldVNode 412 | 413 | if (sameVNode(previousVNode, vNode)) { 414 | return patchVNode(previousVNode, vNode) 415 | } 416 | 417 | const { element } = previousVNode 418 | const parent = element && element.parentNode 419 | 420 | const newVNode = createElement(vNode) 421 | 422 | if (parent) { 423 | insertBefore(parent, newVNode.element, element && element.nextSibling) 424 | return callInsert(newVNode) 425 | } 426 | 427 | return newVNode 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /src/interfaces.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { VNode } from './hyperscript/VNode' 4 | 5 | export type CreateHook = (oldVNode: VNode, vNode: VNode) => VNode; 6 | export type UpdateHook = (oldVNode: VNode, vNode: VNode) => VNode; 7 | export type InsertHook = (vNode: VNode) => VNode; 8 | export type RemoveHook = (vNode: VNode, removeCallback: () => void) => void; 9 | export type DestroyHook = (vNode: VNode) => void; 10 | 11 | export type Module = { 12 | create: CreateHook, 13 | update: UpdateHook, 14 | insert: InsertHook, 15 | remove: RemoveHook, 16 | destroy: DestroyHook 17 | } 18 | -------------------------------------------------------------------------------- /src/module/properties.js: -------------------------------------------------------------------------------- 1 | import { reduce } from '../util' 2 | import { VNode } from '../hyperscript/VNode' 3 | 4 | export function update (oldVNode: VNode, vNode: VNode): VNode { 5 | const oldProps = oldVNode.data.props || {} 6 | const props = vNode.data.props || {} 7 | 8 | if (!oldProps && !props) return vNode 9 | 10 | const keys = Object.keys(props) 11 | 12 | const element = reduce(function (element, key) { 13 | if (!props[key]) { 14 | delete element[key] 15 | } 16 | return element 17 | }, vNode.data.element, keys) 18 | 19 | const newElement = reduce(function (element, key) { 20 | const cur = props[key] 21 | const old = oldProps[key] 22 | 23 | if (old !== cur && key !== 'value' || element[key] !== cur) { 24 | element[key] = cur 25 | } 26 | 27 | return element 28 | }, element, keys) 29 | 30 | return vNode.setElement(newElement) 31 | } 32 | 33 | export const create = update 34 | 35 | export function remove (vNode: VNode, rm: Function) { 36 | rm() 37 | } 38 | 39 | export function destroy (vNode: VNode) {} 40 | 41 | export function insert (vNode: VNode) { 42 | return vNode 43 | } 44 | 45 | export const module = { 46 | update, 47 | create, 48 | insert, 49 | remove, 50 | destroy 51 | } 52 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { VNode } from './hyperscript/VNode' 3 | import { FlowVNode } from './hyperscript/VNode' // eslint-disable-line 4 | 5 | export function isUndef (x: any): boolean { 6 | return x === void 0 7 | } 8 | 9 | export function emptyVNodeAt (node: Element): VNode { 10 | return new FlowVNode(node.tagName.toLowerCase(), node.id || '', copy(node.classList), {}, [], '', node, null) 11 | } 12 | 13 | export function sameVNode (a: VNode, b: VNode): boolean { 14 | return a.selector === b.selector && a.key === b.key 15 | } 16 | 17 | export function forEach (f: Function, arr: Array) { 18 | const length = arr.length 19 | for (let i = 0; i < length; ++i) { // eslint-disable-line immutable/no-let 20 | f(arr[i], i) 21 | } 22 | } 23 | 24 | // copy :: [a] -> [a] 25 | // duplicate a (shallow duplication) 26 | export function copy (a: any): any[] { 27 | const l = a.length 28 | const b = new Array(l) 29 | for (let i = 0; i < l; ++i) { // eslint-disable-line immutable/no-let 30 | b[i] = a[i] 31 | } 32 | return b 33 | } 34 | 35 | // map :: (a -> b) -> [a] -> [b] 36 | // transform each element with f 37 | export function map (f: Function, a: any): any[] { 38 | const l = a.length 39 | const b = new Array(l) 40 | for (let i = 0; i < l; ++i) { // eslint-disable-line immutable/no-let 41 | b[i] = f(a[i], i) 42 | } 43 | return b 44 | } 45 | 46 | // reduce :: (a -> b -> a) -> a -> [b] -> a 47 | // accumulate via left-fold 48 | export function reduce (f: Function, z: any, a: any): any { 49 | let r = z // eslint-disable-line immutable/no-let 50 | for (let i = 0, l = a.length; i < l; ++i) { // eslint-disable-line immutable/no-let 51 | r = f(r, a[i], i) 52 | } 53 | return r 54 | } 55 | 56 | // replace :: a -> Int -> [a] 57 | // replace element at index 58 | export function replace (x: any, i: number, a: any): any[] { 59 | if (i < 0) { 60 | throw new TypeError('i must be >= 0') 61 | } 62 | 63 | const l = a.length 64 | const b = new Array(l) 65 | for (let j = 0; j < l; ++j) { // eslint-disable-line immutable/no-let 66 | b[j] = i === j ? x : a[j] 67 | } 68 | return b 69 | } 70 | --------------------------------------------------------------------------------