├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── countdown │ ├── .babelrc │ ├── app │ │ ├── index.html │ │ └── index.js │ ├── package.json │ └── webpack.config.js └── misc │ └── inspect.gif ├── index.js ├── lib ├── diff.js ├── h.js └── patch.js ├── package.json └── tests ├── fixtures ├── basic.js ├── nested.js ├── simple.js └── textnodes.js └── mocha-tests ├── attributes.test.js ├── diff.test.js ├── events.test.js ├── h.test.js ├── patch.test.js ├── render.test.js └── unrefobjects.test.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "mocha" : true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | } 12 | }, 13 | "rules": { 14 | "indent": [ 15 | "error", 16 | 2 , { "SwitchCase" : 1} 17 | ], 18 | "linebreak-style": [ 19 | "error", 20 | "unix" 21 | ], 22 | "quotes": [ 23 | "error", 24 | "single" 25 | ], 26 | "semi": [ 27 | "error", 28 | "always" 29 | ] 30 | } 31 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | install: 5 | - npm i 6 | script: 7 | - npm run cov -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Svetlana Linuxenko 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Very basic virtual-dom implementation 2 | 3 | [![Build Status](https://travis-ci.org/linuxenko/basic-virtual-dom.svg?branch=master)](https://travis-ci.org/linuxenko/basic-virtual-dom) [![Coverage Status](https://coveralls.io/repos/github/linuxenko/basic-virtual-dom/badge.svg?branch=master)](https://coveralls.io/github/linuxenko/basic-virtual-dom?branch=master) [![dependencies](https://david-dm.org/linuxenko/basic-virtual-dom.svg)](https://github.com/linuxenko/basic-virtual-dom/) [![alpha](https://img.shields.io/badge/stability-Experimental-ff69b4.svg)](https://github.com/linuxenko/basic-virtual-dom) [![npm version](https://img.shields.io/npm/v/basic-virtual-dom.svg)](https://www.npmjs.com/package/basic-virtual-dom) 4 | 5 | ### Features 6 | Support of following patch types: 7 | 8 | * `PATCH_CREATE` 9 | * `PATCH_REMOVE` 10 | * `PATCH_REORDER` 11 | * `PATCH_PROPS` 12 | * Small amount of diffing iterations 13 | * Referal based patches without identifiers 14 | * No iterations over the virtual or dom tree when applying patches 15 | 16 | Seems like it has not so bad memory usage and rendering [performance](https://15lyfromsaturn.github.io/js-repaint-perfs/basic-virtual-dom/index.html) 17 | 18 | [![Example](https://raw.githubusercontent.com/linuxenko/basic-virtual-dom/master/examples/misc/inspect.gif)](https://15lyfromsaturn.github.io/js-repaint-perfs/basic-virtual-dom/index.html) 19 | 20 | ### Example 21 | 22 | Simple day countdown example 23 | ```javascript 24 | /** @jsx h */ 25 | 26 | import {h, patch, diff} from '../../'; 27 | 28 | var initialDom = ( 29 |
30 |

Counter

31 |
32 | ); 33 | 34 | document.getElementById('application') 35 | .appendChild(initialDom.render()); 36 | 37 | setInterval(function() { 38 | var cd = countDown(); 39 | var countDownDom = ( 40 |
41 |

Day Countdown

42 |
43 | {cd.h} :  44 | {cd.m} :  45 | {cd.s} 46 |
47 |
48 | ); 49 | 50 | var diffs = diff(initialDom, countDownDom); 51 | patch(initialDom, diffs); 52 | 53 | }, 1000); 54 | ``` 55 | ### TODO 56 | * test browser support 57 | 58 | ### License 59 | MIT (c) Svetlana Linuxenko 60 | -------------------------------------------------------------------------------- /examples/countdown/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | 'presets' : ['es2015', 'react'] 3 | } 4 | -------------------------------------------------------------------------------- /examples/countdown/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/countdown/app/index.js: -------------------------------------------------------------------------------- 1 | import './index.html'; 2 | 3 | /* coundown gist from https://gist.github.com/loremipson/8834955 */ 4 | var date = new Date(), 5 | month = date.getMonth(), 6 | day = date.getDate(), 7 | weekDay = date.getDay(); 8 | 9 | var hours = { 10 | start: new Date(date.getFullYear(), month, day), 11 | end: new Date(date.getFullYear(), month, day) 12 | }; 13 | 14 | // weekDay var [0 = sun, 1 = mon, 2 = tues ... 5 = fri 6 = sat] 15 | 16 | // If it's Monday - Friday 17 | if(weekDay >= 1 && weekDay <= 5){ 18 | 19 | // Start at 7am, end at 8pm 20 | hours.start.setHours(7); 21 | hours.end.setHours(20); 22 | 23 | // If it's Saturday 24 | } else if(weekDay == 6){ 25 | 26 | // Start at 8am, end at 8pm 27 | hours.start.setHours(8); 28 | hours.end.setHours(20); 29 | 30 | // If it's Sunday 31 | } else { 32 | 33 | // Start at 9am, end at 6pm 34 | hours.start.setHours(9); 35 | hours.end.setHours(18); 36 | } 37 | 38 | function countDown(){ 39 | var date = new Date(), 40 | countHours = ('0' + (hours.end.getHours() - date.getHours())).substr(-2), 41 | countMinutes = ('0' + (59 - date.getMinutes())).substr(-2), 42 | countSeconds = ('0' + (59 - date.getSeconds())).substr(-2); 43 | 44 | return { h : countHours, m : countMinutes, s : countSeconds }; 45 | } 46 | 47 | 48 | /** @jsx h */ 49 | 50 | import {h, patch, diff} from '../../../'; 51 | 52 | var initialDom = ( 53 |
54 |

Counter

55 |
56 | ); 57 | 58 | document.getElementById('application') 59 | .appendChild(initialDom.render()); 60 | 61 | setInterval(function() { 62 | var cd = countDown(); 63 | var countDownDom = ( 64 |
65 |

Day Countdown

66 |
67 | {cd.h} :  68 | {cd.m} :  69 | {cd.s} 70 |
71 |
72 | ); 73 | 74 | var diffs = diff(initialDom, countDownDom); 75 | patch(initialDom, diffs); 76 | 77 | }, 1000); -------------------------------------------------------------------------------- /examples/countdown/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "author": "Svetlana Linuxenko (http://www.linuxenko.pro)", 4 | "license": "MIT", 5 | "devDependencies": { 6 | "babel-core": "^6.18.2", 7 | "babel-loader": "^6.2.8", 8 | "babel-preset-es2015": "^6.18.0", 9 | "babel-preset-react": "^6.16.0", 10 | "file-loader": "^0.9.0", 11 | "webpack": "^1.13.3" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/countdown/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* global __dirname */ 2 | 3 | var path = require('path'); 4 | 5 | var webpack = require('webpack'); 6 | var dir_js = path.resolve(__dirname, 'app'); 7 | var dir_build = path.resolve(__dirname, 'build'); 8 | 9 | module.exports = { 10 | entry: { 11 | app : path.resolve(dir_js, 'index.js') 12 | }, 13 | devtool: 'source-map', 14 | output: { 15 | path: dir_build, 16 | filename: 'bundle.js' 17 | }, 18 | resolveLoader: { 19 | fallback: [path.join(__dirname, 'node_modules')] 20 | }, 21 | resolve: { 22 | modulesDirectories: ['node_modules', '../../../lib', dir_js], 23 | fallback: [path.join(__dirname, 'node_modules')] 24 | }, 25 | devServer: { 26 | contentBase: dir_build, 27 | }, 28 | module: { 29 | loaders: [ 30 | { 31 | loader: 'babel-loader', 32 | test: /\.js$/, 33 | exclude: /node_modules/, 34 | presets : ['es2015', 'react'] 35 | }, 36 | { 37 | test : /\.html$/, 38 | loader : 'file?name=[name].html' 39 | }, 40 | { 41 | test : /\.cur$/, 42 | loader : 'file?name=[name].cur' 43 | } 44 | ] 45 | }, 46 | plugins: [ 47 | new webpack.NoErrorsPlugin() 48 | 49 | ], 50 | stats: { 51 | colors: true 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /examples/misc/inspect.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linuxenko/basic-virtual-dom/aca71fe8c735f57297eb87adcd1a28627ccffed9/examples/misc/inspect.gif -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | exports.h = require('./lib/h').h; 2 | exports.diff = require('./lib/diff').diff; 3 | exports.patch = require('./lib/patch').patch; 4 | 5 | exports.PATCH_CREATE = require('./lib/diff').PATCH_CREATE; 6 | exports.PATCH_REMOVE = require('./lib/diff').PATCH_REMOVE; 7 | exports.PATCH_REPLACE = require('./lib/diff').PATCH_REPLACE; 8 | exports.PATCH_REORDER = require('./lib/diff').PATCH_REORDER; 9 | exports.PATCH_PROPS = require('./lib/diff').PATCH_PROPS; 10 | -------------------------------------------------------------------------------- /lib/diff.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Diff 3 | */ 4 | 5 | var PATCH_CREATE = 0; 6 | var PATCH_REMOVE = 1; 7 | var PATCH_REPLACE = 2; 8 | var PATCH_REORDER = 3; 9 | var PATCH_PROPS = 4; 10 | 11 | /** 12 | * Diff two virtual dom trees 13 | * 14 | * @name diff 15 | * @function 16 | * @access public 17 | * @param {Object} oldNode virtual tree to compare 18 | * @param {Object} newNode virtual tree to compare with 19 | */ 20 | var diff = function (oldNode, newNode) { 21 | if (typeof oldNode === 'undefined' || typeof newNode === 'undefined') { 22 | throw new Error('cannot diff undefined nodes'); 23 | } 24 | 25 | if (!_isNodeSame(oldNode, newNode)) { 26 | throw new Error('unable create diff replace for root node'); 27 | } 28 | 29 | return _diffTree(oldNode, newNode, []); 30 | }; 31 | 32 | /** 33 | * Tree walker function 34 | * 35 | * @name _diffTree 36 | * @function 37 | * @access private 38 | * @param {} a 39 | * @param {} b 40 | * @param {} patches 41 | */ 42 | var _diffTree = function (a, b, patches) { 43 | _diffProps(a, b, patches); 44 | 45 | if (b.tag === 'text') { 46 | if (b.children !== a.children) { 47 | patches.push({ t: PATCH_REPLACE, node: a, with: b }); 48 | } 49 | return; 50 | } 51 | 52 | if (Array.isArray(b.children)) { 53 | _diffChild(a.children, b.children, a, patches); 54 | } else if (Array.isArray(a.children)) { 55 | for (var i = 0; i < a.children.length; i++) { 56 | patches.push({ t: PATCH_REMOVE, from: i, node: _nodeId(a), item: _nodeId(a.children[i]) }); 57 | } 58 | } 59 | 60 | return patches; 61 | }; 62 | 63 | /** 64 | * Tree children diffings 65 | * 66 | * @name _diffChild 67 | * @function 68 | * @access private 69 | * @param {} a 70 | * @param {} b 71 | * @param {} pn 72 | * @param {} patches 73 | */ 74 | var _diffChild = function (a, b, pn, patches) { 75 | var reorderMap = []; 76 | var i; 77 | var j; 78 | var found; 79 | 80 | for (i = 0; i < b.length; i++) { 81 | found = false; 82 | 83 | if (!a) { 84 | if (!pn.children) { 85 | pn.children = []; 86 | } 87 | 88 | if (b[i].tag === 'text') { 89 | patches.push({ t: PATCH_CREATE, to: i, node: _nodeId(pn), item: _nodeId(b[i]) }); 90 | } else { 91 | patches.push({ t: PATCH_CREATE, to: i, node: _nodeId(pn), item: _nodeId(b[i].clone()) }); 92 | } 93 | continue; 94 | } 95 | 96 | for (j = 0; j < a.length; j++) { 97 | if (_isNodeSame(a[j], b[i]) && reorderMap.indexOf(a[j]) === -1) { 98 | if (j !== i) { 99 | patches.push({ t: PATCH_REORDER, from: j, to: i, node: _nodeId(pn), item: _nodeId(a[j]) }); 100 | } 101 | reorderMap.push(a[j]); 102 | 103 | _diffTree(a[j], b[i], patches); 104 | found = true; 105 | break; 106 | } 107 | } 108 | 109 | if (found === false) { 110 | reorderMap.push(null); 111 | patches.push({ t: PATCH_CREATE, to: i, node: _nodeId(pn), item: b[i].tag === 'text' ? _nodeId(b[i]) : _nodeId(b[i].clone()) }); 112 | } 113 | } 114 | 115 | if (!a) return; 116 | 117 | for (i = 0; i < a.length; i++) { 118 | if (reorderMap.indexOf(a[i]) === -1) { 119 | patches.push({ t: PATCH_REMOVE, from: i, node: _nodeId(pn), item: _nodeId(a[i]) }); 120 | } 121 | } 122 | }; 123 | 124 | /** 125 | * Props diffings 126 | * 127 | * @name _diffProps 128 | * @function 129 | * @access private 130 | * @param {} a 131 | * @param {} b 132 | * @param {} patches 133 | * @param {} type 134 | */ 135 | var _diffProps = function (a, b, patches) { 136 | if (!a || !b || !a.props && !b.props) { 137 | return; 138 | } 139 | 140 | var toChange = []; 141 | var toRemove = []; 142 | var battrs = Object.keys(b.props); 143 | var aattrs = Object.keys(a.props); 144 | var aattrsLen = aattrs.filter(function(attr) { 145 | return (attr !== 'ref' && !(attr.match(/^on/))); 146 | }).length; 147 | var i; 148 | 149 | if (a.el && a.el.attributes.length !== aattrsLen) { 150 | for (i = 0; i < a.el.attributes.length; i++) { 151 | var attr = a.el.attributes[i]; 152 | var name = attr.name; 153 | 154 | if (name === 'class') { 155 | name = 'className'; 156 | } 157 | 158 | if (!(name in aattrs)) { 159 | a.props[name] = attr.value; 160 | } 161 | 162 | if (attr.value !== a.props[name]) { 163 | a.props[name] = attr.value; 164 | } 165 | } 166 | aattrs = Object.keys(a.props); 167 | } 168 | 169 | for (i = 0; i < battrs.length || i < aattrs.length; i++) { 170 | if (i < battrs.length) { 171 | if (!(battrs[i] in a.props) || b.props[battrs[i]] !== a.props[battrs[i]]) { 172 | toChange.push({ name: battrs[i], value: b.props[battrs[i]] }); 173 | } 174 | } 175 | 176 | if (i < aattrs.length) { 177 | if (!(aattrs[i] in b.props)) { 178 | toRemove.push({ name: aattrs[i] }); 179 | } 180 | } 181 | } 182 | 183 | if (toRemove.length > 0) { 184 | patches.push({ t: PATCH_PROPS, remove: toRemove, node: _nodeId(a) }); 185 | } 186 | 187 | if (toChange.length > 0) { 188 | patches.push({ t: PATCH_PROPS, change: toChange, node: _nodeId(a) }); 189 | } 190 | }; 191 | 192 | /** 193 | * Node identifier 194 | * 195 | * @name _nodeId 196 | * @function 197 | * @access private 198 | * @param {} node 199 | */ 200 | var _nodeId = function (node) { 201 | return node; 202 | }; 203 | 204 | /** 205 | * Nodes comparison 206 | * 207 | * @name _isNodeSame 208 | * @function 209 | * @access private 210 | * @param {} a 211 | * @param {} b 212 | */ 213 | var _isNodeSame = function (a, b) { 214 | return a.tag === b.tag; 215 | }; 216 | 217 | exports.PATCH_CREATE = PATCH_CREATE; 218 | exports.PATCH_REMOVE = PATCH_REMOVE; 219 | exports.PATCH_REPLACE = PATCH_REPLACE; 220 | exports.PATCH_REORDER = PATCH_REORDER; 221 | exports.PATCH_PROPS = PATCH_PROPS; 222 | exports.diff = diff; 223 | -------------------------------------------------------------------------------- /lib/h.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Element 3 | */ 4 | 5 | /** 6 | * General tree 7 | * 8 | * /** @jsx h * / 9 | * 10 | * @name h 11 | * @function 12 | * @access public 13 | */ 14 | var H = function (argv) { 15 | if (!(this instanceof H)) { 16 | if (!(argv instanceof H)) { 17 | if (typeof argv === 'function') { 18 | return argv.apply(argv, [].slice.call(arguments, 1, arguments.length)); 19 | } 20 | 21 | if (typeof argv === 'object' && typeof argv.render === 'function') { 22 | if ('props' in argv) { 23 | for (var i in arguments[1]) { 24 | argv.props[i] = arguments[1][i]; 25 | } 26 | } else { 27 | argv.props = arguments[1] || []; 28 | } 29 | argv.props.children = [].slice.call(arguments, 2, arguments.length); 30 | return argv.render(argv.props); 31 | } 32 | } 33 | return new H(arguments); 34 | } 35 | 36 | if (argv[0] instanceof H) { 37 | return argv[0]; 38 | } 39 | 40 | this.tag = argv[0].toLowerCase(); 41 | this.props = argv[1] || {}; 42 | 43 | if (argv[2] === null || argv[2] === undefined) { 44 | return; 45 | } 46 | 47 | if (argv.length > 2) { 48 | if (typeof argv[2] !== 'object' && argv.length === 3) { 49 | this.children = [_createTextNode(argv[2])]; 50 | } else if (Array.isArray(argv[2])) { 51 | this.children = argv[2]; 52 | } else { 53 | this.children = [].concat.apply([], [].slice.call(argv, 2, argv.length)) 54 | .filter(function (n) { 55 | return n !== null && n !== undefined && n !== false; 56 | }) 57 | .map(function (n) { 58 | if (!(n instanceof H)) { 59 | return _createTextNode(n); 60 | } else { 61 | return n; 62 | } 63 | }); 64 | } 65 | } 66 | }; 67 | 68 | /** 69 | * Tree renderer 70 | * 71 | * @name render 72 | * @function 73 | * @access public 74 | * @param {Boolean} fasle - do not save DOM into tree 75 | */ 76 | H.prototype.render = function (node, parent) { 77 | node = node || this; 78 | 79 | node.el = createElement(node.tag ? node : this, parent); 80 | 81 | var children = node.children; 82 | 83 | if (typeof children === 'object') { 84 | for (var i = 0; i < children.length; i++) { 85 | node.el.appendChild(this.render(children[i], node.el)); 86 | } 87 | } 88 | 89 | return node.el; 90 | }; 91 | 92 | H.prototype.setProp = function (name, value) { 93 | if (typeof this.el !== 'undefined') { 94 | if (name === 'className') { 95 | this.el.setAttribute('class', value); 96 | } else if (name === 'style' && typeof value !== 'string') { 97 | this.el.setAttribute('style', _stylePropToString(value)); 98 | } else if (name.match(/^on/)) { 99 | this.addEvent(name, value); 100 | } else if (name === 'ref') { 101 | if (typeof value === 'function') { 102 | value(this.el); 103 | } 104 | } else if (typeof value === 'boolean' || value === 'true') { 105 | this.el.setAttribute(name, value); 106 | this.el[name] = Boolean(value); 107 | } else { 108 | this.el.setAttribute(name, value); 109 | } 110 | } 111 | 112 | this.props[name] = value; 113 | }; 114 | 115 | H.prototype.setProps = function (props) { 116 | var propNames = Object.keys(props); 117 | 118 | for (var i = 0; i < propNames.length; i++) { 119 | var prop = propNames[i]; 120 | this.setProp(prop, props[prop]); 121 | } 122 | }; 123 | 124 | H.prototype.rmProp = function (name) { 125 | if (typeof this.el !== 'undefined') { 126 | if (name === 'className') { 127 | this.el.removeAttribute('class'); 128 | } else if (name.match(/^on/)) { 129 | this.removeEvent(name); 130 | } else if (name === 'ref') { 131 | /* Nothing to do */ 132 | } else if (typeof value === 'boolean') { 133 | this.el.removeAttribute(name); 134 | delete this.el[name]; 135 | } else { 136 | this.el.removeAttribute(name); 137 | } 138 | } 139 | 140 | delete this.props[name]; 141 | }; 142 | 143 | H.prototype.addEvent = function (name, listener) { 144 | name = name.slice(2).toLowerCase(); 145 | 146 | this.listeners = this.listeners || {}; 147 | 148 | if (name in this.listeners) { 149 | this.removeEvent(name); 150 | } 151 | 152 | this.listeners[name] = listener; 153 | this.el.addEventListener(name, listener); 154 | }; 155 | 156 | H.prototype.removeEvent = function (name) { 157 | name = name.replace(/^on/, '').toLowerCase(); 158 | if (name in this.listeners) { 159 | this.el.removeEventListener(name, this.listeners[name]); 160 | delete this.listeners[name]; 161 | } 162 | }; 163 | 164 | H.prototype.clone = function () { 165 | var node = { 166 | tag: String(this.tag), 167 | props: _cloneProps(this.props) 168 | }; 169 | 170 | if (typeof this.children !== 'undefined') { 171 | node.children = this.tag === 'text' 172 | ? String(this.children) 173 | : this.children.map(function (child) { 174 | return child.tag === 'text' ? _createTextNode(child.children) : child.clone(); 175 | }); 176 | } 177 | 178 | return H(node.tag, node.props, node.children); 179 | }; 180 | 181 | var _cloneProps = function (props, keepRefs) { 182 | if (typeof keepRefs === 'undefined') { 183 | keepRefs = true; 184 | } 185 | 186 | var attrs = Object.keys(props); 187 | var i; 188 | var name; 189 | var cloned = {}; 190 | 191 | for (i = 0; i < attrs.length; i++) { 192 | name = attrs[i]; 193 | 194 | if (typeof props[name] === 'string') { 195 | cloned[name] = String(props[name]); 196 | } else if (typeof props[name] === 'function' && keepRefs === true) { 197 | cloned[name] = props[name]; 198 | } else if (typeof props[name] === 'boolean') { 199 | cloned[name] = Boolean(props[name]); 200 | } else if (typeof props[name] === 'object') { 201 | cloned[name] = _cloneProps(props[name]); 202 | } 203 | } 204 | 205 | return cloned; 206 | }; 207 | 208 | var _stylePropToString = function (props) { 209 | var out = ''; 210 | var attrs = Object.keys(props); 211 | 212 | for (var i = 0; i < attrs.length; i++) { 213 | out += attrs[i].replace(/([A-Z])/g, '-$1').toLowerCase(); 214 | out += ':'; 215 | out += props[attrs[i]]; 216 | out += ';'; 217 | } 218 | 219 | return out; 220 | }; 221 | 222 | var _createTextNode = function (text) { 223 | return { 224 | tag: 'text', 225 | children: String(text) 226 | }; 227 | }; 228 | 229 | var createElement = function (node, parent) { 230 | node.el = node.tag === 'text' 231 | ? document.createTextNode(node.children) 232 | : document.createElement(node.tag); 233 | 234 | if (typeof node.props !== 'undefined') { 235 | node.setProps(node.props); 236 | } 237 | 238 | if (typeof parent !== 'undefined') { 239 | parent.appendChild(node.el); 240 | } 241 | 242 | return node.el; 243 | }; 244 | 245 | exports.h = H; 246 | exports.createElement = createElement; 247 | -------------------------------------------------------------------------------- /lib/patch.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Patch 3 | */ 4 | 5 | var PATCH_CREATE = require('./diff').PATCH_CREATE; 6 | var PATCH_REMOVE = require('./diff').PATCH_REMOVE; 7 | var PATCH_REPLACE = require('./diff').PATCH_REPLACE; 8 | var PATCH_REORDER = require('./diff').PATCH_REORDER; 9 | var PATCH_PROPS = require('./diff').PATCH_PROPS; 10 | 11 | var createElement = require('./h').createElement; 12 | 13 | /** 14 | * Patch DOM and virtual tree 15 | * 16 | * @name patch 17 | * @function 18 | * @access public 19 | * @param {Object} tree Tree to patch 20 | * @param {Array} patches Array of patches 21 | */ 22 | var patch = function(tree, patches) { 23 | var render = true; 24 | 25 | if (typeof tree.el === 'undefined') { 26 | render = false; 27 | } 28 | 29 | for (var i = 0; i < patches.length; i++) { 30 | var p = patches[i]; 31 | 32 | switch(p.t) { 33 | case PATCH_REORDER: 34 | _patchReorder(p, render); 35 | break; 36 | case PATCH_CREATE: 37 | _patchCreate(p, render); 38 | break; 39 | case PATCH_REMOVE: 40 | _patchRemove(p, render); 41 | break; 42 | case PATCH_REPLACE: 43 | _patchReplace(p, render); 44 | break; 45 | case PATCH_PROPS: 46 | _patchProps(p, render); 47 | break; 48 | } 49 | } 50 | }; 51 | 52 | /** 53 | * Replace existen node content 54 | * 55 | * @name patchReplace 56 | * @function 57 | * @access private 58 | */ 59 | var _patchReplace = function(p, render) { 60 | p.node.children = String(p.with.children); 61 | 62 | if (render === true) { 63 | p.node.el.nodeValue = String(p.with.children); 64 | } 65 | }; 66 | 67 | /** 68 | * Reorder existen node 69 | * 70 | * @name patchReorder 71 | * @function 72 | * @access private 73 | */ 74 | var _patchReorder = function(p, render) { 75 | if (render === true) { 76 | p.node.el.insertBefore(p.item.el, p.node.el.childNodes[p.to]); 77 | } 78 | 79 | p.node.children.splice(p.to, 0, 80 | p.node.children.splice(p.node.children.indexOf(p.item), 1)[0]); 81 | }; 82 | 83 | /** 84 | * Create new tree node 85 | * 86 | * @name patchCreate 87 | * @function 88 | * @access private 89 | */ 90 | var _patchCreate = function(p, render) { 91 | var element; 92 | 93 | if (render === true) { 94 | element = p.item.tag === 'text' ? 95 | createElement(p.item) : p.item.render(); 96 | } 97 | 98 | if (p.node.children.length - 1 < p.to) { 99 | p.node.children.push(p.item); 100 | 101 | if (render === true) { 102 | p.node.el.appendChild(element); 103 | } 104 | } else { 105 | p.node.children.splice(p.to, 0, p.item); 106 | 107 | if (render === true) { 108 | p.node.el.insertBefore(element, p.node.el.childNodes[p.to]); 109 | } 110 | } 111 | }; 112 | 113 | /** 114 | * Remove tree node 115 | * 116 | * @name patchRemove 117 | * @function 118 | * @access private 119 | */ 120 | var _patchRemove = function(p, render) { 121 | if (render === true) { 122 | p.node.el.removeChild(p.item.el); 123 | } 124 | 125 | for (var i = 0; i < p.node.children.length; i++) { 126 | if (p.node.children[i] === p.item) { 127 | p.node.children.splice(i, 1); 128 | } 129 | } 130 | }; 131 | 132 | /** 133 | * Replace props 134 | * 135 | * @name _patchProps 136 | * @function 137 | * @access private 138 | */ 139 | var _patchProps = function(p) { 140 | var i; 141 | 142 | if ('remove' in p) { 143 | for (i = 0; i < p.remove.length; i++) { 144 | p.node.rmProp(p.remove[i].name); 145 | } 146 | return; 147 | } 148 | 149 | if ('change' in p) { 150 | for (i = 0; i < p.change.length; i++) { 151 | p.node.setProp(p.change[i].name, p.change[i].value); 152 | } 153 | return; 154 | } 155 | }; 156 | 157 | exports.patch = patch; 158 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-virtual-dom", 3 | "version": "0.3.3", 4 | "description": "Basic virtual dom implementation", 5 | "main": "index.js", 6 | "files": [ 7 | "lib", 8 | "index.js", 9 | "README.md", 10 | "LICENSE" 11 | ], 12 | "directories": {}, 13 | "dependencies": {}, 14 | "devDependencies": { 15 | "chai": "^3.5.0", 16 | "coveralls": "^2.11.15", 17 | "eslint": "^3.11.1", 18 | "jsdom": "^9.8.3", 19 | "mocha": "^3.2.0", 20 | "mocha-jsdom": "^1.1.0", 21 | "nyc": "^10.0.0", 22 | "sinon": "^2.1.0" 23 | }, 24 | "scripts": { 25 | "test": "npm run ci", 26 | "mocha": "mocha --ui bdd tests/mocha-tests", 27 | "lint": "eslint lib tests", 28 | "ci": "npm run lint && npm run mocha", 29 | "cov": "nyc npm run ci && nyc report --reporter=text-lcov | coveralls" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/linuxenko/basic-virtual-dom.git" 34 | }, 35 | "keywords": [ 36 | "virtual-dom", 37 | "virual dom", 38 | "diff", 39 | "patch", 40 | "browser" 41 | ], 42 | "author": "Svetlana Linuxenko (http://www.linuxenko.pro)", 43 | "license": "MIT", 44 | "bugs": { 45 | "url": "https://github.com/linuxenko/basic-virtual-dom/issues" 46 | }, 47 | "homepage": "https://github.com/linuxenko/basic-virtual-dom#readme" 48 | } 49 | -------------------------------------------------------------------------------- /tests/fixtures/basic.js: -------------------------------------------------------------------------------- 1 | var h = require('../../').h; 2 | 3 | exports.tree1 = h( 4 | 'div', 5 | { id: 'application', className: 'main-app' }, 6 | h( 7 | 'em', 8 | { className: 'em' }, 9 | 'Item 1' 10 | ), 11 | h( 12 | 'div', 13 | null, 14 | 'ffirett' 15 | ), 16 | h( 17 | 'div', 18 | { className: '2to-remove' }, 19 | '2removable div' 20 | ), 21 | h( 22 | 'span', 23 | { className: 'menu-item' }, 24 | 'Item 1' 25 | ), 26 | h( 27 | 'ul', 28 | null, 29 | h( 30 | 'li', 31 | null, 32 | h( 33 | 'span', 34 | { className: 'menu-item' }, 35 | 'Item 1' 36 | ), 37 | h( 38 | 'p', 39 | { className: 'redundant-item' }, 40 | 'new text Item 2' 41 | ) 42 | ), 43 | h( 44 | 'li', 45 | null, 46 | h( 47 | 'div', 48 | { className: 'changed-menu-item' }, 49 | 'new text Item 2' 50 | ), 51 | h( 52 | 'span', 53 | { className: 'menu-item' }, 54 | 'Item 2' 55 | ) 56 | ) 57 | ) 58 | ); 59 | 60 | exports.tree2 = h( 61 | 'div', 62 | { id: 'app', className: 'changed-class' }, 63 | h( 64 | 'span', 65 | { className: 'menu-item' }, 66 | 'Item 1' 67 | ), 68 | h( 69 | 'strong', 70 | null, 71 | 'sttrong' 72 | ), 73 | h( 74 | 'ul', 75 | { className : 'test test' }, 76 | h( 77 | 'li', 78 | null, 79 | h( 80 | 'span', 81 | { className : 'llll', id : 'kkkkk' }, 82 | 'Item changed text 1' 83 | ) 84 | ), 85 | h( 86 | 'li', 87 | null, 88 | h( 89 | 'span', 90 | { className: 'changed-menu-item' }, 91 | 'new text Item 2' 92 | ), 93 | h( 94 | 'div', 95 | { className: 'changed-menu-item' }, 96 | 'new text Item 2' 97 | ) 98 | ), 99 | h( 100 | 'li', 101 | null, 102 | h( 103 | 'span', 104 | { className: 'menu-item' }, 105 | 'Item 3' 106 | ) 107 | ) 108 | ), 109 | h( 110 | 'div', 111 | { className: 'to-remove' }, 112 | 'removable div' 113 | ) 114 | ); 115 | -------------------------------------------------------------------------------- /tests/fixtures/nested.js: -------------------------------------------------------------------------------- 1 | var h = require('../../').h; 2 | 3 | exports.a = h( 4 | 'div', 5 | { id: 'app', className: 'changed-class' }, 6 | h( 7 | 'ul', 8 | null, 9 | h( 10 | 'li', 11 | null, 12 | h( 13 | 'span', 14 | { className: 'menu-item' }, 15 | 'Item 1' 16 | ) 17 | ), 18 | h( 19 | 'li', 20 | null, 21 | h( 22 | 'span', 23 | { className: 'changed-menu-item' }, 24 | 'Item 2' 25 | ) 26 | ), 27 | h( 28 | 'li', 29 | null, 30 | h( 31 | 'span', 32 | { className: 'menu-item' }, 33 | 'Item 3' 34 | ) 35 | ) 36 | ) 37 | ); 38 | 39 | exports.b = h( 40 | 'div', 41 | { id: 'app', className: 'changed-class' }, 42 | h( 43 | 'ul', 44 | null, 45 | h( 46 | 'li', 47 | null, 48 | h( 49 | 'span', 50 | { className: 'menu-item' }, 51 | 'Item 1' 52 | ) 53 | ), 54 | h( 55 | 'li', 56 | null, 57 | h( 58 | 'span', 59 | { className: 'changed-menu-item' }, 60 | 'Item 2' 61 | ) 62 | ), 63 | h( 64 | 'li', 65 | null, 66 | h( 67 | 'span', 68 | { className: 'menu-item' }, 69 | 'Item 3 added text' 70 | ) 71 | ) 72 | ) 73 | ); 74 | 75 | 76 | exports.a1 = h( 77 | 'div', 78 | { id: 'app', className: 'changed-class' }, 79 | h( 80 | 'ul', 81 | null, 82 | h( 83 | 'li', 84 | null, 85 | h( 86 | 'span', 87 | { className: 'menu-item' }, 88 | 'Item 1' 89 | ) 90 | ), 91 | h( 92 | 'li', 93 | null, 94 | h( 95 | 'span', 96 | { className: 'changed-menu-item' }, 97 | 'Item 2' 98 | ) 99 | ), 100 | h( 101 | 'li', 102 | null, 103 | h( 104 | 'span', 105 | { className: 'menu-item' }, 106 | 'Item 3' 107 | ) 108 | ) 109 | ) 110 | ); 111 | 112 | exports.b1 = h( 113 | 'div', 114 | { id: 'app', className: 'changed-class' }, 115 | h( 116 | 'ul', 117 | null, 118 | h( 119 | 'li', 120 | null, 121 | h( 122 | 'span', 123 | { className: 'menu-item' }, 124 | 'Item 1' 125 | ) 126 | ), 127 | h( 128 | 'li', 129 | null, 130 | h( 131 | 'span', 132 | { className: 'changed-menu-item' }, 133 | 'Item 2' 134 | ) 135 | ), 136 | h( 137 | 'li', 138 | null, 139 | h( 140 | 'span', 141 | { className: 'menu-item' }, 142 | 'Item 3 added text' 143 | ) 144 | ) 145 | ) 146 | ); 147 | 148 | -------------------------------------------------------------------------------- /tests/fixtures/simple.js: -------------------------------------------------------------------------------- 1 | var h = require('../../').h; 2 | 3 | exports.a = h( 4 | 'div', 5 | { id: 'application', className: 'test-class test-class2' }, 6 | h( 7 | 'div', 8 | null, 9 | 'text' 10 | ) 11 | ); 12 | 13 | exports.a1 = h( 14 | 'div', 15 | { id: 'application', className: 'test-class test-class2' }, 16 | 'text' 17 | ); 18 | 19 | exports.a2 = h( 20 | 'div', 21 | { id: 'application', className: 'no-class' }, 22 | h( 23 | 'div', 24 | null, 25 | 'changed' 26 | ) 27 | ); 28 | 29 | exports.b = h( 30 | 'div', 31 | { id: 'application', className: 'test-class test-class2' }, 32 | 'text' 33 | ); 34 | 35 | exports.c = h( 36 | 'div', 37 | { id: 'application', className: 'test-class test-class2' }, 38 | h( 39 | 'div', 40 | null, 41 | 'changed' 42 | ) 43 | ); 44 | 45 | exports.d = h( 46 | 'div', 47 | null, 48 | h( 49 | 'span', 50 | { id : 'text-node', className : 'text-node' }, 51 | 'node-text' 52 | ) 53 | ); 54 | 55 | exports.e = h( 56 | 'div', 57 | { 'data-name' : 'to remove', className : 'test-class' }, 58 | h( 59 | 'div', 60 | null, 61 | 'changed' 62 | ) 63 | ); 64 | 65 | exports.f = h( 66 | 'div', 67 | { id: 'app', className: 'changed-class' }, 68 | h( 69 | 'span', 70 | { className: 'changed-menu-item' }, 71 | 'Reorder me' 72 | ), 73 | h( 74 | 'ul', 75 | null, 76 | h( 77 | 'li', 78 | null, 79 | h( 80 | 'span', 81 | { className: 'menu-item' }, 82 | 'Item 1' 83 | ) 84 | ), 85 | h( 86 | 'li', 87 | null, 88 | h( 89 | 'span', 90 | { className: 'changed-menu-item' }, 91 | 'Item 2' 92 | ) 93 | ), 94 | h( 95 | 'li', 96 | null, 97 | h( 98 | 'span', 99 | { className: 'menu-item' }, 100 | 'Item 3' 101 | ) 102 | ) 103 | ) 104 | ); 105 | 106 | exports.f1 = h( 107 | 'div', 108 | { id: 'app', className: 'changed-class' }, 109 | h( 110 | 'ul', 111 | null, 112 | h( 113 | 'li', 114 | null, 115 | h( 116 | 'span', 117 | { className: 'menu-item' }, 118 | 'Item 1' 119 | ) 120 | ), 121 | h( 122 | 'li', 123 | null, 124 | h( 125 | 'span', 126 | { className: 'changed-menu-item' }, 127 | 'Item 2' 128 | ) 129 | ), 130 | h( 131 | 'li', 132 | null, 133 | h( 134 | 'span', 135 | { className: 'menu-item' }, 136 | 'Item 3' 137 | ) 138 | ) 139 | ), 140 | h( 141 | 'span', 142 | { className: 'changed-menu-item' }, 143 | 'Reorder me' 144 | ) 145 | ); 146 | -------------------------------------------------------------------------------- /tests/fixtures/textnodes.js: -------------------------------------------------------------------------------- 1 | var h = require('../../').h; 2 | exports.a = h( 3 | 'div', 4 | null, 5 | h( 6 | 'div', 7 | null, 8 | h( 9 | 'h3', 10 | null, 11 | 'Counter' 12 | ) 13 | ) 14 | ); 15 | 16 | exports.b = h( 17 | 'div', 18 | null, 19 | h( 20 | 'div', 21 | null, 22 | h( 23 | 'h3', 24 | null, 25 | 'Day Countdown' 26 | ) 27 | ), 28 | h( 29 | 'div', 30 | { className: 'clock' }, 31 | h( 32 | 'strong', 33 | null, 34 | '1' 35 | ), 36 | ' :', 37 | h( 38 | 'strong', 39 | null, 40 | '2' 41 | ), 42 | ' :', 43 | h( 44 | 'strong', 45 | null, 46 | '3' 47 | ) 48 | ) 49 | ); 50 | 51 | -------------------------------------------------------------------------------- /tests/mocha-tests/attributes.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | 3 | var jsdom = require('mocha-jsdom'); 4 | 5 | var h = require('../../').h; 6 | var diff = require('../../').diff; 7 | var patch = require('../../').patch; 8 | 9 | describe('Test attributes', function() { 10 | jsdom(); 11 | 12 | it('should setup style attribute', function() { 13 | var a = h('div', { style: { backgroundColor: '#fff', left: '20px' } }, 14 | h('div', { style: 'right:20px;' }) 15 | ); 16 | 17 | var dom = a.render(); 18 | 19 | expect(a.props['style']).to.be.exists; 20 | expect(a.props['style']).to.be.deep.equal({ backgroundColor: '#fff', left: '20px' }); 21 | expect(dom.getAttribute('style')).to.be.a('string'); 22 | expect(dom.getAttribute('style')).to.be.equal('background-color:#fff;left:20px;'); 23 | expect(a.children[0].props['style']).to.be.equal('right:20px;'); 24 | }); 25 | 26 | it('should diff and patch style attribute', function() { 27 | var a = h('div', { style: { left: '21px' }}, ''); 28 | var b = h('div', { style: { backgroundColor: '#fff', left: '20px' } }, 29 | h('div', { style: 'right:20px;' }) 30 | ); 31 | 32 | var dom = a.render(); 33 | patch(a, diff(a, b)); 34 | 35 | expect(a.props['style']).to.be.exists; 36 | expect(a.props['style']).to.be.deep.equal({ backgroundColor: '#fff', left: '20px' }); 37 | expect(dom.getAttribute('style')).to.be.a('string'); 38 | expect(dom.getAttribute('style')).to.be.equal('background-color:#fff;left:20px;'); 39 | expect(a.children[0].props['style']).to.be.equal('right:20px;'); 40 | }); 41 | 42 | it('should patch attrs modified via dom node', function () { 43 | var a = h('div', null, ''); 44 | var b = h('div', null, ''); 45 | 46 | var dom = a.render(); 47 | 48 | dom.classList.add('active'); 49 | patch(a, diff(a, b)); 50 | 51 | expect(dom.attributes.length).to.be.equal(0); 52 | expect(dom.props).to.be.an('undefined'); 53 | }); 54 | 55 | it('should patch attrs created via dom node', function () { 56 | var a = h('div', { onClick: function () {}}, ''); 57 | var b = h('div', { ref: function () {}, onClick: function () {}} , ''); 58 | 59 | var dom = a.render(); 60 | 61 | dom.classList.add('active'); 62 | 63 | expect(dom.attributes.length).to.be.equal(1); 64 | patch(a, diff(a, b)); 65 | 66 | expect(dom.attributes.length).to.be.equal(0); 67 | expect(Object.keys(a.props).length).to.be.equal(2); 68 | }); 69 | 70 | it('should patch attrs created via dom node #2', function () { 71 | var a = h('div', { onClick: function () {}}, ''); 72 | var b = h('div', { onClick: function () {}, tabindex: 0} , ''); 73 | 74 | var dom = a.render(); 75 | 76 | dom.classList.add('active'); 77 | 78 | expect(dom.attributes.length).to.be.equal(1); 79 | patch(a, diff(a, b)); 80 | 81 | expect(dom.attributes.length).to.be.equal(1); 82 | expect(dom.attributes[0].name).to.be.equal('tabindex'); 83 | expect(Object.keys(a.props).length).to.be.equal(2); 84 | }); 85 | 86 | it('should patch listeners #1', function () { 87 | var listener1 = function() { return 1; }; 88 | var a = h('div', { onClick: listener1}, ''); 89 | var b = h('div', { onClick: function () { return 2; }, tabindex: 0} , ''); 90 | 91 | a.render(); 92 | 93 | expect(a.props.onClick()).to.be.equal(1); 94 | patch(a, diff(a, b)); 95 | expect(a.props.onClick()).to.be.equal(2); 96 | }); 97 | 98 | it('should patch replace listeners #2', function () { 99 | var fail = false; 100 | var listener1 = function() { fail = true; }; 101 | var a = h('div', { onClick: listener1}, ''); 102 | var b = h('div', { tabindex: 0} , ''); 103 | 104 | var dom = a.render(); 105 | patch(a, diff(a, b)); 106 | 107 | dom.click(); 108 | expect(fail).to.be.false; 109 | }); 110 | }); -------------------------------------------------------------------------------- /tests/mocha-tests/diff.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | 3 | var jsdom = require('mocha-jsdom'); 4 | 5 | var diff = require('../../').diff; 6 | var h = require('../../').h; 7 | 8 | var a = require('../fixtures/simple').a; 9 | var b = require('../fixtures/simple').b; 10 | var c = require('../fixtures/simple').c; 11 | var e = require('../fixtures/simple').e; 12 | 13 | var na = require('../fixtures/nested').a; 14 | var nb = require('../fixtures/nested').b; 15 | 16 | var PATCH_REPLACE = require('../../').PATCH_REPLACE; 17 | 18 | describe('Test diff()', function() { 19 | jsdom(); 20 | 21 | it('should create trees', function() { 22 | expect(a).to.be.an('object'); 23 | expect(b).to.be.an('object'); 24 | }); 25 | 26 | it('should deal with empty arguments', function() { 27 | expect(function() { diff(a); }).to.throw(); 28 | expect(function() { diff(undefined, a); }).to.throw(); 29 | }); 30 | 31 | it('should diff simple tree', function() { 32 | var diffs; 33 | 34 | a.render(); 35 | 36 | expect(function() { 37 | diffs = diff(a, c); 38 | }).not.throw(); 39 | 40 | expect(diffs).to.be.an('array'); 41 | expect(diffs.length).to.be.equal(1); 42 | expect(diffs[0].t).to.be.equal(PATCH_REPLACE); 43 | }); 44 | 45 | it('should diff nonexisten node', function() { 46 | var dom = b.render(); 47 | var diffs; 48 | 49 | expect(dom.children).not.be.exists; 50 | 51 | diffs = diff(b, a); 52 | 53 | expect(diffs.length).to.be.equal(2); 54 | }); 55 | 56 | it('should diff props', function() { 57 | e.render(); 58 | var diffs = diff(e, c); 59 | 60 | 61 | expect(diffs[0].t).to.be.equal(4); 62 | expect(diffs[1].t).to.be.equal(4); 63 | expect(diffs[1].change.length).to.be.equal(2); 64 | }); 65 | 66 | it('should diff complex tree', function() { 67 | na.render(); 68 | expect(na.children[0].children.length).to.be.equal(3); 69 | var diffs = diff(na, nb); 70 | 71 | expect(diffs).to.be.an('array'); 72 | expect(diffs.length).to.be.equal(1); 73 | expect(diffs[0].t).to.be.equal(2); 74 | }); 75 | 76 | it('should not diff root node', function() { 77 | var src = h('span', null, 'hello world'); 78 | var dst = h('strong', null, 'bye bye'); 79 | 80 | src.render(); 81 | expect(function() { diff(src, dst); }).to.throw(); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /tests/mocha-tests/events.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var jsdom = require('mocha-jsdom'); 3 | 4 | var h = require('../../').h; 5 | var diff = require('../../').diff; 6 | var patch = require('../../').patch; 7 | 8 | describe('Test events', function() { 9 | jsdom(); 10 | 11 | it('should handle very simple click event', function(done) { 12 | var tree = h('div', { onClick : function() { done(); } }, ''); 13 | 14 | expect(tree.props.onClick).to.be.a('function'); 15 | tree.render(); 16 | tree.el.click(); 17 | }); 18 | 19 | it('should repatch event handlers', function(done) { 20 | var counter = 0; 21 | var a = h('div', { onClick : function() { counter++; } }, ''); 22 | var b = h('div', { onClick : function() { 23 | expect(counter).to.be.equal(1); 24 | done(); 25 | } }, ''); 26 | 27 | a.render(); 28 | a.el.click(); 29 | 30 | expect(counter).to.be.equal(1); 31 | expect(Object.keys(a.listeners).length).to.be.equal(1); 32 | 33 | patch(a, diff(a, b)); 34 | expect(Object.keys(a.listeners).length).to.be.equal(1); 35 | a.el.click(); 36 | }); 37 | 38 | it('should work with refs', function() { 39 | var elRef; 40 | var a = h('div', { ref : function(ref) { elRef = ref; } }, ''); 41 | 42 | expect(elRef).to.be.an('undefined'); 43 | a.render(); 44 | expect(elRef).to.be.equal(a.el); 45 | }); 46 | 47 | it('should replace refs by patch', function() { 48 | var elRef1; 49 | var elRef2; 50 | var a = h('div', { ref : function(ref) { elRef1 = ref; } }, ''); 51 | var b = h('div', { ref : function(ref) { elRef2 = ref; } }, ''); 52 | 53 | a.render(); 54 | var diffs = diff(a, b); 55 | patch(a, diffs); 56 | 57 | expect(elRef1).to.be.equal(a.el); 58 | expect(elRef2).to.be.equal(a.el); 59 | }); 60 | }); 61 | 62 | -------------------------------------------------------------------------------- /tests/mocha-tests/h.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var sinon = require('sinon'); 3 | var jsdom = require('mocha-jsdom'); 4 | 5 | var a = require('../fixtures/simple').a; 6 | var d = require('../fixtures/simple').d; 7 | 8 | var h = require('../../').h; 9 | 10 | describe('Test h()', function() { 11 | jsdom(); 12 | it('should create object tree', function() { 13 | expect(a).to.be.exists; 14 | expect(a).to.be.an('object'); 15 | expect(a.tag).to.be.equal('div'); 16 | expect(a.children).to.be.an('array'); 17 | }); 18 | 19 | it('should create object with props', function() { 20 | expect(a.props.className).to.be.a('string'); 21 | expect(a.props.className).to.be.equal('test-class test-class2'); 22 | }); 23 | 24 | it('should create object with childs', function() { 25 | expect(a.children[0].props).to.be.an('object'); 26 | expect(a.children[0].tag).to.be.a('string'); 27 | expect(a.children[0].tag).to.be.equal('div'); 28 | expect(a.children[0].children[0].tag).to.be.a('string'); 29 | expect(a.children[0].children[0].tag).to.be.equal('text'); 30 | expect(a.children[0].children[0].children).to.be.a('string'); 31 | }); 32 | 33 | it('should create childrem props', function() { 34 | expect(d.children[0].tag).to.be.equal('span'); 35 | expect(d.children[0].children[0].children).to.be.equal('node-text'); 36 | expect(d.children[0].props.className).to.be.equal('text-node'); 37 | expect(d.children[0].props.id).to.be.equal('text-node'); 38 | }); 39 | 40 | it('should create more compex trees', function() { 41 | var nested; 42 | 43 | expect(function() { 44 | nested = require('../fixtures/nested').a; 45 | }).not.to.throw(); 46 | 47 | expect(nested.tag).to.be.equal('div'); 48 | expect(nested.children).to.be.an('array'); 49 | }); 50 | 51 | it('should create empty text nodes', function() { 52 | var empty = h('div', null, ''); 53 | 54 | expect(empty.tag).to.be.equal('div'); 55 | expect(empty.children[0].tag).to.be.equal('text'); 56 | expect(empty.children[0].children).to.be.equal(''); 57 | expect(empty.children[0].children).to.be.a('string'); 58 | }); 59 | 60 | it('should create nested node text', function() { 61 | var node; 62 | 63 | expect(function() { 64 | node = h('div', null, h('strong', null, ''), 'L', h('strong', null, 'test')); 65 | }).not.throw(); 66 | 67 | expect(node.children.length).to.be.equal(3); 68 | expect(node.children[1].tag).to.be.equal('text'); 69 | expect(node.children[1].children).to.be.equal('L'); 70 | }); 71 | 72 | it('should create nested with text first tree', function() { 73 | var node; 74 | 75 | expect(function() { 76 | node = h('div', null, '', h('strong', null, 'rrr'), h('strong', null, 'test')); 77 | }).not.throw(); 78 | 79 | expect(node.children.length).to.be.equal(3); 80 | expect(node.children[0].tag).to.be.equal('text'); 81 | expect(node.children[0].children).to.be.equal(''); 82 | }); 83 | 84 | it('should handle react like nesting', function() { 85 | var Test = h('div', null, 'text'); 86 | var Result = h('div', null, h(Test, null)); 87 | 88 | expect(Result.children[0].tag).to.be.equal('div'); 89 | expect(Result.children[0].children[0].children).to.be.equal('text'); 90 | }); 91 | 92 | it('should render h with falsy childs', function() { 93 | expect(function() { 94 | h('div', null, null).render(); 95 | h('div', null, 0).render(); 96 | h('div', null, '').render(); 97 | h('div', null, undefined).render(); 98 | }).not.throw(); 99 | 100 | var p = h('div', null, null); 101 | expect(p.children).to.be.an('undefined'); 102 | p = h('div', null, undefined); 103 | expect(p.children).to.be.an('undefined'); 104 | p = h('div', null, ''); 105 | expect(p.children[0].children).to.be.equal(''); 106 | p = h('div', null, 0); 107 | expect(p.children[0].children).to.be.equal('0'); 108 | }); 109 | 110 | it('should render h with nested mixed types', function() { 111 | var p = h('button', null, 'Clicked ', 1, null); 112 | expect(p.children.length).to.be.equal(2); 113 | expect(function() { p.render(); }).not.throw(); 114 | }); 115 | 116 | it('should create boolean props', function() { 117 | var p = h('div', null, h('input', { type: 'checkbox', checked: 'true' })).render(); 118 | expect(p.children[0].getAttribute('checked')).to.be.equal('true'); 119 | var checkbox = p.childNodes[0]; 120 | expect(checkbox.checked).to.be.true; 121 | }); 122 | 123 | it('should create from arrays of childs', function() { 124 | var p = h('div', null , 125 | h('span', null,'child1'), 126 | 'child2', 127 | h('span', null, 'child3'), 128 | [ h('div', null, '1'), h('div', null, '2') ] 129 | ); 130 | 131 | expect(p.children[3].tag).to.be.equal('div'); 132 | }); 133 | 134 | it('should handle nesting functions', function() { 135 | var rendered = sinon.spy(); 136 | var Nested = { 137 | render: function() { 138 | rendered(); 139 | return h('div', null, this.props.name, this.props.children); 140 | } 141 | }; 142 | 143 | var cl = h('div', null, h(Nested, {name: 'hello'}, 'world')); 144 | 145 | expect(rendered.calledOnce).to.be.true; 146 | expect(cl.children[0].children.length).to.be.equal(2); 147 | expect(cl.children[0].children[0].children).to.be.equal('hello'); 148 | expect(cl.children[0].children[1].children).to.be.equal('world'); 149 | }); 150 | 151 | it('should only update props', function() { 152 | var Nested = { 153 | props: { ownProp: 'keepme' }, 154 | render: function() { 155 | return h('div', null, this.props.name, this.props.children, this.props.ownProp); 156 | } 157 | }; 158 | 159 | var cl = h('div', null, h(Nested, {name: 'hello'}, 'world')); 160 | 161 | expect(cl.children[0].children.length).to.be.equal(3); 162 | expect(cl.children[0].children[0].children).to.be.equal('hello'); 163 | expect(cl.children[0].children[1].children).to.be.equal('world'); 164 | expect(cl.children[0].children[2].children).to.be.equal('keepme'); 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /tests/mocha-tests/patch.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | 3 | var jsdom = require('mocha-jsdom'); 4 | 5 | var diff = require('../../').diff; 6 | var patch = require('../../').patch; 7 | var h = require('../../').h; 8 | 9 | var a = require('../fixtures/simple').a; 10 | var b = require('../fixtures/simple').b; 11 | 12 | var a1 = require('../fixtures/simple').a1; 13 | var a2 = require('../fixtures/simple').a2; 14 | 15 | var f = require('../fixtures/simple').f; 16 | var f1 = require('../fixtures/simple').f1; 17 | 18 | var tree1 = require('../fixtures/basic').tree1; 19 | var tree2 = require('../fixtures/basic').tree2; 20 | 21 | describe('Test patch()', function() { 22 | jsdom(); 23 | 24 | it('should patch simple nodes', function() { 25 | var dom = b.render(); 26 | var diffs; 27 | 28 | expect(dom.childNodes[0]).to.be.instanceof(window.Text); 29 | diffs = diff(b, a); 30 | 31 | expect(function() { 32 | patch(b, diffs); 33 | }).not.throw(); 34 | 35 | expect(dom.childNodes[0]).to.be.instanceof(window.HTMLDivElement); 36 | expect(dom.childNodes[0].childNodes[0]).to.be.instanceof(window.Text); 37 | expect(dom.childNodes[0].childNodes[0].textContent) 38 | .to.be.equal('text'); 39 | }); 40 | 41 | it('should patch virtual tree', function() { 42 | a1.render(); 43 | var diffs; 44 | 45 | expect(a1.children).to.be.an('array'); 46 | expect(a1.children[0].children).to.be.a('string'); 47 | expect(a1.children[0].tag).to.be.equal('text'); 48 | 49 | diffs = diff(a1, a2); 50 | patch(a1, diffs); 51 | 52 | expect(a1.children).to.be.an('array'); 53 | expect(a1.children[0].children).to.be.an('array'); 54 | expect(a1.children[0].tag).to.be.equal('div'); 55 | }); 56 | 57 | it('should patch props', function() { 58 | expect(a1.props.className).to.be.equal(a2.props.className); 59 | }); 60 | 61 | it('should reorder nested tree', function() { 62 | f.render(); 63 | 64 | expect(f.children[0].tag).to.be.equal('span'); 65 | expect(f.children[1].tag).to.be.equal('ul'); 66 | 67 | var diffs = diff(f, f1); 68 | patch(f, diffs); 69 | 70 | expect(f.children[1].tag).to.be.equal('span'); 71 | expect(f.children[0].tag).to.be.equal('ul'); 72 | }); 73 | 74 | it('should reorder more complex tree', function() { 75 | tree1.render(); 76 | 77 | expect(tree1.children[0].tag).to.be.equal('em'); 78 | expect(tree1.children[1].tag).to.be.equal('div'); 79 | expect(tree1.children[2].tag).to.be.equal('div'); 80 | expect(tree1.children[3].tag).to.be.equal('span'); 81 | expect(tree1.children[4].tag).to.be.equal('ul'); 82 | 83 | patch(tree1, diff(tree1, tree2)); 84 | 85 | expect(tree1.children[0].tag).to.be.equal('span'); 86 | expect(tree1.children[1].tag).to.be.equal('strong'); 87 | expect(tree1.children[2].tag).to.be.equal('ul'); 88 | expect(tree1.children[3].tag).to.be.equal('div'); 89 | expect(tree1.children[4]).to.be.a('undefined'); 90 | 91 | /* tested reordering */ 92 | expect(tree1.children[2].children[1].children[0].tag).to.be.equal('span'); 93 | expect(tree1.children[2].children[1].children[1].tag).to.be.equal('div'); 94 | expect(tree1.children[2].children[1].children[2]).to.be.a('undefined'); 95 | 96 | }); 97 | 98 | it('should complex tree inners be equal', function() { 99 | expect(tree1.el.innerHTML).to.be.equal(tree2.render().innerHTML); 100 | }); 101 | 102 | it('should replace root node props and text', function() { 103 | var src = h('span', null, 'hello world'); 104 | var dst = h('span', { c : 'e', d : 'z'}, 'bye bye'); 105 | 106 | src.render(); 107 | 108 | expect(src.el.attributes.length).to.be.equal(0); 109 | 110 | patch(src, diff(src, dst)); 111 | 112 | expect(src.children[0].children).to.be.equal('bye bye'); 113 | expect(src.el.attributes['c'].value).to.be.equal('e'); 114 | expect(src.el.attributes['d'].value).to.be.equal('z'); 115 | }); 116 | 117 | it('should remove some props', function() { 118 | var src = h('span', { c : 'e', d : 'z'}, 'hello world'); 119 | var dst = h('span', null, 'bye bye'); 120 | 121 | src.render(); 122 | expect(src.el.attributes.length).to.be.equal(2); 123 | patch(src, diff(src, dst)); 124 | expect(src.el.attributes.length).to.be.equal(0); 125 | }); 126 | 127 | it('should patch boolean props', function() { 128 | var src = h('span', { c : true, d : 'z'}, 'hello world'); 129 | var dst = h('span', { b : true, c : false}, 'bye bye'); 130 | 131 | src.render(); 132 | expect(src.el.attributes.length).to.be.equal(2); 133 | patch(src, diff(src, dst)); 134 | expect(src.el.attributes.length).to.be.equal(2); 135 | expect(src.el.getAttribute('b')).to.be.equal('true'); 136 | expect(src.el.getAttribute('c')).to.be.equal('false'); 137 | }); 138 | 139 | it('should patch empty node', function() { 140 | var t = h('div', null, ''); 141 | var p = h('div', { a : 'b' }, h('span', null, '')); 142 | 143 | t.render(); 144 | patch(t, diff(t, p)); 145 | p.render(); 146 | 147 | expect(t.children.length).to.be.equal(1); 148 | expect(t.children[0].tag).to.be.equal('span'); 149 | expect(t.props.a).to.be.equal('b'); 150 | expect(t.el.getAttribute('a')).to.be.equal('b'); 151 | }); 152 | 153 | it('should patch complex text nodes sequence', function() { 154 | var src = require('../fixtures/textnodes').a; 155 | var dst = require('../fixtures/textnodes').b; 156 | 157 | src.render(); 158 | expect(src.children.length).to.be.equal(1); 159 | patch(src, diff(src, dst)); 160 | expect(src.children.length).to.be.equal(2); 161 | expect(src.children[1].children.length).to.be.equal(5); 162 | }); 163 | 164 | it('should be able patch before render()', function() { 165 | var t = h('div', null, ''); 166 | var p = h('div', { a : 'b' }, h('span', null, 'text')); 167 | 168 | expect(function() { patch(t, diff(t, p)); }).not.throw(); 169 | 170 | expect(t.el).to.be.an('undefined'); 171 | expect(t.children[0].el).to.be.an('undefined'); 172 | expect(t.children[0].children[0].el).to.be.an('undefined'); 173 | expect(t.children[0].children[0].children).to.be.equal('text'); 174 | 175 | t.render(); 176 | 177 | expect(t.el).to.be.instanceof(window.HTMLDivElement); 178 | expect(t.children[0].el).to.be.instanceof(window.HTMLSpanElement); 179 | expect(t.children[0].children[0].el).to.be.instanceof(window.Text); 180 | expect(t.children[0].children[0].children).to.be.equal('text'); 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /tests/mocha-tests/render.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | 3 | var jsdom = require('mocha-jsdom'); 4 | 5 | var a = require('../fixtures/simple').a; 6 | var b = require('../fixtures/simple').b; 7 | var c = require('../fixtures/simple').c; 8 | 9 | var h = require('../../').h; 10 | 11 | describe('Test render()', function() { 12 | jsdom(); 13 | 14 | it('should render simple nodes', function() { 15 | expect(a).to.be.an('object'); 16 | expect(b).to.be.an('object'); 17 | expect(c).to.be.an('object'); 18 | 19 | expect(function() { 20 | a.render(); 21 | b.render(); 22 | c.render(); 23 | }).not.throw(); 24 | }); 25 | 26 | it('shoud render DOM nodes', function() { 27 | var dom = a.render(); 28 | expect(dom).to.be.instanceof(window.HTMLDivElement); 29 | dom = b.render(); 30 | expect(dom).to.be.instanceof(window.HTMLDivElement); 31 | }); 32 | 33 | it('should render text nodes', function() { 34 | var dom = a.render(); 35 | expect(dom.childNodes[0].childNodes[0]).to.be.instanceof(window.Text); 36 | expect(dom.childNodes[0].childNodes[0].textContent).to.be.equal('text'); 37 | }); 38 | 39 | it('should render more complex nodes', function() { 40 | var dom; 41 | 42 | expect(function() { 43 | dom = require('../fixtures/nested').a.render(); 44 | }).not.throw(); 45 | 46 | expect(dom).to.be.instanceof(window.HTMLDivElement); 47 | 48 | expect(dom.childNodes[0]).to.be.instanceof(window.HTMLUListElement); 49 | expect(dom.childNodes[0].childNodes[0]) 50 | .to.be.instanceof(window.HTMLLIElement); 51 | expect(dom.childNodes[0].childNodes[1]) 52 | .to.be.instanceof(window.HTMLLIElement); 53 | expect(dom.childNodes[0].childNodes[2]) 54 | .to.be.instanceof(window.HTMLLIElement); 55 | }); 56 | 57 | it('should render empty text node', function() { 58 | var tree = h('div', { prop1 : 'propval' }, ''); 59 | 60 | expect(function() { tree.render(); }).not.throw(); 61 | }); 62 | 63 | it('should set props', function() { 64 | var tree = h('div', { prop1 : 'propval' }, ''); 65 | expect(tree.props.prop1).to.be.equal('propval'); 66 | tree.render(); 67 | expect(tree.el.getAttribute('prop1')).to.be.equal('propval'); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /tests/mocha-tests/unrefobjects.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var jsdom = require('mocha-jsdom'); 3 | 4 | var h = require('../../').h; 5 | var diff = require('../../').diff; 6 | var patch = require('../../').patch; 7 | 8 | describe('Test extended h { }', function() { 9 | jsdom(); 10 | 11 | it('should clone h { } object', function() { 12 | var a = h('div', {test : '123'}, 'test'); 13 | var b = a.clone(); 14 | 15 | expect(b).not.equal(a); 16 | expect(b).to.be.instanceof(h); 17 | expect(b.props).not.be.equal(a.props); 18 | expect(b.props).not.be.equal(a.props); 19 | expect(b.children).not.be.equal(a.children); 20 | 21 | b.render(); 22 | 23 | expect(a.el).to.be.an('undefined'); 24 | expect(a.children[0].el).to.be.an('undefined'); 25 | }); 26 | 27 | it('should clone references ->', function() { 28 | var refFn = function() {}; 29 | var a = h('div', { onClick : refFn, test : '123' }, 'test'); 30 | var b = a.clone(); 31 | 32 | expect(a).not.equal(b); 33 | expect(a.props).not.equal(b.props); 34 | expect(a.props.onClick).to.equal(b.props.onClick); 35 | }); 36 | 37 | it('should not affect cloned node', function() { 38 | var a = require('../fixtures/nested').a1; 39 | var b = require('../fixtures/nested').b1; 40 | 41 | var c = a.clone(); 42 | 43 | c.render(); 44 | 45 | patch(c, diff(c, b)); 46 | 47 | expect(function() { 48 | 49 | var nextTree = function(a) { 50 | if (typeof a === 'object' && a.el) throw new Error('Element found'); 51 | 52 | if (Array.isArray(a.children)) 53 | for(var i = 0; i < a.children.length; i++) { 54 | nextTree(a.children[i]); 55 | } 56 | }; 57 | 58 | nextTree(a); 59 | nextTree(b); 60 | nextTree(c.clone()); 61 | }).not.throw(); 62 | 63 | }); 64 | }); 65 | 66 | --------------------------------------------------------------------------------