├── examples
├── ReactDOM.html
├── renderDOM.html
├── arrayString.html
├── treeString.html
├── arrayString.js
├── ReactDOM.js
├── treeString.js
└── renderDOM.js
├── index.js
├── tests
├── index.js
├── array.js
└── tree.js
├── src
├── dom
│ ├── README.md
│ ├── index.js
│ ├── dom.js
│ ├── createDOMNode.js
│ ├── renderDOM.js
│ └── patchDOM.js
├── index.js
├── ChildOperationTypes.js
├── patch.js
└── diff.js
├── HISTORY.md
├── .editorconfig
├── .gitignore
├── .travis.yml
├── package.json
└── README.md
/examples/ReactDOM.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/renderDOM.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/arrayString.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/treeString.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./src/');
2 |
--------------------------------------------------------------------------------
/tests/index.js:
--------------------------------------------------------------------------------
1 | import './array';
2 | import './tree';
3 |
--------------------------------------------------------------------------------
/src/dom/README.md:
--------------------------------------------------------------------------------
1 | ## diff and patch for DOM
2 |
3 | just for demo
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { default as diff } from './diff';
2 | export { default as patch } from './patch';
3 |
--------------------------------------------------------------------------------
/src/dom/index.js:
--------------------------------------------------------------------------------
1 | export { default as dom } from './dom';
2 | export { default as renderDOM } from './renderDOM';
3 |
--------------------------------------------------------------------------------
/HISTORY.md:
--------------------------------------------------------------------------------
1 | # History
2 | ----
3 |
4 | ## 0.2.0 / 2016-08-26
5 |
6 | - change name currentNode -> fromNode, path -> fromPath, nextNode -> afterNode
--------------------------------------------------------------------------------
/src/ChildOperationTypes.js:
--------------------------------------------------------------------------------
1 | export const MOVE = 'move';
2 | export const UPDATE = 'update';
3 | export const REMOVE = 'remove';
4 | export const NEW = 'new';
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # top-most EditorConfig file
2 | root = true
3 |
4 | # Unix-style newlines with a newline ending every file
5 | [*.{js,css}]
6 | end_of_line = lf
7 | insert_final_newline = true
8 | indent_style = space
9 | indent_size = 2
10 |
--------------------------------------------------------------------------------
/src/dom/dom.js:
--------------------------------------------------------------------------------
1 | export default function dom(type, props, ...children) {
2 | const ret = {
3 | type,
4 | props,
5 | children,
6 | };
7 | if (props && props.key) {
8 | ret.key = props.key;
9 | delete props.key;
10 | }
11 | return ret;
12 | }
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | *.log
3 | .idea
4 | .ipr
5 | .iws
6 | *~
7 | ~*
8 | *.diff
9 | *.patch
10 | *.bak
11 | .DS_Store
12 | Thumbs.db
13 | .project
14 | .*proj
15 | .svn
16 | *.swp
17 | *.swo
18 | *.pyc
19 | *.pyo
20 | node_modules
21 | .cache
22 | *.css
23 | build
24 | lib
25 | coverage
26 | typings/
27 | !tests/index.js
--------------------------------------------------------------------------------
/src/dom/createDOMNode.js:
--------------------------------------------------------------------------------
1 | function createDOMNode(vdom, doc = document) {
2 | if (typeof vdom === 'string') {
3 | return doc.createTextNode(vdom);
4 | }
5 | const { type, props } = vdom;
6 | const node = doc.createElement(type);
7 | for (const name in props) {
8 | if (props.hasOwnProperty(name)) {
9 | node.setAttribute(name, props[name]);
10 | }
11 | }
12 | if (vdom.children) {
13 | vdom.children.forEach((c) => {
14 | const child = createDOMNode(c, doc);
15 | node.appendChild(child);
16 | });
17 | }
18 | return node;
19 | }
20 |
21 | export default createDOMNode;
22 |
--------------------------------------------------------------------------------
/examples/arrayString.js:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 |
3 | import { diff, patch } from 'tree-diff';
4 |
5 | const a = ['1', '2', '3'];
6 | const b = ['4', '3', '1', '2'];
7 |
8 | const operations = diff(a, b);
9 |
10 | console.log('operations', operations);
11 |
12 | patch(operations, {
13 | processNew(q) {
14 | a.splice(q.toIndex, 0, q.afterNode);
15 | },
16 | processRemove(q) {
17 | const r = a[q.fromIndex];
18 | a.splice(q.fromIndex, 1);
19 | return r;
20 | },
21 | processUpdate() {
22 | },
23 | processMove(q, r) {
24 | a.splice(q.toIndex, 0, r);
25 | },
26 | });
27 |
28 | console.log(a, b);
29 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | sudo: false
4 |
5 | notifications:
6 | email:
7 | - yiminghe@gmail.com
8 |
9 | node_js:
10 | - 4.0.0
11 |
12 | before_install:
13 | - |
14 | if ! git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '(\.md$)|(^(docs|examples))/'
15 | then
16 | echo "Only docs were updated, stopping build process."
17 | exit
18 | fi
19 | npm install npm@3.x -g
20 | phantomjs --version
21 | script:
22 | - |
23 | if [ "$TEST_TYPE" = test ]; then
24 | npm test
25 | else
26 | npm run $TEST_TYPE
27 | fi
28 | env:
29 | matrix:
30 | - TEST_TYPE=lint
31 | - TEST_TYPE=test
32 | - TEST_TYPE=coverage
33 |
--------------------------------------------------------------------------------
/src/patch.js:
--------------------------------------------------------------------------------
1 | import { MOVE, NEW } from './ChildOperationTypes';
2 |
3 | function patch({ removeQueue, insertQueue, updateQueue }, {
4 | processNew,
5 | processUpdate,
6 | processMove,
7 | processRemove,
8 | }) {
9 | updateQueue.forEach((q) => {
10 | processUpdate(q);
11 | });
12 |
13 | const moves = {};
14 |
15 | removeQueue.forEach((q) => {
16 | const ret = processRemove(q);
17 | if (q.type === MOVE) {
18 | moves[q.toPath.join(',')] = ret;
19 | }
20 | });
21 |
22 | insertQueue.forEach((q) => {
23 | if (q.type === NEW) {
24 | processNew(q);
25 | } else if (q.type === MOVE) {
26 | processMove(q, moves[q.toPath.join(',')]);
27 | }
28 | });
29 | }
30 |
31 | export default patch;
32 |
--------------------------------------------------------------------------------
/examples/ReactDOM.js:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 |
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 |
6 | const Wrap = React.createClass({
7 | propTypes: {
8 | component: React.PropTypes.node,
9 | },
10 | componentDidMount() {
11 | console.log('componentDidMount', this.props.component);
12 | },
13 | componentWillUnmount() {
14 | console.log('componentWillUnmount', this.props.component);
15 | },
16 | render() {
17 | return this.props.component;
18 | },
19 | });
20 |
21 | const vdom1 = (
22 | 1}/>
23 | 2}/>
24 | change}/>
25 |
);
26 |
27 | ReactDOM.render(vdom1, document.getElementById('__react-content'));
28 |
29 | document.getElementById('t').onclick = () => {
30 | ReactDOM.render(
31 | 2}/>
32 | 1}/>
33 | 3}/>
34 | change}/>
35 |
, document.getElementById('__react-content'));
36 | };
37 |
--------------------------------------------------------------------------------
/examples/treeString.js:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 |
3 | import { diff, patch } from 'tree-diff';
4 |
5 | const a = [{ value: '1' }, { value: '2', children: ['1', '2', '3'] }, { value: '3' }];
6 | const b = [{ value: '4' }, { value: '3' }, { value: '1' },
7 | { value: '2', children: ['4', '3', '1', '2'] }];
8 |
9 | const operations = diff(a, b, {
10 | shouldUpdate(v1, v2) {
11 | if (v1.value && v2.value) {
12 | return v1.value === v2.value;
13 | }
14 | return v1 === v2;
15 | },
16 | });
17 |
18 | console.log('operations', operations);
19 |
20 | function getArray(q, ensure = true) {
21 | let array;
22 | if (q.parentNode) {
23 | array = q.parentNode.children;
24 | if (ensure) {
25 | array = q.parentNode.children = q.parentNode.children || [];
26 | }
27 | } else {
28 | array = a;
29 | }
30 | return array;
31 | }
32 |
33 | patch(operations, {
34 | processNew(q) {
35 | getArray(q).splice(q.toIndex, 0, q.afterNode);
36 | },
37 | processRemove(q) {
38 | const arr = getArray(q);
39 | const r = arr[q.fromIndex];
40 | arr.splice(q.fromIndex, 1);
41 | return r;
42 | },
43 | processUpdate() {
44 | },
45 | processMove(q, r) {
46 | getArray(q).splice(q.toIndex, 0, r);
47 | },
48 | });
49 |
50 | console.log(a, b);
51 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tree-diff",
3 | "version": "0.2.0",
4 | "description": "diff tree node like React",
5 | "keywords": [
6 | "tree diff",
7 | "algorithm"
8 | ],
9 | "engines": {
10 | "node": ">=4.0.0"
11 | },
12 | "homepage": "https://github.com/yiminghe/tree-diff",
13 | "author": "yiminghe@gmail.com",
14 | "repository": "yiminghe/tree-diff",
15 | "bugs": "https://github.com/yiminghe/tree-diff/issues",
16 | "files": [
17 | "lib"
18 | ],
19 | "license": "MIT",
20 | "main": "./lib/index",
21 | "config": {
22 | "port": 8000
23 | },
24 | "scripts": {
25 | "watch-tsc": "rc-tools run watch-tsc",
26 | "build": "rc-tools run build",
27 | "gh-pages": "rc-tools run gh-pages",
28 | "start": "rc-tools run server",
29 | "pub": "rc-tools run pub",
30 | "lint": "rc-tools run lint",
31 | "karma": "rc-tools run karma",
32 | "saucelabs": "rc-tools run saucelabs",
33 | "test": "rc-tools run test",
34 | "chrome-test": "rc-tools run chrome-test",
35 | "coverage": "rc-tools run coverage"
36 | },
37 | "dependencies": {
38 | },
39 | "devDependencies": {
40 | "expect.js": "0.3.x",
41 | "react": "15.x",
42 | "react-dom": "15.x",
43 | "pre-commit": "1.x",
44 | "rc-tools": "5.x"
45 | },
46 | "pre-commit": [
47 | "lint"
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/src/dom/renderDOM.js:
--------------------------------------------------------------------------------
1 | import diff from '../diff';
2 | import createDOMNode from './createDOMNode';
3 | import patchDOM from './patchDOM';
4 |
5 | function getDefaultKeyFromIndex(index) {
6 | return `.${index}`;
7 | }
8 |
9 | function shouldUpdate(prevElement, nextElement, prevElementIndex, nextElementIndex) {
10 | const prevEmpty = prevElement === null || prevElement === false;
11 | const nextEmpty = nextElement === null || nextElement === false;
12 | if (prevEmpty || nextEmpty) {
13 | return prevEmpty === nextEmpty;
14 | }
15 |
16 | const prevType = typeof prevElement;
17 | const nextType = typeof nextElement;
18 | if (prevType === 'string' || prevType === 'number') {
19 | return (nextType === 'string' || nextType === 'number');
20 | }
21 | return (
22 | nextType === 'object' &&
23 | prevElement.type === nextElement.type &&
24 | (prevElement.key || getDefaultKeyFromIndex(prevElementIndex))
25 | === (nextElement.key || getDefaultKeyFromIndex(nextElementIndex))
26 | );
27 | }
28 |
29 | function renderDOM(vdom_, root) {
30 | let vdom = vdom_;
31 | if (!Array.isArray(vdom_)) {
32 | vdom = [vdom_];
33 | }
34 | vdom.forEach((n) => {
35 | const c = createDOMNode(n);
36 | root.appendChild(c);
37 | });
38 | let currentVDom = vdom;
39 | return {
40 | update(nextVDom_) {
41 | let nextVDom = nextVDom_;
42 | if (!Array.isArray(nextVDom)) {
43 | nextVDom = [nextVDom];
44 | }
45 | const queue = diff(currentVDom, nextVDom, { shouldUpdate });
46 | currentVDom = nextVDom;
47 | patchDOM(queue, root);
48 | return queue;
49 | },
50 | };
51 | }
52 |
53 | export default renderDOM;
54 |
--------------------------------------------------------------------------------
/examples/renderDOM.js:
--------------------------------------------------------------------------------
1 | /** @jsx dom */
2 |
3 | /* eslint no-console:0 */
4 |
5 | import { dom, renderDOM } from 'tree-diff/src/dom/';
6 |
7 | function simplifyOperations(operations) {
8 | return JSON.parse(JSON.stringify(operations, (k, v) => {
9 | return k === 'children' || k === 'props' ? undefined : v;
10 | }));
11 | }
12 |
13 | (() => {
14 | const vdom1 = (
15 |
no key
16 |
1
17 |
2
18 |
19 |
);
20 | const container = document.createElement('div');
21 | document.getElementById('__react-content').appendChild(container);
22 |
23 | const { update } = renderDOM(vdom1, container);
24 |
25 | document.getElementById('t').onclick = () => {
26 | const operations = update(
27 |
no key
28 |
2
29 |
1
30 |
3
31 |
32 |
);
33 | console.log('operations', (simplifyOperations(operations)));
34 | };
35 | })();
36 |
37 | (() => {
38 | const vdom1 = (
39 |
with key
40 |
1
41 |
2
42 |
43 |
);
44 | const container = document.createElement('div');
45 | document.getElementById('__react-content').appendChild(container);
46 |
47 | const { update } = renderDOM(vdom1, container);
48 |
49 | document.getElementById('t2').onclick = () => {
50 | const operations = update(
51 |
with key
52 |
2
53 |
1
54 |
3
55 |
56 |
);
57 |
58 | console.log('operations', (simplifyOperations(operations)));
59 | };
60 | })();
61 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # tree-diff
2 |
3 | diff tree nodes like React.
4 |
5 | [![NPM version][npm-image]][npm-url]
6 | [![build status][travis-image]][travis-url]
7 | [![Test coverage][coveralls-image]][coveralls-url]
8 | [![gemnasium deps][gemnasium-image]][gemnasium-url]
9 | [![npm download][download-image]][download-url]
10 |
11 | [npm-image]: http://img.shields.io/npm/v/tree-diff.svg?style=flat-square
12 | [npm-url]: http://npmjs.org/package/tree-diff
13 | [travis-image]: https://img.shields.io/travis/yiminghe/tree-diff.svg?style=flat-square
14 | [travis-url]: https://travis-ci.org/yiminghe/tree-diff
15 | [coveralls-image]: https://img.shields.io/coveralls/yiminghe/tree-diff.svg?style=flat-square
16 | [coveralls-url]: https://coveralls.io/r/yiminghe/tree-diff?branch=master
17 | [gemnasium-image]: http://img.shields.io/gemnasium/yiminghe/tree-diff.svg?style=flat-square
18 | [gemnasium-url]: https://gemnasium.com/yiminghe/tree-diff
19 | [node-image]: https://img.shields.io/badge/node.js-%3E=_0.10-green.svg?style=flat-square
20 | [node-url]: http://nodejs.org/download/
21 | [download-image]: https://img.shields.io/npm/dm/tree-diff.svg?style=flat-square
22 | [download-url]: https://npmjs.org/package/tree-diff
23 |
24 | ## Demo
25 |
26 | http://yiminghe.github.io/tree-diff
27 |
28 | ## Api
29 |
30 | ### diff(fromNodes: any[], afterNodes : any[], options = {}): Patch
31 |
32 | #### options.shouldUpdate(node1, node2, node1Index, node2Index)
33 |
34 | decide whether change node1 to node2 or detroy node1 and recreate node2
35 |
36 | #### options.childrenKey="children"
37 |
38 | children member name if node type is object
39 |
40 | ### patch(patcher: Patcher, operations)
41 |
42 | for example: src/dom/patchDOM.js
43 |
44 | #### operations.processNew
45 |
46 | how to process new node
47 |
48 | #### operations.processRemove
49 |
50 | how to process node removal
51 |
52 | #### operations.processMove
53 |
54 | how to process node move
55 |
56 | #### operations.processUpdate
57 |
58 | how to process node update
--------------------------------------------------------------------------------
/src/dom/patchDOM.js:
--------------------------------------------------------------------------------
1 | import patch from '../patch';
2 | import createDOMNode from './createDOMNode';
3 |
4 | function findNodeByPath(root, fromPath) {
5 | let parent = root;
6 | const node = fromPath.reduce(
7 | (n, p) => {
8 | parent = n;
9 | return n.childNodes[p];
10 | },
11 | root) || null;
12 | return {
13 | node,
14 | parent,
15 | };
16 | }
17 |
18 | function processNew(root, q) {
19 | const { afterNode, toPath } = q;
20 | const newNode = createDOMNode(afterNode);
21 | const { parent, node } = findNodeByPath(root, toPath);
22 | parent.insertBefore(newNode, node);
23 | }
24 |
25 | function processMove(_, q, { parent, node }) {
26 | const { toPath } = q;
27 | parent.insertBefore(node, parent.childNodes[toPath[toPath.length - 1]] || null);
28 | }
29 |
30 | function processRemove(root, q) {
31 | const { fromPath } = q;
32 | const { parent, node } = findNodeByPath(root, fromPath);
33 | parent.removeChild(node);
34 | return { parent, node };
35 | }
36 |
37 | function processUpdate(root, q) {
38 | const { fromPath, fromNode, afterNode } = q;
39 | const { node } = findNodeByPath(root, fromPath);
40 | if (typeof afterNode === 'string') {
41 | if (afterNode !== fromNode) {
42 | node.nodeValue = afterNode;
43 | }
44 | return;
45 | }
46 | const currentProps = fromNode.props;
47 | const nextProps = afterNode.props;
48 | for (const nextName in nextProps) {
49 | if (nextProps.hasOwnProperty(nextName)) {
50 | const nextValue = nextProps[nextName];
51 | const currentValue = currentProps[nextName];
52 | if (nextValue !== currentValue) {
53 | node.setAttribute(nextName, nextValue);
54 | }
55 | }
56 | }
57 | for (const currentName in currentProps) {
58 | if (currentProps.hasOwnProperty(currentName) && !nextProps.hasOwnProperty(currentName)) {
59 | node.removeAttribute(currentName);
60 | }
61 | }
62 | }
63 |
64 | function patchDOM(queue, root) {
65 | patch(queue, {
66 | processNew: processNew.bind(null, root),
67 | processMove: processMove.bind(null, root),
68 | processUpdate: processUpdate.bind(null, root),
69 | processRemove: processRemove.bind(null, root),
70 | });
71 | }
72 |
73 | export default patchDOM;
74 |
--------------------------------------------------------------------------------
/tests/array.js:
--------------------------------------------------------------------------------
1 | /* eslint quotes:0, quote-props:0, comma-dangle:0 */
2 |
3 | import expect from 'expect.js';
4 | import { diff, patch } from 'tree-diff';
5 |
6 | describe('array-diff', () => {
7 | function simplifyOperations(operations) {
8 | return JSON.parse(JSON.stringify(operations, (k, v) => {
9 | return k === 'children' || k === 'props' ||
10 | k === 'fromIndex' || k === 'toIndex' ? undefined : v;
11 | }));
12 | }
13 |
14 |
15 | it('works for array', () => {
16 | const a = ['1', '2', '3'];
17 | const b = ['4', '3', '1', '2'];
18 |
19 | const operations = diff(a, b);
20 |
21 | expect(simplifyOperations(operations)).to.eql(
22 | {
23 | 'insertQueue': [
24 | { 'type': 'new', 'afterNode': '4', 'toPath': [0] },
25 | {
26 | 'type': 'move',
27 | 'fromNode': '1',
28 | 'afterNode': '1',
29 | 'fromPath': [0],
30 | 'toPath': [2]
31 | },
32 | {
33 | 'type': 'move',
34 | 'fromNode': '2',
35 | 'afterNode': '2',
36 | 'fromPath': [1],
37 | 'toPath': [3]
38 | }
39 | ],
40 | 'updateQueue': [
41 | {
42 | 'type': 'update',
43 | 'fromNode': '3',
44 | 'afterNode': '3',
45 | 'fromPath': [2]
46 | },
47 | {
48 | 'type': 'update',
49 | 'fromNode': '1',
50 | 'afterNode': '1',
51 | 'fromPath': [0]
52 | },
53 | {
54 | 'type': 'update',
55 | 'fromNode': '2',
56 | 'afterNode': '2',
57 | 'fromPath': [1]
58 | }
59 | ],
60 | 'removeQueue': [
61 | {
62 | 'type': 'move',
63 | 'fromNode': '2',
64 | 'afterNode': '2',
65 | 'fromPath': [1],
66 | 'toPath': [3]
67 | },
68 | {
69 | 'type': 'move',
70 | 'fromNode': '1',
71 | 'afterNode': '1',
72 | 'fromPath': [0],
73 | 'toPath': [2]
74 | }
75 | ]
76 | }
77 | );
78 |
79 | patch(operations, {
80 | processNew(q) {
81 | a.splice(q.toPath[0], 0, q.afterNode);
82 | },
83 | processRemove(q) {
84 | const r = a[q.fromPath[0]];
85 | a.splice(q.fromPath[0], 1);
86 | return r;
87 | },
88 | processUpdate() {
89 | },
90 | processMove(q, r) {
91 | a.splice(q.toPath[0], 0, r);
92 | }
93 | });
94 |
95 | expect(a).to.eql(b);
96 | });
97 | });
98 |
--------------------------------------------------------------------------------
/src/diff.js:
--------------------------------------------------------------------------------
1 | import { UPDATE, MOVE, REMOVE, NEW } from './ChildOperationTypes';
2 |
3 | function indexOf(nodes, node, isSame, nodeIndex) {
4 | const len = nodes.length;
5 | for (let i = 0; i < len; i++) {
6 | if (isSame(node, nodes[i], nodeIndex, i)) {
7 | return i;
8 | }
9 | }
10 | return -1;
11 | }
12 |
13 | function nativeShould(a, b) {
14 | return a === b;
15 | }
16 |
17 | function sortByIndex(a, b) {
18 | if (a.fromIndex === b.fromIndex) {
19 | return 0;
20 | }
21 | return a.fromIndex > b.fromIndex ? -1 : 1;
22 | }
23 |
24 | // diff by level
25 | function diff(fromNodes, afterNodes, options = {}, internal = {}) {
26 | const { shouldUpdate = nativeShould, childrenKey = 'children' } = options;
27 | const { fromPath = [], parentNode } = internal;
28 | let insertQueue = [];
29 | let updateQueue = [];
30 | let removeQueue = [];
31 | let lastIndex = 0;
32 | let tmp;
33 | afterNodes.forEach((afterNode, toIndex) => {
34 | const fromIndex = indexOf(fromNodes, afterNode, shouldUpdate, toIndex);
35 | if (fromIndex !== -1) {
36 | const fromNode = fromNodes[fromIndex];
37 | updateQueue.push({
38 | type: UPDATE,
39 | fromNode,
40 | afterNode,
41 | parentNode,
42 | fromIndex,
43 | fromPath: fromPath.concat(fromIndex),
44 | });
45 | if (fromIndex < lastIndex) {
46 | tmp = {
47 | type: MOVE,
48 | fromNode,
49 | afterNode,
50 | parentNode,
51 | fromIndex,
52 | toIndex,
53 | fromPath: fromPath.concat(fromIndex),
54 | toPath: fromPath.concat(toIndex),
55 | };
56 | insertQueue.push(tmp);
57 | removeQueue.push(tmp);
58 | }
59 | lastIndex = Math.max(fromIndex, lastIndex);
60 | } else {
61 | insertQueue.push({
62 | type: NEW,
63 | afterNode,
64 | parentNode,
65 | toIndex,
66 | toPath: fromPath.concat(toIndex),
67 | });
68 | }
69 | });
70 |
71 | fromNodes.forEach((fromNode, fromIndex) => {
72 | const toIndex = indexOf(afterNodes, fromNode, shouldUpdate, fromIndex);
73 | if (toIndex === -1) {
74 | removeQueue.push({
75 | type: REMOVE,
76 | fromNode,
77 | parentNode,
78 | fromIndex,
79 | fromPath: fromPath.concat(fromIndex),
80 | });
81 | }
82 | });
83 |
84 | removeQueue.sort(sortByIndex);
85 |
86 | if (childrenKey) {
87 | updateQueue.concat().forEach((o) => {
88 | const currentChildren = o.fromNode[childrenKey] || [];
89 | const nextChildren = o.afterNode[childrenKey] || [];
90 | // bottom up
91 | const ret = diff(currentChildren, nextChildren, options, {
92 | fromPath: o.fromPath,
93 | parentNode: o.fromNode,
94 | });
95 | insertQueue = ret.insertQueue.concat(insertQueue);
96 | updateQueue = ret.updateQueue.concat(updateQueue);
97 | removeQueue = ret.removeQueue.concat(removeQueue);
98 | });
99 | }
100 |
101 | return {
102 | insertQueue,
103 | updateQueue,
104 | removeQueue,
105 | };
106 | }
107 |
108 | export default function diffTree(fromNodes, afterNodes, options) {
109 | return diff(fromNodes, afterNodes, options);
110 | }
111 |
--------------------------------------------------------------------------------
/tests/tree.js:
--------------------------------------------------------------------------------
1 | /** @jsx dom */
2 |
3 | /* eslint quotes:0, quote-props:0, comma-dangle:0 */
4 |
5 | import expect from 'expect.js';
6 | import { dom, renderDOM } from '../src/dom/';
7 |
8 | function simplifyOperations(operations) {
9 | return JSON.parse(JSON.stringify(operations, (k, v) => {
10 | return k === 'children' || k === 'props' ||
11 | k === 'fromIndex' || k === 'toIndex' ? undefined : v;
12 | }));
13 | }
14 |
15 | describe('tree-diff', () => {
16 | it('works without key', () => {
17 | const vdom1 = (
18 |
no key
19 |
1
20 |
2
21 |
22 |
);
23 | const container = document.createElement('div');
24 | const { update } = renderDOM(vdom1, container);
25 | const operations = update(
26 |
no key
27 |
2
28 |
1
29 |
3
30 |
31 |
);
32 | const containerChildNodes = container.childNodes[0].childNodes;
33 | expect(containerChildNodes.length).to.be(5);
34 | expect(containerChildNodes[0].nodeName.toLowerCase()).to.be('h2');
35 | expect(containerChildNodes[1].id).to.be('2');
36 | expect(containerChildNodes[2].id).to.be('1');
37 | expect(containerChildNodes[3].id).to.be('3');
38 | expect(containerChildNodes[4].nodeName.toLowerCase()).to.be('button');
39 | expect(simplifyOperations(operations)).to.eql({
40 | 'insertQueue': [
41 | {
42 | 'type': 'new',
43 | 'afterNode': { 'type': 'p' },
44 | 'parentNode': { 'type': 'div' },
45 | 'toPath': [0, 3]
46 | },
47 | {
48 | 'type': 'new',
49 | 'afterNode': { 'type': 'button' },
50 | 'parentNode': { 'type': 'div' },
51 | 'toPath': [0, 4]
52 | }
53 | ],
54 | 'updateQueue': [
55 | {
56 | 'type': 'update',
57 | 'fromNode': '2',
58 | 'afterNode': '1',
59 | 'parentNode': { 'type': 'p' },
60 | 'fromPath': [0, 2, 0]
61 | },
62 | {
63 | 'type': 'update',
64 | 'fromNode': '1',
65 | 'afterNode': '2',
66 | 'parentNode': { 'type': 'p' },
67 | 'fromPath': [0, 1, 0]
68 | },
69 | {
70 | 'type': 'update',
71 | 'fromNode': 'no key',
72 | 'afterNode': 'no key',
73 | 'parentNode': { 'type': 'h2' },
74 | 'fromPath': [0, 0, 0]
75 | },
76 | {
77 | 'type': 'update',
78 | 'fromNode': { 'type': 'h2' },
79 | 'afterNode': { 'type': 'h2' },
80 | 'parentNode': { 'type': 'div' },
81 | 'fromPath': [0, 0]
82 | },
83 | {
84 | 'type': 'update',
85 | 'fromNode': { 'type': 'p' },
86 | 'afterNode': { 'type': 'p' },
87 | 'parentNode': { 'type': 'div' },
88 | 'fromPath': [0, 1]
89 | },
90 | {
91 | 'type': 'update',
92 | 'fromNode': { 'type': 'p' },
93 | 'afterNode': { 'type': 'p' },
94 | 'parentNode': { 'type': 'div' },
95 | 'fromPath': [0, 2]
96 | },
97 | {
98 | 'type': 'update',
99 | 'fromNode': { 'type': 'div' },
100 | 'afterNode': { 'type': 'div' },
101 | 'fromPath': [0]
102 | }
103 | ],
104 | 'removeQueue': [
105 | {
106 | 'type': 'remove',
107 | 'fromNode': { 'type': 'button' },
108 | 'parentNode': { 'type': 'div' },
109 | 'fromPath': [0, 3]
110 | }
111 | ]
112 | });
113 | });
114 |
115 | it('works with key', () => {
116 | const vdom1 = (
117 |
with key
118 |
1
119 |
2
120 |
121 |
);
122 | const container = document.createElement('div');
123 | const { update } = renderDOM(vdom1, container);
124 | const operations = update(
125 |
with key
126 |
2
127 |
1
128 |
3
129 |
130 |
);
131 | const containerChildNodes = container.childNodes[0].childNodes;
132 | expect(containerChildNodes.length).to.be(5);
133 | expect(containerChildNodes[0].nodeName.toLowerCase()).to.be('h2');
134 | expect(containerChildNodes[1].textContent).to.be('2');
135 | expect(containerChildNodes[2].textContent).to.be('1');
136 | expect(containerChildNodes[3].textContent).to.be('3');
137 | expect(containerChildNodes[4].nodeName.toLowerCase()).to.be('button');
138 | expect(simplifyOperations(operations)).to.eql(
139 | {
140 | 'insertQueue': [
141 | {
142 | 'type': 'move',
143 | 'fromNode': { 'type': 'p', 'key': '1' },
144 | 'afterNode': { 'type': 'p', 'key': '1' },
145 | 'parentNode': { 'type': 'div' },
146 | 'fromPath': [0, 1],
147 | 'toPath': [0, 2]
148 | },
149 | {
150 | 'type': 'new',
151 | 'afterNode': { 'type': 'p', 'key': '3' },
152 | 'parentNode': { 'type': 'div' },
153 | 'toPath': [0, 3]
154 | }
155 | ],
156 | 'updateQueue': [
157 | {
158 | 'type': 'update',
159 | 'fromNode': 'change',
160 | 'afterNode': 'change',
161 | 'parentNode': { 'type': 'button', 'key': 't' },
162 | 'fromPath': [0, 3, 0]
163 | },
164 | {
165 | 'type': 'update',
166 | 'fromNode': '1',
167 | 'afterNode': '1',
168 | 'parentNode': { 'type': 'p', 'key': '1' },
169 | 'fromPath': [0, 1, 0]
170 | },
171 | {
172 | 'type': 'update',
173 | 'fromNode': '2',
174 | 'afterNode': '2',
175 | 'parentNode': { 'type': 'p', 'key': '2' },
176 | 'fromPath': [0, 2, 0]
177 | },
178 | {
179 | 'type': 'update',
180 | 'fromNode': 'with key',
181 | 'afterNode': 'with key',
182 | 'parentNode': { 'type': 'h2' },
183 | 'fromPath': [0, 0, 0]
184 | },
185 | {
186 | 'type': 'update',
187 | 'fromNode': { 'type': 'h2' },
188 | 'afterNode': { 'type': 'h2' },
189 | 'parentNode': { 'type': 'div' },
190 | 'fromPath': [0, 0]
191 | },
192 | {
193 | 'type': 'update',
194 | 'fromNode': { 'type': 'p', 'key': '2' },
195 | 'afterNode': { 'type': 'p', 'key': '2' },
196 | 'parentNode': { 'type': 'div' },
197 | 'fromPath': [0, 2]
198 | },
199 | {
200 | 'type': 'update',
201 | 'fromNode': { 'type': 'p', 'key': '1' },
202 | 'afterNode': { 'type': 'p', 'key': '1' },
203 | 'parentNode': { 'type': 'div' },
204 | 'fromPath': [0, 1]
205 | },
206 | {
207 | 'type': 'update',
208 | 'fromNode': { 'type': 'button', 'key': 't' },
209 | 'afterNode': { 'type': 'button', 'key': 't' },
210 | 'parentNode': { 'type': 'div' },
211 | 'fromPath': [0, 3]
212 | },
213 | {
214 | 'type': 'update',
215 | 'fromNode': { 'type': 'div' },
216 | 'afterNode': { 'type': 'div' },
217 | 'fromPath': [0]
218 | }
219 | ],
220 | 'removeQueue': [
221 | {
222 | 'type': 'move',
223 | 'fromNode': { 'type': 'p', 'key': '1' },
224 | 'afterNode': { 'type': 'p', 'key': '1' },
225 | 'parentNode': { 'type': 'div' },
226 | 'fromPath': [0, 1],
227 | 'toPath': [0, 2]
228 | }
229 | ]
230 | }
231 | );
232 | });
233 | });
234 |
--------------------------------------------------------------------------------