diff --git a/README.md b/README.md index 81708c8..070972d 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ to proceed with morphing for that branch, otherwise the current fromEl tree is u - **onNodeDiscarded** (`Function(node)`) - Called after a `Node` in the `from` tree has been discarded. - **onBeforeElChildrenUpdated** (`Function(fromEl, toEl)`) - Called before the children of a `HTMLElement` in the `from` tree are updated. If this function returns `false` then the child nodes will not be updated. - **childrenOnly** (`Boolean`) - If `true` then only the children of the `fromNode` and `toNode` nodes will be morphed (the containing element will be skipped). Defaults to `false`. +- **keyedRoot** (`Boolean`) - If `true` then a root node whose key differs from the target root node's key will be replaced instead of updated in place. Matching keyed descendants are preserved. Defaults to `false`. - **skipFromChildren** (`Function(fromEl)`) - called when indexing a the `fromEl` tree. False by default. Return `true` to skip indexing the from tree, which will keep current items in place after patch rather than removing them when not found in the `toEl`. ```javascript @@ -130,6 +131,7 @@ var morphedNode = morphdom(fromNode, toNode, { return true; }, childrenOnly: false, + keyedRoot: false, skipFromChildren: function(fromEl, toEl) { return false; } @@ -262,4 +264,3 @@ npm test # License MIT - diff --git a/index.d.ts b/index.d.ts index 27af78d..0c006d9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -10,6 +10,7 @@ interface MorphDomOptions { skipFromChildren?: (fromEl: HTMLElement) => boolean; addChild?: (parent: HTMLElement, child: HTMLElement) => void; childrenOnly?: boolean; + keyedRoot?: boolean; } declare function morphdom( diff --git a/src/morphdom.js b/src/morphdom.js index bc0ee4a..690fc95 100644 --- a/src/morphdom.js +++ b/src/morphdom.js @@ -55,6 +55,7 @@ export default function morphdomFactory(morphAttrs) { var skipFromChildren = options.skipFromChildren || noop; var addChild = options.addChild || function(parent, child){ return parent.appendChild(child); }; var childrenOnly = options.childrenOnly === true; + var keyedRoot = options.keyedRoot === true; // This object is used as a lookup to quickly find all keyed elements in the original DOM tree. var fromNodesLookup = Object.create(null); @@ -416,6 +417,37 @@ export default function morphdomFactory(morphAttrs) { } } // END: morphChildren(...) + function cleanupKeyedNodes() { + for (var i=0, len=keyedRemovalList.length; inew'); + expect(toRoot.querySelector('#shared')).to.equal(shared); + expect(added.map(function(node) { return node.id; }).filter(Boolean)).to.deep.equal(['new-root', 'new-only']); + expect(updated.map(function(node) { return node.id; }).filter(Boolean)).to.deep.equal(['shared']); + expect(discarded.map(function(node) { return node.id; }).filter(Boolean)).to.deep.equal(['old-root', 'old-only']); + expect(lifecycle).to.deep.equal(['discarded:old-root', 'added:new-root', 'added:new-only', 'discarded:old-only']); + }); + + it('replaces keyed roots with different tag names', function() { + var parent = document.createElement('div'); + var fromRoot = document.createElement('div'); + fromRoot.id = 'old-root'; + parent.appendChild(fromRoot); + + var toRoot = document.createElement('article'); + toRoot.id = 'new-root'; + toRoot.textContent = 'new'; + + var morphedRoot = morphdom(fromRoot, toRoot, {keyedRoot: true}); + + expect(morphedRoot).to.equal(toRoot); + expect(parent.firstChild).to.equal(toRoot); + expect(parent.innerHTML).to.equal('
new
'); + }); + + it('replaces keyed SVG roots', function() { + var fromTemplate = document.createElement('template'); + fromTemplate.innerHTML = 'old'; + var toTemplate = document.createElement('template'); + toTemplate.innerHTML = 'new'; + var parent = document.createElement('div'); + var fromRoot = fromTemplate.content.firstChild; + var toRoot = toTemplate.content.firstChild; + var shared = fromRoot.firstChild; + parent.appendChild(fromRoot); + + var morphedRoot = morphdom(fromRoot, toRoot, {keyedRoot: true}); + + expect(morphedRoot).to.equal(toRoot); + expect(parent.firstChild).to.equal(toRoot); + expect(toRoot.firstChild).to.equal(shared); + expect(shared.textContent).to.equal('new'); + }); + it('nested duplicate ids are morphed correctly', function() { var el1 = document.createElement('div'); el1.innerHTML = '

A

B

';