Precise DOM morphing
morphing typescript dom

Allow comments in JS output

This will make it easier to debug for people who don’t like TypeScript.

+18 -1
+17
dist/morphlex.js
··· 6 6 } 7 7 morphNodes(node, guide, idMap); 8 8 } 9 + // For each node with an ID, push that ID into the IdSet on the IdMap, for each of its parent elements. 9 10 function populateIdSets(node, idMap) { 10 11 const elementsWithIds = node.querySelectorAll("[id]"); 11 12 for (const elementWithId of elementsWithIds) { 12 13 const id = elementWithId.id; 14 + // Ignore empty IDs 13 15 if (id === "") 14 16 continue; 15 17 let current = elementWithId; ··· 22 24 } 23 25 } 24 26 } 27 + // This is where we actually morph the nodes. The `morph` function exists to set up the `idMap`. 25 28 function morphNodes(node, guide, idMap, insertBefore, parent) { 29 + // TODO: We should extract this into a separate function. 26 30 if (parent && insertBefore && insertBefore !== node) 27 31 parent.insertBefore(guide, insertBefore); 28 32 if (isElement(node) && isElement(guide) && node.tagName === guide.tagName) { 33 + // We need to check if the element is an input, option, or textarea here, because they have 34 + // special attributes not covered by the isEqualNode check. 29 35 if (!isInput(node) && !isOption(node) && !isTextArea(node) && node.isEqualNode(guide)) 30 36 return; 31 37 else { ··· 51 57 } 52 58 } 53 59 function morphAttributes(elem, guide) { 60 + // Remove any excess attributes from the element that aren’t present in the guide. 54 61 for (const { name } of elem.attributes) 55 62 guide.hasAttribute(name) || elem.removeAttribute(name); 63 + // Copy attributes from the guide to the element, if they don’t already match. 56 64 for (const { name, value } of guide.attributes) 57 65 elem.getAttribute(name) === value || elem.setAttribute(name, value); 58 66 elem.nodeValue; 67 + // For certain types of elements, we need to do some extra work to ensure the element’s state matches the guide’s state. 59 68 if (isInput(elem) && isInput(guide)) { 60 69 if (elem.checked !== guide.checked) 61 70 elem.checked = guide.checked; ··· 76 85 text.textContent = guide.value; 77 86 } 78 87 } 88 + // Iterates over the child nodes of the guide element, morphing the main element’s child nodes to match. 79 89 function morphChildNodes(elem, guide, idMap) { 80 90 const childNodes = [...elem.childNodes]; 81 91 const guideChildNodes = [...guide.childNodes]; ··· 89 99 else if (child) 90 100 child.remove(); 91 101 } 102 + // Remove any excess child nodes from the main element. This is separate because 103 + // the loop above might modify the length of the main element’s child nodes. 92 104 while (elem.childNodes.length > guide.childNodes.length) 93 105 elem.lastChild?.remove(); 94 106 } ··· 100 112 } 101 113 function morphChildElement(child, guide, parent, idMap) { 102 114 const guideIdSet = idMap.get(guide); 115 + // Generate the array in advance of the loop 103 116 const guideSetArray = guideIdSet ? [...guideIdSet] : []; 104 117 let currentNode = child; 105 118 let nextMatchByTagName = null; 119 + // Try find a match by idSet, while also looking out for the next best match by tagName. 106 120 while (currentNode) { 107 121 if (isElement(currentNode)) { 108 122 if (currentNode.id === guide.id) { ··· 125 139 else 126 140 child.replaceWith(guide.cloneNode(true)); 127 141 } 142 + // We cannot use `instanceof` when nodes might be from different documents, 143 + // so we use type guards instead. This keeps TypeScript happy, while doing 144 + // the necessary checks at runtime. 128 145 function isText(node) { 129 146 return node.nodeType === 3; 130 147 }
+1 -1
tsconfig.json
··· 7 7 "rootDir": "src", 8 8 "strict": true, 9 9 "target": "es2020", 10 - "removeComments": true, 10 + "removeComments": false, 11 11 "outDir": "dist", 12 12 "baseUrl": ".", 13 13 "noEmit": false,