Precise DOM morphing
morphing typescript dom
0
fork

Configure Feed

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

Further performance improvements

+90 -42
+47 -22
dist/morphlex.js
··· 1 1 export function morph(node, guide) { 2 2 const idMap = new Map(); 3 3 if (isParentNode(node) && isParentNode(guide)) { 4 - populateIdMapForNode(node, idMap); 5 - populateIdMapForNode(guide, idMap); 4 + populateIdSets(node, idMap); 5 + populateIdSets(guide, idMap); 6 6 } 7 7 morphNodes(node, guide, idMap); 8 8 } 9 - function populateIdMapForNode(node, idMap) { 9 + function populateIdSets(node, idMap) { 10 10 const elementsWithIds = node.querySelectorAll("[id]"); 11 11 for (const elementWithId of elementsWithIds) { 12 12 const id = elementWithId.id; ··· 25 25 function morphNodes(node, guide, idMap, insertBefore, parent) { 26 26 if (parent && insertBefore && insertBefore !== node) 27 27 parent.insertBefore(guide, insertBefore); 28 - if (isText(node) && isText(guide)) { 29 - if (node.textContent !== guide.textContent) 30 - node.textContent = guide.textContent; 28 + if (isElement(node) && isElement(guide) && node.tagName === guide.tagName) { 29 + if (!isInput(node) && !isOption(node) && !isTextArea(node) && node.isEqualNode(guide)) 30 + return; 31 + else { 32 + if (node.hasAttributes() || guide.hasAttributes()) 33 + morphAttributes(node, guide); 34 + if (node.hasChildNodes() || guide.hasChildNodes()) 35 + morphChildNodes(node, guide, idMap); 36 + } 31 37 } 32 - else if (isElement(node) && isElement(guide) && node.tagName === guide.tagName) { 33 - if (node.hasAttributes() || guide.hasAttributes()) 34 - morphAttributes(node, guide); 35 - if (node.hasChildNodes() || guide.hasChildNodes()) 36 - morphChildNodes(node, guide, idMap); 38 + else { 39 + if (node.isEqualNode(guide)) 40 + return; 41 + else if (isText(node) && isText(guide)) { 42 + if (node.textContent !== guide.textContent) 43 + node.textContent = guide.textContent; 44 + } 45 + else if (isComment(node) && isComment(guide)) { 46 + if (node.nodeValue !== guide.nodeValue) 47 + node.nodeValue = guide.nodeValue; 48 + } 49 + else 50 + node.replaceWith(guide.cloneNode(true)); 37 51 } 38 - else 39 - node.replaceWith(guide.cloneNode(true)); 40 52 } 41 53 function morphAttributes(elem, guide) { 42 54 for (const { name } of elem.attributes) 43 55 guide.hasAttribute(name) || elem.removeAttribute(name); 44 56 for (const { name, value } of guide.attributes) 45 57 elem.getAttribute(name) === value || elem.setAttribute(name, value); 46 - if (isInput(elem) && isInput(guide) && elem.value !== guide.value) 47 - elem.value = guide.value; 58 + elem.nodeValue; 59 + if (isInput(elem) && isInput(guide) && elem.type !== "file") { 60 + if (elem.value !== guide.value) 61 + elem.value = guide.value; 62 + if (elem.checked !== guide.checked) 63 + elem.checked = guide.checked; 64 + if (elem.disabled !== guide.disabled) 65 + elem.disabled = guide.disabled; 66 + } 48 67 else if (isOption(elem) && isOption(guide) && elem.selected !== guide.selected) 49 68 elem.selected = guide.selected; 50 - else if (isTextArea(elem) && isTextArea(guide) && elem.value !== guide.value) 51 - elem.value = guide.value; 69 + else if (isTextArea(elem) && isTextArea(guide)) { 70 + if (elem.value !== guide.value) 71 + elem.value = guide.value; 72 + const text = elem.firstChild; 73 + if (text && text.textContent !== guide.value) 74 + text.textContent = guide.value; 75 + } 52 76 } 53 77 function morphChildNodes(elem, guide, idMap) { 54 78 const childNodes = [...elem.childNodes]; ··· 100 124 child.replaceWith(guide.cloneNode(true)); 101 125 } 102 126 function isText(node) { 103 - return node.nodeType === Node.TEXT_NODE; 127 + return node.nodeType === 3; 128 + } 129 + function isComment(node) { 130 + return node.nodeType === 8; 104 131 } 105 132 function isElement(node) { 106 - return node.nodeType === Node.ELEMENT_NODE; 133 + return node.nodeType === 1; 107 134 } 108 135 function isInput(element) { 109 136 return element.localName === "input"; ··· 115 142 return element.localName === "textarea"; 116 143 } 117 144 function isParentNode(node) { 118 - return (node.nodeType === Node.ELEMENT_NODE || 119 - node.nodeType === Node.DOCUMENT_NODE || 120 - node.nodeType === Node.DOCUMENT_FRAGMENT_NODE); 145 + return node.nodeType === 1 || node.nodeType === 9 || node.nodeType === 11; 121 146 } 122 147 //# sourceMappingURL=morphlex.js.map
+43 -20
src/morphlex.ts
··· 5 5 const idMap: IdMap = new Map(); 6 6 7 7 if (isParentNode(node) && isParentNode(guide)) { 8 - populateIdMapForNode(node, idMap); 9 - populateIdMapForNode(guide, idMap); 8 + populateIdSets(node, idMap); 9 + populateIdSets(guide, idMap); 10 10 } 11 11 12 12 morphNodes(node, guide, idMap); 13 13 } 14 14 15 - // For each node with an ID, push that ID into the IDSet on the IDMap, for each of its parent elements. 16 - function populateIdMapForNode(node: ParentNode, idMap: IdMap): void { 15 + // For each node with an ID, push that ID into the IdSet on the IdMap, for each of its parent elements. 16 + function populateIdSets(node: ParentNode, idMap: IdMap): void { 17 17 const elementsWithIds: NodeListOf<Element> = node.querySelectorAll("[id]"); 18 18 19 19 for (const elementWithId of elementsWithIds) { 20 20 const id = elementWithId.id; 21 + 22 + // Ignore empty IDs 21 23 if (id === "") continue; 24 + 22 25 let current: Element | null = elementWithId; 23 26 24 27 while (current) { ··· 35 38 // TODO: We should extract this into a separate function. 36 39 if (parent && insertBefore && insertBefore !== node) parent.insertBefore(guide, insertBefore); 37 40 38 - if (isText(node) && isText(guide)) { 39 - if (node.textContent !== guide.textContent) node.textContent = guide.textContent; 40 - } else if (isElement(node) && isElement(guide) && node.tagName === guide.tagName) { 41 - if (node.hasAttributes() || guide.hasAttributes()) morphAttributes(node, guide); 42 - if (node.hasChildNodes() || guide.hasChildNodes()) morphChildNodes(node, guide, idMap); 43 - } else node.replaceWith(guide.cloneNode(true)); 41 + if (isElement(node) && isElement(guide) && node.tagName === guide.tagName) { 42 + // We need to check if the element is an input, option, or textarea here, because they have 43 + // special attributes not covered by the isEqualNode check. 44 + if (!isInput(node) && !isOption(node) && !isTextArea(node) && node.isEqualNode(guide)) return; 45 + else { 46 + if (node.hasAttributes() || guide.hasAttributes()) morphAttributes(node, guide); 47 + if (node.hasChildNodes() || guide.hasChildNodes()) morphChildNodes(node, guide, idMap); 48 + } 49 + } else { 50 + if (node.isEqualNode(guide)) return; 51 + else if (isText(node) && isText(guide)) { 52 + if (node.textContent !== guide.textContent) node.textContent = guide.textContent; 53 + } else if (isComment(node) && isComment(guide)) { 54 + if (node.nodeValue !== guide.nodeValue) node.nodeValue = guide.nodeValue; 55 + } else node.replaceWith(guide.cloneNode(true)); 56 + } 44 57 } 45 58 46 59 function morphAttributes(elem: Element, guide: Element): void { ··· 50 63 // Copy attributes from the guide to the element, if they don’t already match. 51 64 for (const { name, value } of guide.attributes) elem.getAttribute(name) === value || elem.setAttribute(name, value); 52 65 66 + elem.nodeValue; 67 + 53 68 // For certain types of elements, we need to do some extra work to ensure the element’s state matches the guide’s state. 54 - if (isInput(elem) && isInput(guide) && elem.value !== guide.value) elem.value = guide.value; 55 - else if (isOption(elem) && isOption(guide) && elem.selected !== guide.selected) elem.selected = guide.selected; 56 - else if (isTextArea(elem) && isTextArea(guide) && elem.value !== guide.value) elem.value = guide.value; 69 + if (isInput(elem) && isInput(guide) && elem.type !== "file") { 70 + if (elem.value !== guide.value) elem.value = guide.value; 71 + if (elem.checked !== guide.checked) elem.checked = guide.checked; 72 + if (elem.disabled !== guide.disabled) elem.disabled = guide.disabled; 73 + } else if (isOption(elem) && isOption(guide) && elem.selected !== guide.selected) elem.selected = guide.selected; 74 + else if (isTextArea(elem) && isTextArea(guide)) { 75 + if (elem.value !== guide.value) elem.value = guide.value; 76 + 77 + const text = elem.firstChild as Text | null; 78 + if (text && text.textContent !== guide.value) text.textContent = guide.value; 79 + } 57 80 } 58 81 59 82 // Iterates over the child nodes of the guide element, morphing the main element’s child nodes to match. ··· 117 140 // the necessary checks at runtime. 118 141 119 142 function isText(node: Node): node is Text { 120 - return node.nodeType === Node.TEXT_NODE; 143 + return node.nodeType === 3; 144 + } 145 + 146 + function isComment(node: Node): node is Comment { 147 + return node.nodeType === 8; 121 148 } 122 149 123 150 function isElement(node: Node): node is Element { 124 - return node.nodeType === Node.ELEMENT_NODE; 151 + return node.nodeType === 1; 125 152 } 126 153 127 154 function isInput(element: Element): element is HTMLInputElement { ··· 137 164 } 138 165 139 166 function isParentNode(node: Node): node is ParentNode { 140 - return ( 141 - node.nodeType === Node.ELEMENT_NODE || 142 - node.nodeType === Node.DOCUMENT_NODE || 143 - node.nodeType === Node.DOCUMENT_FRAGMENT_NODE 144 - ); 167 + return node.nodeType === 1 || node.nodeType === 9 || node.nodeType === 11; 145 168 }