Precise DOM morphing
morphing typescript dom
0
fork

Configure Feed

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

Initial MorphLite sketch

+111
+1
README.md
··· 1 + # MorphLite
+100
morphlite.ts
··· 1 + type IdSet = Set<string>; 2 + type IdMap = Map<Node, IdSet>; 3 + 4 + export default function tinyMorph(from: Node, to: Node): void { 5 + const idMap: IdMap = new Map(); 6 + 7 + if (from instanceof Element && to instanceof Element) { 8 + populateIdMapForNode(from, idMap); 9 + populateIdMapForNode(to, idMap); 10 + } 11 + 12 + morph(from, to, idMap); 13 + } 14 + 15 + function morph(from: Node, to: Node, idMap: IdMap, insertBefore?: Node, parent?: Node): void { 16 + idMap.delete(from); 17 + idMap.delete(to); 18 + 19 + if (parent && insertBefore && insertBefore !== from) parent.insertBefore(to, insertBefore); 20 + 21 + if (from instanceof Text && to instanceof Text) { 22 + from.textContent = to.textContent; 23 + } else if (from instanceof Element && to instanceof Element) { 24 + if (from.tagName === to.tagName) { 25 + if (to.attributes.length > 0) morphAttributes(from, to); 26 + if (to.childNodes.length > 0) morphChildNodes(from, to, idMap); 27 + } else { 28 + from.replaceWith(to.cloneNode(true)); 29 + } 30 + } 31 + } 32 + 33 + function morphAttributes(from: Element, to: Element): void { 34 + for (const { name } of from.attributes) to.hasAttribute(name) || from.removeAttribute(name); 35 + for (const { name, value } of to.attributes) from.getAttribute(name) !== value && from.setAttribute(name, value); 36 + 37 + if (from instanceof HTMLInputElement && to instanceof HTMLInputElement) from.value = to.value; 38 + if (from instanceof HTMLOptionElement && to instanceof HTMLOptionElement) from.selected = to.selected; 39 + if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) from.value = to.value; 40 + } 41 + 42 + function morphChildNodes(from: Element, to: Element, idMap: IdMap): void { 43 + for (let i = 0; i < to.childNodes.length; i++) { 44 + const childA = from.childNodes[i] as ChildNode | undefined; 45 + const childB = to.childNodes[i] as ChildNode | undefined; 46 + 47 + if (childA && childB) morphChildNode(childA, childB, idMap, from); 48 + else if (childB) from.appendChild(childB.cloneNode(true)); 49 + } 50 + 51 + while (from.childNodes.length > to.childNodes.length) from.lastChild?.remove(); 52 + } 53 + 54 + function morphChildNode(from: ChildNode, to: ChildNode, idMap: IdMap, parent: Element): void { 55 + if (from instanceof Element && to instanceof Element) { 56 + let current: ChildNode | null = from; 57 + let nextBestMatch: ChildNode | null = null; 58 + 59 + while (current && current instanceof Element) { 60 + if (current.id !== "" && current.id === to.id) { 61 + morph(current, to, idMap, from, parent); 62 + break; 63 + } else { 64 + const setA = idMap.get(current) as IdSet | undefined; 65 + const setB = idMap.get(to) as IdSet | undefined; 66 + 67 + if (setA && setB && numberOfItemsInCommon(setA, setB) > 0) { 68 + return morph(current, to, idMap, from, parent); 69 + } else if (!nextBestMatch && current.tagName === to.tagName) { 70 + nextBestMatch = current; 71 + } 72 + } 73 + 74 + current = current.nextSibling; 75 + } 76 + 77 + if (nextBestMatch) morph(nextBestMatch, to, idMap, from, parent); 78 + else from.replaceWith(to.cloneNode(true)); 79 + } else morph(from, to, idMap); 80 + } 81 + 82 + function populateIdMapForNode(node: ParentNode, idMap: IdMap): void { 83 + const parent: HTMLElement | null = node.parentElement; 84 + const elements: NodeListOf<Element> = node.querySelectorAll("[id]"); 85 + 86 + for (const element of elements) { 87 + if (element.id === "") continue; 88 + let current: Element | null = element; 89 + 90 + while (current && current !== parent) { 91 + const idSet: IdSet | undefined = idMap.get(current); 92 + idSet ? idSet.add(element.id) : idMap.set(current, new Set([element.id])); 93 + current = current.parentElement; 94 + } 95 + } 96 + } 97 + 98 + function numberOfItemsInCommon<T>(a: Set<T>, b: Set<T>): number { 99 + return [...a].filter((item) => b.has(item)).length; 100 + }
+10
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "noEmit": true, 4 + "strict": true, 5 + "target": "es2015", 6 + "noImplicitOverride": true, 7 + "allowUnreachableCode": false 8 + }, 9 + "include": ["app/frontend/entrypoints/**/*"] 10 + }