Precise DOM morphing
morphing typescript dom
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Rename from “guide” to “reference”

+112 -108
+3 -1
README.md
··· 19 19 ```javascript 20 20 import { morph } from "morphlex"; 21 21 22 - morph(currentNode, guideNode); 22 + morph(currentNode, referenceNode); 23 23 ``` 24 + 25 + The `currentNode` will be morphed into the state of the `referenceNode`. The `referenceNode` will not be mutated in this process. 24 26 25 27 ## Run tests 26 28
+1 -1
dist/morphlex.d.ts
··· 1 - export declare function morph(node: ChildNode, guide: ChildNode): void; 1 + export declare function morph(node: ChildNode, reference: ChildNode): void;
+54 -53
dist/morphlex.js
··· 1 - export function morph(node, guide) { 1 + export function morph(node, reference) { 2 2 const idMap = new Map(); 3 - if (isParentNode(node) && isParentNode(guide)) { 3 + if (isParentNode(node) && isParentNode(reference)) { 4 4 populateIdSets(node, idMap); 5 - populateIdSets(guide, idMap); 5 + populateIdSets(reference, idMap); 6 6 } 7 - morphNodes(node, guide, idMap); 7 + morphNodes(node, reference, idMap); 8 8 } 9 9 // For each node with an ID, push that ID into the IdSet on the IdMap, for each of its parent elements. 10 10 function populateIdSets(node, idMap) { ··· 23 23 } 24 24 } 25 25 // This is where we actually morph the nodes. The `morph` function exists to set up the `idMap`. 26 - function morphNodes(node, guide, idMap) { 27 - if (isElement(node) && isElement(guide) && node.tagName === guide.tagName) { 26 + function morphNodes(node, ref, idMap) { 27 + if (isElement(node) && isElement(ref) && node.tagName === ref.tagName) { 28 28 // We need to check if the element is an input, option, or textarea here, because they have 29 29 // special attributes not covered by the isEqualNode check. 30 - if (!isInput(node) && !isOption(node) && !isTextArea(node) && node.isEqualNode(guide)) return; 30 + if (!isInput(node) && !isOption(node) && !isTextArea(node) && node.isEqualNode(ref)) return; 31 31 else { 32 - if (node.hasAttributes() || guide.hasAttributes()) morphAttributes(node, guide); 33 - if (node.hasChildNodes() || guide.hasChildNodes()) morphChildNodes(node, guide, idMap); 32 + if (node.hasAttributes() || ref.hasAttributes()) morphAttributes(node, ref); 33 + if (node.hasChildNodes() || ref.hasChildNodes()) morphChildNodes(node, ref, idMap); 34 34 } 35 35 } else { 36 - if (node.isEqualNode(guide)) return; 37 - else if (isText(node) && isText(guide)) { 38 - if (node.textContent !== guide.textContent) node.textContent = guide.textContent; 39 - } else if (isComment(node) && isComment(guide)) { 40 - if (node.nodeValue !== guide.nodeValue) node.nodeValue = guide.nodeValue; 41 - } else node.replaceWith(guide.cloneNode(true)); 36 + if (node.isEqualNode(ref)) return; 37 + else if (isText(node) && isText(ref)) { 38 + if (node.textContent !== ref.textContent) node.textContent = ref.textContent; 39 + } else if (isComment(node) && isComment(ref)) { 40 + if (node.nodeValue !== ref.nodeValue) node.nodeValue = ref.nodeValue; 41 + } else node.replaceWith(ref.cloneNode(true)); 42 42 } 43 43 } 44 - function morphAttributes(elem, guide) { 45 - // Remove any excess attributes from the element that aren’t present in the guide. 46 - for (const { name } of elem.attributes) guide.hasAttribute(name) || elem.removeAttribute(name); 47 - // Copy attributes from the guide to the element, if they don’t already match. 48 - for (const { name, value } of guide.attributes) elem.getAttribute(name) === value || elem.setAttribute(name, value); 49 - elem.nodeValue; 50 - // For certain types of elements, we need to do some extra work to ensure the element’s state matches the guide’s state. 51 - if (isInput(elem) && isInput(guide)) { 52 - if (elem.checked !== guide.checked) elem.checked = guide.checked; 53 - if (elem.disabled !== guide.disabled) elem.disabled = guide.disabled; 54 - if (elem.indeterminate !== guide.indeterminate) elem.indeterminate = guide.indeterminate; 55 - if (elem.type !== "file" && elem.value !== guide.value) elem.value = guide.value; 56 - } else if (isOption(elem) && isOption(guide) && elem.selected !== guide.selected) elem.selected = guide.selected; 57 - else if (isTextArea(elem) && isTextArea(guide)) { 58 - if (elem.value !== guide.value) elem.value = guide.value; 59 - const text = elem.firstChild; 60 - if (text && isText(text) && text.textContent !== guide.value) text.textContent = guide.value; 44 + function morphAttributes(elm, ref) { 45 + // Remove any excess attributes from the element that aren’t present in the reference. 46 + for (const { name } of elm.attributes) ref.hasAttribute(name) || elm.removeAttribute(name); 47 + // Copy attributes from the reference to the element, if they don’t already match. 48 + for (const { name, value } of ref.attributes) elm.getAttribute(name) === value || elm.setAttribute(name, value); 49 + elm.nodeValue; 50 + // For certain types of elements, we need to do some extra work to ensure 51 + // the element’s state matches the reference elements’ state. 52 + if (isInput(elm) && isInput(ref)) { 53 + if (elm.checked !== ref.checked) elm.checked = ref.checked; 54 + if (elm.disabled !== ref.disabled) elm.disabled = ref.disabled; 55 + if (elm.indeterminate !== ref.indeterminate) elm.indeterminate = ref.indeterminate; 56 + if (elm.type !== "file" && elm.value !== ref.value) elm.value = ref.value; 57 + } else if (isOption(elm) && isOption(ref) && elm.selected !== ref.selected) elm.selected = ref.selected; 58 + else if (isTextArea(elm) && isTextArea(ref)) { 59 + if (elm.value !== ref.value) elm.value = ref.value; 60 + const text = elm.firstChild; 61 + if (text && isText(text) && text.textContent !== ref.value) text.textContent = ref.value; 61 62 } 62 63 } 63 - // Iterates over the child nodes of the guide element, morphing the main element’s child nodes to match. 64 - function morphChildNodes(elem, guide, idMap) { 64 + // Iterates over the child nodes of the reference element, morphing the main element’s child nodes to match. 65 + function morphChildNodes(elem, ref, idMap) { 65 66 const childNodes = [...elem.childNodes]; 66 - const guideChildNodes = [...guide.childNodes]; 67 - for (let i = 0; i < guideChildNodes.length; i++) { 67 + const refChildNodes = [...ref.childNodes]; 68 + for (let i = 0; i < refChildNodes.length; i++) { 68 69 const child = childNodes.at(i); 69 - const guideChild = guideChildNodes.at(i); 70 - if (child && guideChild) morphChildNode(child, guideChild, elem, idMap); 71 - else if (guideChild) elem.appendChild(guideChild.cloneNode(true)); 70 + const refChild = refChildNodes.at(i); 71 + if (child && refChild) morphChildNode(child, refChild, elem, idMap); 72 + else if (refChild) elem.appendChild(refChild.cloneNode(true)); 72 73 else if (child) child.remove(); 73 74 } 74 75 // Remove any excess child nodes from the main element. This is separate because 75 76 // the loop above might modify the length of the main element’s child nodes. 76 - while (elem.childNodes.length > guide.childNodes.length) elem.lastChild?.remove(); 77 + while (elem.childNodes.length > ref.childNodes.length) elem.lastChild?.remove(); 77 78 } 78 - function morphChildNode(child, guide, parent, idMap) { 79 - if (isElement(child) && isElement(guide)) morphChildElement(child, guide, parent, idMap); 80 - else morphNodes(child, guide, idMap); 79 + function morphChildNode(child, ref, parent, idMap) { 80 + if (isElement(child) && isElement(ref)) morphChildElement(child, ref, parent, idMap); 81 + else morphNodes(child, ref, idMap); 81 82 } 82 - function morphChildElement(child, guide, parent, idMap) { 83 - const guideIdSet = idMap.get(guide); 83 + function morphChildElement(child, ref, parent, idMap) { 84 + const refIdSet = idMap.get(ref); 84 85 // Generate the array in advance of the loop 85 - const guideSetArray = guideIdSet ? [...guideIdSet] : []; 86 + const refSetArray = refIdSet ? [...refIdSet] : []; 86 87 let currentNode = child; 87 88 let nextMatchByTagName = null; 88 89 // Try find a match by idSet, while also looking out for the next best match by tagName. 89 90 while (currentNode) { 90 91 if (isElement(currentNode)) { 91 - if (currentNode.id === guide.id) { 92 + if (currentNode.id === ref.id) { 92 93 parent.insertBefore(currentNode, child); 93 - return morphNodes(currentNode, guide, idMap); 94 + return morphNodes(currentNode, ref, idMap); 94 95 } else if (currentNode.id !== "") { 95 96 const currentIdSet = idMap.get(currentNode); 96 - if (currentIdSet && guideSetArray.some((it) => currentIdSet.has(it))) { 97 + if (currentIdSet && refSetArray.some((it) => currentIdSet.has(it))) { 97 98 parent.insertBefore(currentNode, child); 98 - return morphNodes(currentNode, guide, idMap); 99 + return morphNodes(currentNode, ref, idMap); 99 100 } 100 - } else if (!nextMatchByTagName && currentNode.tagName === guide.tagName) { 101 + } else if (!nextMatchByTagName && currentNode.tagName === ref.tagName) { 101 102 nextMatchByTagName = currentNode; 102 103 } 103 104 } ··· 105 106 } 106 107 if (nextMatchByTagName) { 107 108 parent.insertBefore(nextMatchByTagName, child); 108 - morphNodes(nextMatchByTagName, guide, idMap); 109 - } else child.replaceWith(guide.cloneNode(true)); 109 + morphNodes(nextMatchByTagName, ref, idMap); 110 + } else child.replaceWith(ref.cloneNode(true)); 110 111 } 111 112 // We cannot use `instanceof` when nodes might be from different documents, 112 113 // so we use type guards instead. This keeps TypeScript happy, while doing
+54 -53
src/morphlex.ts
··· 1 1 type IdSet = Set<string>; 2 2 type IdMap = Map<Node, IdSet>; 3 3 4 - export function morph(node: ChildNode, guide: ChildNode): void { 4 + export function morph(node: ChildNode, reference: ChildNode): void { 5 5 const idMap: IdMap = new Map(); 6 6 7 - if (isParentNode(node) && isParentNode(guide)) { 7 + if (isParentNode(node) && isParentNode(reference)) { 8 8 populateIdSets(node, idMap); 9 - populateIdSets(guide, idMap); 9 + populateIdSets(reference, idMap); 10 10 } 11 11 12 - morphNodes(node, guide, idMap); 12 + morphNodes(node, reference, idMap); 13 13 } 14 14 15 15 // For each node with an ID, push that ID into the IdSet on the IdMap, for each of its parent elements. ··· 34 34 } 35 35 36 36 // This is where we actually morph the nodes. The `morph` function exists to set up the `idMap`. 37 - function morphNodes(node: ChildNode, guide: ChildNode, idMap: IdMap): void { 38 - if (isElement(node) && isElement(guide) && node.tagName === guide.tagName) { 37 + function morphNodes(node: ChildNode, ref: ChildNode, idMap: IdMap): void { 38 + if (isElement(node) && isElement(ref) && node.tagName === ref.tagName) { 39 39 // We need to check if the element is an input, option, or textarea here, because they have 40 40 // special attributes not covered by the isEqualNode check. 41 - if (!isInput(node) && !isOption(node) && !isTextArea(node) && node.isEqualNode(guide)) return; 41 + if (!isInput(node) && !isOption(node) && !isTextArea(node) && node.isEqualNode(ref)) return; 42 42 else { 43 - if (node.hasAttributes() || guide.hasAttributes()) morphAttributes(node, guide); 44 - if (node.hasChildNodes() || guide.hasChildNodes()) morphChildNodes(node, guide, idMap); 43 + if (node.hasAttributes() || ref.hasAttributes()) morphAttributes(node, ref); 44 + if (node.hasChildNodes() || ref.hasChildNodes()) morphChildNodes(node, ref, idMap); 45 45 } 46 46 } else { 47 - if (node.isEqualNode(guide)) return; 48 - else if (isText(node) && isText(guide)) { 49 - if (node.textContent !== guide.textContent) node.textContent = guide.textContent; 50 - } else if (isComment(node) && isComment(guide)) { 51 - if (node.nodeValue !== guide.nodeValue) node.nodeValue = guide.nodeValue; 52 - } else node.replaceWith(guide.cloneNode(true)); 47 + if (node.isEqualNode(ref)) return; 48 + else if (isText(node) && isText(ref)) { 49 + if (node.textContent !== ref.textContent) node.textContent = ref.textContent; 50 + } else if (isComment(node) && isComment(ref)) { 51 + if (node.nodeValue !== ref.nodeValue) node.nodeValue = ref.nodeValue; 52 + } else node.replaceWith(ref.cloneNode(true)); 53 53 } 54 54 } 55 55 56 - function morphAttributes(elem: Element, guide: Element): void { 57 - // Remove any excess attributes from the element that aren’t present in the guide. 58 - for (const { name } of elem.attributes) guide.hasAttribute(name) || elem.removeAttribute(name); 56 + function morphAttributes(elm: Element, ref: Element): void { 57 + // Remove any excess attributes from the element that aren’t present in the reference. 58 + for (const { name } of elm.attributes) ref.hasAttribute(name) || elm.removeAttribute(name); 59 59 60 - // Copy attributes from the guide to the element, if they don’t already match. 61 - for (const { name, value } of guide.attributes) elem.getAttribute(name) === value || elem.setAttribute(name, value); 60 + // Copy attributes from the reference to the element, if they don’t already match. 61 + for (const { name, value } of ref.attributes) elm.getAttribute(name) === value || elm.setAttribute(name, value); 62 62 63 - elem.nodeValue; 63 + elm.nodeValue; 64 64 65 - // For certain types of elements, we need to do some extra work to ensure the element’s state matches the guide’s state. 66 - if (isInput(elem) && isInput(guide)) { 67 - if (elem.checked !== guide.checked) elem.checked = guide.checked; 68 - if (elem.disabled !== guide.disabled) elem.disabled = guide.disabled; 69 - if (elem.indeterminate !== guide.indeterminate) elem.indeterminate = guide.indeterminate; 70 - if (elem.type !== "file" && elem.value !== guide.value) elem.value = guide.value; 71 - } else if (isOption(elem) && isOption(guide) && elem.selected !== guide.selected) elem.selected = guide.selected; 72 - else if (isTextArea(elem) && isTextArea(guide)) { 73 - if (elem.value !== guide.value) elem.value = guide.value; 65 + // For certain types of elements, we need to do some extra work to ensure 66 + // the element’s state matches the reference elements’ state. 67 + if (isInput(elm) && isInput(ref)) { 68 + if (elm.checked !== ref.checked) elm.checked = ref.checked; 69 + if (elm.disabled !== ref.disabled) elm.disabled = ref.disabled; 70 + if (elm.indeterminate !== ref.indeterminate) elm.indeterminate = ref.indeterminate; 71 + if (elm.type !== "file" && elm.value !== ref.value) elm.value = ref.value; 72 + } else if (isOption(elm) && isOption(ref) && elm.selected !== ref.selected) elm.selected = ref.selected; 73 + else if (isTextArea(elm) && isTextArea(ref)) { 74 + if (elm.value !== ref.value) elm.value = ref.value; 74 75 75 - const text = elem.firstChild; 76 - if (text && isText(text) && text.textContent !== guide.value) text.textContent = guide.value; 76 + const text = elm.firstChild; 77 + if (text && isText(text) && text.textContent !== ref.value) text.textContent = ref.value; 77 78 } 78 79 } 79 80 80 - // Iterates over the child nodes of the guide element, morphing the main element’s child nodes to match. 81 - function morphChildNodes(elem: Element, guide: Element, idMap: IdMap): void { 81 + // Iterates over the child nodes of the reference element, morphing the main element’s child nodes to match. 82 + function morphChildNodes(elem: Element, ref: Element, idMap: IdMap): void { 82 83 const childNodes = [...elem.childNodes]; 83 - const guideChildNodes = [...guide.childNodes]; 84 + const refChildNodes = [...ref.childNodes]; 84 85 85 - for (let i = 0; i < guideChildNodes.length; i++) { 86 + for (let i = 0; i < refChildNodes.length; i++) { 86 87 const child = childNodes.at(i); 87 - const guideChild = guideChildNodes.at(i); 88 + const refChild = refChildNodes.at(i); 88 89 89 - if (child && guideChild) morphChildNode(child, guideChild, elem, idMap); 90 - else if (guideChild) elem.appendChild(guideChild.cloneNode(true)); 90 + if (child && refChild) morphChildNode(child, refChild, elem, idMap); 91 + else if (refChild) elem.appendChild(refChild.cloneNode(true)); 91 92 else if (child) child.remove(); 92 93 } 93 94 94 95 // Remove any excess child nodes from the main element. This is separate because 95 96 // the loop above might modify the length of the main element’s child nodes. 96 - while (elem.childNodes.length > guide.childNodes.length) elem.lastChild?.remove(); 97 + while (elem.childNodes.length > ref.childNodes.length) elem.lastChild?.remove(); 97 98 } 98 99 99 - function morphChildNode(child: ChildNode, guide: ChildNode, parent: Element, idMap: IdMap): void { 100 - if (isElement(child) && isElement(guide)) morphChildElement(child, guide, parent, idMap); 101 - else morphNodes(child, guide, idMap); 100 + function morphChildNode(child: ChildNode, ref: ChildNode, parent: Element, idMap: IdMap): void { 101 + if (isElement(child) && isElement(ref)) morphChildElement(child, ref, parent, idMap); 102 + else morphNodes(child, ref, idMap); 102 103 } 103 104 104 - function morphChildElement(child: Element, guide: Element, parent: Element, idMap: IdMap): void { 105 - const guideIdSet = idMap.get(guide); 105 + function morphChildElement(child: Element, ref: Element, parent: Element, idMap: IdMap): void { 106 + const refIdSet = idMap.get(ref); 106 107 107 108 // Generate the array in advance of the loop 108 - const guideSetArray = guideIdSet ? [...guideIdSet] : []; 109 + const refSetArray = refIdSet ? [...refIdSet] : []; 109 110 110 111 let currentNode: ChildNode | null = child; 111 112 let nextMatchByTagName: ChildNode | null = null; ··· 113 114 // Try find a match by idSet, while also looking out for the next best match by tagName. 114 115 while (currentNode) { 115 116 if (isElement(currentNode)) { 116 - if (currentNode.id === guide.id) { 117 + if (currentNode.id === ref.id) { 117 118 parent.insertBefore(currentNode, child); 118 - return morphNodes(currentNode, guide, idMap); 119 + return morphNodes(currentNode, ref, idMap); 119 120 } else if (currentNode.id !== "") { 120 121 const currentIdSet = idMap.get(currentNode); 121 122 122 - if (currentIdSet && guideSetArray.some((it) => currentIdSet.has(it))) { 123 + if (currentIdSet && refSetArray.some((it) => currentIdSet.has(it))) { 123 124 parent.insertBefore(currentNode, child); 124 - return morphNodes(currentNode, guide, idMap); 125 + return morphNodes(currentNode, ref, idMap); 125 126 } 126 - } else if (!nextMatchByTagName && currentNode.tagName === guide.tagName) { 127 + } else if (!nextMatchByTagName && currentNode.tagName === ref.tagName) { 127 128 nextMatchByTagName = currentNode; 128 129 } 129 130 } ··· 133 134 134 135 if (nextMatchByTagName) { 135 136 parent.insertBefore(nextMatchByTagName, child); 136 - morphNodes(nextMatchByTagName, guide, idMap); 137 - } else child.replaceWith(guide.cloneNode(true)); 137 + morphNodes(nextMatchByTagName, ref, idMap); 138 + } else child.replaceWith(ref.cloneNode(true)); 138 139 } 139 140 140 141 // We cannot use `instanceof` when nodes might be from different documents,