├── 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 | --------------------------------------------------------------------------------