Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -130,6 +131,7 @@ var morphedNode = morphdom(fromNode, toNode, {
return true;
},
childrenOnly: false,
keyedRoot: false,
skipFromChildren: function(fromEl, toEl) {
return false;
}
Expand Down Expand Up @@ -262,4 +264,3 @@ npm test
# License

MIT

1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface MorphDomOptions {
skipFromChildren?: (fromEl: HTMLElement) => boolean;
addChild?: (parent: HTMLElement, child: HTMLElement) => void;
childrenOnly?: boolean;
keyedRoot?: boolean;
}

declare function morphdom(
Expand Down
41 changes: 33 additions & 8 deletions src/morphdom.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -416,6 +417,37 @@ export default function morphdomFactory(morphAttrs) {
}
} // END: morphChildren(...)

function cleanupKeyedNodes() {
for (var i=0, len=keyedRemovalList.length; i<len; i++) {
var elToRemove = fromNodesLookup[keyedRemovalList[i]];
if (elToRemove) {
removeNode(elToRemove, elToRemove.parentNode, false);
}
}
}

if (keyedRoot && !childrenOnly) {
var fromNodeKey = getNodeKey(fromNode);
var toNodeKey = getNodeKey(toNode);

if ((fromNodeKey || toNodeKey) && fromNodeKey !== toNodeKey) {
if (toNode.actualize) {
toNode = toNode.actualize(fromNode.ownerDocument || doc);
}

onNodeDiscarded(fromNode);

if (fromNode.parentNode) {
fromNode.parentNode.replaceChild(toNode, fromNode);
}

handleNodeAdded(toNode);
walkDiscardedChildNodes(fromNode, false);
cleanupKeyedNodes();
return toNode;
}
}

var morphedNode = fromNode;
var morphedNodeType = morphedNode.nodeType;
var toNodeType = toNode.nodeType;
Expand Down Expand Up @@ -463,14 +495,7 @@ export default function morphdomFactory(morphAttrs) {
// 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<len; i++) {
var elToRemove = fromNodesLookup[keyedRemovalList[i]];
if (elToRemove) {
removeNode(elToRemove, elToRemove.parentNode, false);
}
}
}
cleanupKeyedNodes();
}

if (!childrenOnly && morphedNode !== fromNode && fromNode.parentNode) {
Expand Down
97 changes: 97 additions & 0 deletions test/browser/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,103 @@ describe('morphdom' , function() {
expect(el1.children[1].textContent).to.equal('B');
});

it('updates a keyed root in place by default', function() {
var parent = document.createElement('div');
var fromRoot = document.createElement('section');
fromRoot.id = 'old-root';
fromRoot.innerHTML = '<span id="shared">old</span>';
parent.appendChild(fromRoot);
var shared = fromRoot.firstChild;

var toRoot = document.createElement('section');
toRoot.id = 'new-root';
toRoot.innerHTML = '<span id="shared">new</span>';

var morphedRoot = morphdom(fromRoot, toRoot);

expect(morphedRoot).to.equal(fromRoot);
expect(parent.firstChild).to.equal(fromRoot);
expect(fromRoot.id).to.equal('new-root');
expect(fromRoot.firstChild).to.equal(shared);
expect(shared.textContent).to.equal('new');
});

it('replaces a keyed root and preserves matching keyed descendants', function() {
var parent = document.createElement('div');
var fromRoot = document.createElement('section');
fromRoot.id = 'old-root';
fromRoot.innerHTML = '<span id="shared"><i id="old-only">old</i></span>';
parent.appendChild(fromRoot);
var shared = fromRoot.firstChild;

var toRoot = document.createElement('section');
toRoot.id = 'new-root';
toRoot.innerHTML = '<span id="shared"><strong id="new-only">new</strong></span>';

var added = [];
var updated = [];
var discarded = [];
var lifecycle = [];
var morphedRoot = morphdom(fromRoot, toRoot, {
keyedRoot: true,
onNodeAdded: function(node) {
added.push(node);
if (node.id) lifecycle.push('added:' + node.id);
},
onElUpdated: function(node) { updated.push(node); },
onNodeDiscarded: function(node) {
discarded.push(node);
if (node.id) lifecycle.push('discarded:' + node.id);
}
});

expect(morphedRoot).to.equal(toRoot);
expect(parent.firstChild).to.equal(toRoot);
expect(fromRoot.parentNode).to.equal(null);
expect(toRoot.outerHTML).to.equal('<section id="new-root"><span id="shared"><strong id="new-only">new</strong></span></section>');
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('<article id="new-root">new</article>');
});

it('replaces keyed SVG roots', function() {
var fromTemplate = document.createElement('template');
fromTemplate.innerHTML = '<svg id="old-root"><g id="shared"><text>old</text></g></svg>';
var toTemplate = document.createElement('template');
toTemplate.innerHTML = '<svg id="new-root"><g id="shared"><text>new</text></g></svg>';
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 = '<p id="hi" class="foo">A</p><p id="hi" class="bar">B</p>';
Expand Down
Loading