Skip to content
Merged
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
40 changes: 24 additions & 16 deletions src/idiomorph.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
/**
* @callback Morph
*
* @param {Element | Document} oldNode
* @param {Node} oldNode
* @param {Node | HTMLCollection | Node[] | string | null} newContent
* @param {Config} [config]
* @returns {Promise<Node[]> | Node[]}
Expand Down Expand Up @@ -152,27 +152,27 @@ var Idiomorph = (function () {
/**
* Core idiomorph function for morphing one DOM tree to another
*
* @param {Element | Document} oldNode
* @param {Node} oldNode
* @param {Node | HTMLCollection | Node[] | string | null} newContent
* @param {Config} [config]
* @returns {Promise<Node[]> | Node[]}
*/
function morph(oldNode, newContent, config = {}) {
oldNode = normalizeElement(oldNode);
const oldElt = normalizeElement(oldNode);
const newNode = normalizeParent(newContent);
const ctx = createMorphContext(oldNode, newNode, config);
const ctx = createMorphContext(oldElt, newNode, config);

return withHeadBlocking(
ctx,
oldNode,
oldElt,
newNode,
/** @param {MorphContext} ctx */ (ctx) => {
const morphedNodes = saveAndRestoreFocus(ctx, () => {
if (ctx.morphStyle === "innerHTML") {
morphChildren(ctx, oldNode, newNode);
return Array.from(oldNode.childNodes);
morphChildren(ctx, oldElt, newNode);
return Array.from(oldElt.childNodes);
} else {
return morphOuterHTML(ctx, oldNode, newNode);
return morphOuterHTML(ctx, oldElt, newNode);
}
});
ctx.pantry.remove();
Expand Down Expand Up @@ -1018,6 +1018,10 @@ var Idiomorph = (function () {
if (!["innerHTML", "outerHTML"].includes(morphStyle)) {
throw `Do not understand how to morph style ${morphStyle}`;
}
// Text and Comment have no ParentNode methods, so they cannot take innerHTML
if (morphStyle === "innerHTML" && !oldNode.append) {
throw `Cannot morph the innerHTML of a ${oldNode.nodeName} node, as it cannot have children`;
}

const headStyle = mergedConfig.head.style || "merge";
if (!["merge", "append", "morph", "none"].includes(headStyle)) {
Expand Down Expand Up @@ -1095,23 +1099,26 @@ var Idiomorph = (function () {
}

/**
* Returns all elements with a non-empty ID contained within the root element and its
* Returns all elements with a non-empty ID contained within the root node and its
* descendants, each paired with its id so that it only has to be read once.
*
* @param {Element} root
* @param {Node} root
* @returns {IdElement[]}
*/
function findIdElements(root) {
/** @type {IdElement[]} */
let elements = [];
for (const elt of root.querySelectorAll("[id]")) {
// root could be a text or comment node which has no `querySelectorAll`,
// or a document fragment which has no `getAttribute`
const rootElt = /** @type {Partial<Element>} */ (root);
for (const elt of rootElt.querySelectorAll?.("[id]") ?? []) {
// elt.id is unsafe because of form input shadowing, and `id=""` is not persistable
const id = elt.getAttribute("id");
if (id) elements.push({ elt, id });
}
// root could be a document fragment which doesn't have `getAttribute`
const rootId = root.getAttribute?.("id");
if (rootId) elements.push({ elt: root, id: rootId });
const rootId = rootElt.getAttribute?.("id");
if (rootId)
elements.push({ elt: /** @type {Element} */ (root), id: rootId });
return elements;
}

Expand Down Expand Up @@ -1230,14 +1237,15 @@ var Idiomorph = (function () {

/**
*
* @param {Element | Document} content
* @param {Node} content
* @returns {Element}
*/
function normalizeElement(content) {
if (content instanceof Document) {
return content.documentElement;
} else {
return content;
// a Text or Comment node is not an Element, but morphOuterHTML only ever reads Node members off it
return /** @type {Element} */ (content);
}
}

Expand Down
30 changes: 30 additions & 0 deletions test/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,28 @@ describe("Core morphing tests", function () {
);
});

it("morphs outerHTML properly when oldNode is a text node", function () {
let parent = make("<div>Foo</div>");
let initial = parent.firstChild;
Idiomorph.morph(initial, "<button>Bar</button>", {
morphStyle: "outerHTML",
});
parent.innerHTML.should.equal("<button>Bar</button>");
});

it("morphs outerHTML properly when oldNode is a comment node", function () {
let parent = make(
"<div><p>Before</p><!-- placeholder --><p>After</p></div>",
);
let initial = parent.childNodes[1];
Idiomorph.morph(initial, "<button>Bar</button>", {
morphStyle: "outerHTML",
});
parent.innerHTML.should.equal(
"<p>Before</p><button>Bar</button><p>After</p>",
);
});

it("morphs innerHTML as content properly when argument is null", function () {
let initial = make("<div>Foo</div>");
Idiomorph.morph(initial, null, { morphStyle: "innerHTML" });
Expand Down Expand Up @@ -178,6 +200,14 @@ describe("Core morphing tests", function () {
}).should.throw("Do not understand how to morph style magic");
});

it("errors on innerHTML of a node that cannot have children", function () {
(() => {
Idiomorph.morph(make("<div>Foo</div>").firstChild, [], {
morphStyle: "innerHTML",
});
}).should.throw("Cannot morph the innerHTML of a #text node");
});

it("errors on bad head style", function () {
(() => {
Idiomorph.morph(make("<p>"), [], { head: { style: "magic" } });
Expand Down
Loading