├── .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 | [](https://travis-ci.org/linuxenko/basic-virtual-dom) [](https://coveralls.io/github/linuxenko/basic-virtual-dom?branch=master) [](https://github.com/linuxenko/basic-virtual-dom/) [](https://github.com/linuxenko/basic-virtual-dom) [](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 | [](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 |
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 |
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 |
--------------------------------------------------------------------------------