diff --git a/CHANGELOG.md b/CHANGELOG.md index a66f9f8..7b003f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ Changelog ## 2.0.x +### 2.0.2 + +- Fixed [#78](https://github.com/patrick-steele-idem/morphdom/issues/78) - Elements under `onBeforeElChildrenUpdated` element removed if they have `id` set + +### 2.0.1 + +- Small optimization and more tests + ### 2.0.0 - Fixed [#47](https://github.com/patrick-steele-idem/morphdom/issues/47) - Detect and handle reorder of siblings diff --git a/src/index.js b/src/index.js index 9dcb276..42a4411 100644 --- a/src/index.js +++ b/src/index.js @@ -237,18 +237,34 @@ function morphdom(fromNode, toNode, options) { // This object is used as a lookup to quickly find all keyed elements in the original DOM tree. var fromNodesLookup = {}; + var keyedRemovalList; - function walkDiscardedChildNodes(node) { + function addKeyedRemoval(key) { + if (keyedRemovalList) { + keyedRemovalList.push(key); + } else { + keyedRemovalList = [key]; + } + } + + function walkDiscardedChildNodes(node, skipKeyedNodes) { if (node.nodeType === ELEMENT_NODE) { var curChild = node.firstChild; while (curChild) { - if (!getNodeKey(curChild)) { + + var key = undefined; + + if (skipKeyedNodes && (key = getNodeKey(curChild))) { + // If we are skipping keyed nodes then we add the key + // to a list so that it can be handled at the very end. + addKeyedRemoval(key); + } else { // Only report the node as discarded if it is not keyed. We do this because // at the end we loop through all keyed elements that were unmatched // and then discard them in one final pass. onNodeDiscarded(curChild); if (curChild.firstChild) { - walkDiscardedChildNodes(curChild); + walkDiscardedChildNodes(curChild, skipKeyedNodes); } } @@ -257,7 +273,15 @@ function morphdom(fromNode, toNode, options) { } } - function removeNode(node, parentNode) { + /** + * Removes a DOM node out of the original DOM + * + * @param {Node} node The node to remove + * @param {Node} parentNode The nodes parent + * @param {Boolean} skipKeyedNodes If true then elements with keys will be skipped and not discarded. + * @return {undefined} + */ + function removeNode(node, parentNode, skipKeyedNodes) { if (onBeforeNodeDiscarded(node) === false) { return; } @@ -267,7 +291,7 @@ function morphdom(fromNode, toNode, options) { } onNodeDiscarded(node); - walkDiscardedChildNodes(node); + walkDiscardedChildNodes(node, skipKeyedNodes); } // // TreeWalker implementation is no faster, but keeping this around in case this changes in the future @@ -340,6 +364,8 @@ function morphdom(fromNode, toNode, options) { function morphEl(fromEl, toEl, childrenOnly) { var toElKey = getNodeKey(toEl); + var curFromNodeKey; + if (toElKey) { // If an element with an ID is being morphed then it is will be in the final // DOM so clear it out of the saved elements collection @@ -373,7 +399,7 @@ function morphdom(fromNode, toNode, options) { curToNodeKey = getNodeKey(curToNodeChild); while (curFromNodeChild) { - var curFromNodeKey = getNodeKey(curFromNodeChild); + curFromNodeKey = getNodeKey(curFromNodeChild); fromNextSibling = curFromNodeChild.nextSibling; var curFromNodeType = curFromNodeChild.nodeType; @@ -409,8 +435,15 @@ function morphdom(fromNode, toNode, options) { // all lifecycle hooks are correctly invoked fromEl.insertBefore(matchingFromEl, curFromNodeChild); - if (!curFromNodeKey) { - removeNode(curFromNodeChild, fromEl); + if (curFromNodeKey) { + // Since the node is keyed it might be matched up later so we defer + // the actual removal to later + addKeyedRemoval(curFromNodeKey); + } else { + // NOTE: we skip nested keyed nodes from being removed since there is + // still a chance they will be matched up later + removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */); + } fromNextSibling = curFromNodeChild.nextSibling; curFromNodeChild = matchingFromEl; @@ -456,8 +489,14 @@ function morphdom(fromNode, toNode, options) { // target tree and we don't want to discard it just yet since it still might find a // home in the final DOM tree. After everything is done we will remove any keyed nodes // that didn't find a home - if (!curFromNodeKey) { - removeNode(curFromNodeChild, fromEl); + if (curFromNodeKey) { + // Since the node is keyed it might be matched up later so we defer + // the actual removal to later + addKeyedRemoval(curFromNodeKey); + } else { + // NOTE: we skip nested keyed nodes from being removed since there is + // still a chance they will be matched up later + removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */); } curFromNodeChild = fromNextSibling; @@ -486,8 +525,14 @@ function morphdom(fromNode, toNode, options) { // to be removed while (curFromNodeChild) { fromNextSibling = curFromNodeChild.nextSibling; - if (!getNodeKey(curFromNodeChild)) { - removeNode(curFromNodeChild, fromEl); + if ((curFromNodeKey = getNodeKey(curFromNodeChild))) { + // Since the node is keyed it might be matched up later so we defer + // the actual removal to later + addKeyedRemoval(curFromNodeKey); + } else { + // NOTE: we skip nested keyed nodes from being removed since there is + // still a chance they will be matched up later + removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */); } curFromNodeChild = fromNextSibling; } @@ -534,10 +579,17 @@ function morphdom(fromNode, toNode, options) { } else { morphEl(morphedNode, toNode, childrenOnly); - for (var k in fromNodesLookup) { - var elToRemove = fromNodesLookup[k]; - if (elToRemove) { - removeNode(elToRemove, elToRemove.parentNode); + // We now need to loop over any keyed nodes that might need to be + // removed. We only do the removal if we know that the keyed node + // never found a match. When a keyed node is matched up we remove + // it out of fromNodesLookup and we use fromNodesLookup to determine + // if a keyed node has been matched up or not + if (keyedRemovalList) { + for (var i=0, len=keyedRemovalList.length; i'); }); + it('should not remove keyed elements that are part of a DOM subtree that is skipped using onBeforeElUpdated', function() { + var el1 = document.createElement('div'); + el1.innerHTML = ''; + + var el2 = document.createElement('div'); + el2.innerHTML = ''; + + morphdom(el1, el2, { + onBeforeElUpdated: function(el) { + if (el.id === 'skipMe') { + return false; + } + } + }); + + expect(el1.querySelector('#skipMeChild') != null).to.equal(true); + }); + + it('should not remove keyed elements that are part of a DOM subtree that is skipped using onBeforeElChildrenUpdated', function() { + var el1 = document.createElement('div'); + el1.innerHTML = ''; + + var el2 = document.createElement('div'); + el2.innerHTML = ''; + + morphdom(el1, el2, { + onBeforeElChildrenUpdated: function(el) { + if (el.id === 'skipMe') { + return false; + } + } + }); + + expect(el1.querySelector('#skipMeChild') != null).to.equal(true); + }); + // xit('should reuse DOM element with matching ID and class name (2)', function() { // // NOTE: This test is currently failing. We need to improve the special case code // // for handling incompatible root nodes.