Precise DOM morphing
morphing typescript dom
0
fork

Configure Feed

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

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,