morphdom
Advanced tools
Comparing version 0.1.2 to 0.1.3
215
lib/index.js
@@ -12,3 +12,3 @@ var specialAttrHandlers = { | ||
INPUT$checked: function(el, value) { | ||
el.checked = value == null ? false : true; | ||
el.checked = (value == null) ? false : true; | ||
} | ||
@@ -63,10 +63,2 @@ }; | ||
function saveEl(morpher, el) { | ||
morpher.saved[el.id] = el; | ||
} | ||
function morphEl(morpher, fromNode, toNode) { | ||
morpher.tasks.push(new MorphElTask(fromNode, toNode)); | ||
} | ||
function moveChildren(from, to) { | ||
@@ -82,29 +74,43 @@ var curChild = from.firstChild; | ||
function removeNode(morpher, node, parentNode) { | ||
if (morpher.onBeforeRemoveNode(node) !== false) { | ||
parentNode.removeChild(node); | ||
function morphdom(fromNode, toNode, options) { | ||
if (!options) { | ||
options = {}; | ||
} | ||
var savedEls = {}; // Used to save off DOM elements with IDs | ||
var unmatchedEls = {}; | ||
var onFromNodeFound = options.onFromNodeFound || noop; | ||
var onBeforeMorphEl = options.onBeforeMorphEl || noop; | ||
var onBeforeMorphElChildren = options.onBeforeMorphElChildren || noop; | ||
function removeNodeHelper(node) { | ||
var id = node.id; | ||
// If the node has an ID then save it off since we will want | ||
// to reuse it in case the target DOM tree has a DOM element | ||
// with the same ID | ||
if (node.id) { | ||
saveEl(morpher, node); | ||
} else { | ||
morpher.onFromNodeRemoved(node); | ||
if (id) { | ||
savedEls[id] = node; | ||
} | ||
if (node.nodeType === 1) { | ||
var curChild = node.firstChild; | ||
while(curChild) { | ||
onFromNodeFound(curChild); | ||
removeNodeHelper(curChild); | ||
curChild = curChild.nextSibling; | ||
} | ||
} | ||
} | ||
} | ||
function MorphElTask(fromNode, toNode) { | ||
this.from = fromNode; | ||
this.to = toNode; | ||
this.node = null; // The morphed node | ||
} | ||
function removeNode(node, parentNode, alreadyVisited) { | ||
parentNode.removeChild(node); | ||
MorphElTask.prototype = { | ||
run: function(morpher) { | ||
var fromNode = this.from; | ||
var toNode = this.to; | ||
if (!alreadyVisited) { | ||
removeNodeHelper(node); | ||
} | ||
} | ||
if (morpher.onBeforeMorphEl(fromNode, toNode) === false) { | ||
function morphEl(fromNode, toNode, alreadyVisited) { | ||
if (onBeforeMorphEl(fromNode, toNode) === false) { | ||
return; | ||
@@ -115,3 +121,3 @@ } | ||
if (morpher.onBeforeMorphElChildren(fromNode, toNode) === false) { | ||
if (onBeforeMorphElChildren(fromNode, toNode) === false) { | ||
return; | ||
@@ -127,2 +133,3 @@ } | ||
var savedEl; | ||
var unmatchedEl; | ||
@@ -134,9 +141,7 @@ outer: while(curToNodeChild) { | ||
if (curToNodeId && (savedEl = morpher.saved[curToNodeId])) { | ||
delete morpher.saved[curToNodeId]; // Do some cleanup since we need to know which nodes were actually | ||
// completely removed from the DOM | ||
if (curToNodeId && (savedEl = savedEls[curToNodeId])) { | ||
// We are reusing a "from node" with an ID that matches | ||
// the ID of the current "to node" | ||
fromNode.insertBefore(savedEl, curFromNodeChild); | ||
morphEl(morpher, savedEl, curToNodeChild); | ||
morphEl(savedEl, curToNodeChild, true); | ||
curToNodeChild = toNextSibling; | ||
@@ -147,5 +152,16 @@ continue; | ||
while(curFromNodeChild) { | ||
morpher.onFromNodeFound(curFromNodeChild); | ||
var curFromNodeId = curFromNodeChild.id; | ||
fromNextSibling = curFromNodeChild.nextSibling; | ||
fromNextSibling = curFromNodeChild.nextSibling; | ||
if (!alreadyVisited) { | ||
onFromNodeFound(curFromNodeChild); | ||
if (curFromNodeId && (unmatchedEl = unmatchedEls[curFromNodeId])) { | ||
unmatchedEl.parentNode.replaceChild(curFromNodeChild, unmatchedEl); | ||
morphEl(curFromNodeChild, unmatchedEl, alreadyVisited); | ||
curFromNodeChild = fromNextSibling; | ||
continue; | ||
} | ||
} | ||
var curFromNodeType = curFromNodeChild.nodeType; | ||
@@ -159,7 +175,7 @@ | ||
// We have compatible DOM elements | ||
if (curFromNodeChild.id || curToNodeId) { | ||
if (curFromNodeId || curToNodeId) { | ||
// If either DOM element has an ID then we handle | ||
// those differently since we want to match up | ||
// by ID | ||
if (curToNodeId === curFromNodeChild.id) { | ||
if (curToNodeId === curFromNodeId) { | ||
isCompatible = true; | ||
@@ -175,3 +191,3 @@ } | ||
// task to morph the compatible DOM elements | ||
morphEl(morpher, curFromNodeChild, curToNodeChild); | ||
morphEl(curFromNodeChild, curToNodeChild, alreadyVisited); | ||
} | ||
@@ -191,3 +207,3 @@ } else if (curFromNodeType === 3) { // Both nodes being compared are Text nodes | ||
// No compatible match so remove the old node from the DOM | ||
removeNode(morpher, curFromNodeChild, fromNode); | ||
removeNode(curFromNodeChild, fromNode, alreadyVisited); | ||
@@ -197,2 +213,17 @@ curFromNodeChild = fromNextSibling; | ||
if (curToNodeId) { | ||
if ((savedEl = savedEls[curToNodeId])) { | ||
morphEl(savedEl, curToNodeChild, true); | ||
curToNodeChild = savedEl; // We want to append the saved element instead | ||
} else { | ||
// The current DOM element in the target tree has an ID | ||
// but we did not find a match in any of the corresponding | ||
// siblings. We just put the target element in the old DOM tree | ||
// but if we later find an element in the old DOM tree that has | ||
// a matching ID then we will replace the target element | ||
// with the corresponding old element and morph the old element | ||
unmatchedEls[curToNodeId] = curToNodeChild; | ||
} | ||
} | ||
// If we got this far then we did not find a candidate match for our "to node" | ||
@@ -210,88 +241,58 @@ // and we exhausted all of the children "from" nodes. Therefore, we will just | ||
while(curFromNodeChild) { | ||
morpher.onFromNodeFound(curFromNodeChild); | ||
if (!alreadyVisited) { | ||
onFromNodeFound(curFromNodeChild); | ||
} | ||
fromNextSibling = curFromNodeChild.nextSibling; | ||
removeNode(morpher, curFromNodeChild, fromNode); | ||
removeNode(curFromNodeChild, fromNode, alreadyVisited); | ||
curFromNodeChild = fromNextSibling; | ||
} | ||
} | ||
}; | ||
function Morpher(options) { | ||
if (!options) { | ||
options = {}; | ||
} | ||
var morphedNode = fromNode; | ||
var morphedNodeType = morphedNode.nodeType; | ||
var toNodeType = toNode.nodeType; | ||
// NOTE: We use a task stack to handle DOM trees of any size by | ||
// avoiding recursion | ||
this.tasks = []; | ||
// Invoke the callback for the top-level "from node". We'll handle all | ||
// of the nested nodes in the code that does the morph element task | ||
onFromNodeFound(fromNode); | ||
this.saved = {}; // Used to save off DOM elements with IDs | ||
this.onFromNodeFound = options.onFromNodeFound || noop; | ||
this.onFromNodeRemoved = options.onFromNodeRemoved || noop; | ||
this.onBeforeMorphEl = options.onBeforeMorphEl || noop; | ||
this.onBeforeMorphElChildren = options.onBeforeMorphElChildren || noop; | ||
this.onBeforeRemoveNode = options.onBeforeRemoveNode || noop; | ||
} | ||
Morpher.prototype = { | ||
morph: function(fromNode, toNode) { | ||
var tasks = this.tasks; | ||
var morphedNode = fromNode; | ||
var morphedNodeType = morphedNode.nodeType; | ||
var toNodeType = toNode.nodeType; | ||
// Invoke the callback for the top-level "from node". We'll handle all | ||
// of the nested nodes in the code that does the morph element task | ||
this.onFromNodeFound(fromNode); | ||
// Handle the case where we are given two DOM nodes that are not | ||
// compatible (e.g. <div> --> <span> or <div> --> TEXT) | ||
if (morphedNodeType === 1) { | ||
if (toNodeType === 1) { | ||
if (morphedNode.tagName !== toNode.tagName) { | ||
morphedNode = moveChildren(morphedNode, document.createElement(toNode.tagName)); | ||
} | ||
} else { | ||
// Going from an element node to a text node | ||
return toNode; | ||
// Handle the case where we are given two DOM nodes that are not | ||
// compatible (e.g. <div> --> <span> or <div> --> TEXT) | ||
if (morphedNodeType === 1) { | ||
if (toNodeType === 1) { | ||
if (morphedNode.tagName !== toNode.tagName) { | ||
morphedNode = moveChildren(morphedNode, document.createElement(toNode.tagName)); | ||
} | ||
} else if (morphedNodeType === 3) { // Text node | ||
if (toNodeType === 3) { | ||
morphedNode.nodeValue = toNode.nodeValue; | ||
return morphedNode; | ||
} else { | ||
// Text node to something else | ||
return toNode; | ||
} | ||
} else { | ||
// Going from an element node to a text node | ||
return toNode; | ||
} | ||
morphEl(this, morphedNode, toNode); | ||
// Keep going until there is no more scheduled work | ||
while(tasks.length) { | ||
tasks.pop().run(this); | ||
} else if (morphedNodeType === 3) { // Text node | ||
if (toNodeType === 3) { | ||
morphedNode.nodeValue = toNode.nodeValue; | ||
return morphedNode; | ||
} else { | ||
// Text node to something else | ||
return toNode; | ||
} | ||
} | ||
// Fire the "onFromNodeRemoved" event for any saved elements | ||
// that never found a new home in the morphed DOM | ||
var savedEls = this.saved; | ||
for (var savedElId in savedEls) { | ||
if (savedEls.hasOwnProperty(savedElId)) { | ||
this.onFromNodeRemoved(savedEls[savedElId]); | ||
} | ||
} | ||
morphEl(morphedNode, toNode); | ||
if (morphedNode !== fromNode && fromNode.parentNode) { | ||
fromNode.parentNode.replaceChild(morphedNode, fromNode); | ||
} | ||
// Fire the "onFromNodeRemoved" event for any saved elements | ||
// that never found a new home in the morphed DOM | ||
// for (var savedElId in savedEls) { | ||
// if (savedEls.hasOwnProperty(savedElId)) { | ||
// onFromNodeRemoved(savedEls[savedElId]); | ||
// } | ||
// } | ||
return morphedNode; | ||
if (morphedNode !== fromNode && fromNode.parentNode) { | ||
fromNode.parentNode.replaceChild(morphedNode, fromNode); | ||
} | ||
}; | ||
function morphdom(oldNode, newNode, options) { | ||
var morpher = new Morpher(options); | ||
return morpher.morph(oldNode, newNode); | ||
return morphedNode; | ||
} | ||
module.exports = morphdom; |
@@ -29,3 +29,3 @@ { | ||
"dependencies": {}, | ||
"version": "0.1.2" | ||
"version": "0.1.3" | ||
} |
@@ -103,17 +103,19 @@ var chai = require('chai'); | ||
function markDescendentsRemoved(node) { | ||
var curNode = node.firstChild; | ||
while(curNode) { | ||
if (curNode.$testOnFromNodeFlag) { | ||
throw new Error('Descendent of removed node was incorrectly visited. Node: ' + curNode); | ||
} | ||
function isNodeInTree(node, rootNode) { | ||
if (node == null) { | ||
throw new Error('Invalid arguments'); | ||
} | ||
var currentNode = node; | ||
curNode.$testRemovedDescendentFlag = true; | ||
if (curNode.nodeType === 1) { | ||
markDescendentsRemoved(curNode); | ||
while (true) { | ||
if (currentNode == null) { | ||
return false; | ||
} else if (currentNode == rootNode) { | ||
return true; | ||
} | ||
curNode = curNode.nextSibling; | ||
currentNode = currentNode.parentNode; | ||
} | ||
return false; | ||
} | ||
@@ -144,19 +146,6 @@ | ||
node.$testOnFromNodeFlag = true; | ||
} | ||
function onFromNodeRemoved(node) { | ||
if (node.$testOnFromNodeRemovedFlag) { | ||
throw new Error('Duplicate onFromNodeRemoved for: ' + node); | ||
} | ||
node.$testOnFromNodeRemovedFlag = true; | ||
markDescendentsRemoved(node); | ||
} | ||
var morphedNode = morphdom(fromNode, toNode, { | ||
onFromNodeFound: onFromNodeFound, | ||
onFromNodeRemoved: onFromNodeRemoved | ||
onFromNodeFound: onFromNodeFound | ||
}); | ||
@@ -194,9 +183,15 @@ | ||
allFromNodes.forEach(function(node) { | ||
if (node.$testOnFromNodeFlag && node.$testRemovedDescendentFlag) { | ||
throw new Error('Descendent of a removed "from" node was visited. Node: ' + node); | ||
if (!node.$testOnFromNodeFlag) { | ||
throw new Error('"from" node was not reported as visited. Node: ' + node); | ||
} | ||
if (!node.$testOnFromNodeFlag && !node.$testRemovedDescendentFlag) { | ||
throw new Error('"from" node not found during morph ' + node); | ||
} | ||
// if (isNodeInTree(node, morphedNode)) { | ||
// if (node.$testOnFromNodeRemovedFlag) { | ||
// throw new Error('onFromNodeRemoved(node) called for node that is in the final DOM tree: ' + node); | ||
// } | ||
// } else { | ||
// if (!node.$testOnFromNodeRemovedFlag) { | ||
// throw new Error('"from" node was removed but onFromNodeRemoved(node) was not called: ' + node); | ||
// } | ||
// } | ||
}); | ||
@@ -203,0 +198,0 @@ } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
31916
53
593