···11+{
22+ "name": "morphlite",
33+ "version": "0.0.1",
44+ "description": "Morphlite is an attempt to create a DOM morphing function in less than 100 lines of code to use with [HTMZ](https://leanrada.com/htmz/) in small projects.",
55+ "main": "dist/morphlite.js",
66+ "scripts": {
77+ "test": "web-test-runner test/**/*.test.js --node-resolve",
88+ "build": "tsc",
99+ "watch": "tsc -w",
1010+ "test:watch": "npm run test -- --watch"
1111+ },
1212+ "author": "Joel Drapper",
1313+ "license": "MIT",
1414+ "devDependencies": {
1515+ "@open-wc/testing": "^3.0.0-next.5",
1616+ "@web/test-runner": "^0.18.0",
1717+ "typescript": "^5.3.3"
1818+ }
1919+}
+120
src/morphlite.ts
···11+type IdSet = Set<string>;
22+type IdMap = Map<Node, IdSet>;
33+44+export function morph(from: Node, to: Node): void {
55+ const idMap: IdMap = new Map();
66+77+ if (isElement(from) && isElement(to)) {
88+ populateIdMapForNode(from, idMap);
99+ populateIdMapForNode(to, idMap);
1010+ }
1111+1212+ morphNodes(from, to, idMap);
1313+}
1414+1515+function morphNodes(from: Node, to: Node, idMap: IdMap, insertBefore?: Node, parent?: Node): void {
1616+ if (parent && insertBefore && insertBefore !== from) parent.insertBefore(to, insertBefore);
1717+1818+ if (isText(from) && isText(to)) {
1919+ if (from.textContent !== to.textContent) from.textContent = to.textContent;
2020+ } else if (isElement(from) && isElement(to)) {
2121+ if (from.tagName === to.tagName) {
2222+ if (to.attributes.length > 0) morphAttributes(from, to);
2323+ if (to.childNodes.length > 0) morphChildNodes(from, to, idMap);
2424+ } else from.replaceWith(to.cloneNode(true));
2525+ } else {
2626+ throw new Error(
2727+ `Cannot morph nodes of different types: from is ${from.constructor.name}, to is ${to.constructor.name}`,
2828+ );
2929+ }
3030+}
3131+3232+function morphAttributes(from: Element, to: Element): void {
3333+ for (const { name } of from.attributes) to.hasAttribute(name) || from.removeAttribute(name);
3434+ for (const { name, value } of to.attributes) from.getAttribute(name) !== value && from.setAttribute(name, value);
3535+3636+ if (isInput(from) && isInput(to)) from.value = to.value;
3737+ if (isOption(from) && isOption(to)) from.selected = to.selected;
3838+ if (isTextArea(from) && isTextArea(to)) from.value = to.value;
3939+}
4040+4141+function morphChildNodes(from: Element, to: Element, idMap: IdMap): void {
4242+ for (let i = 0; i < to.childNodes.length; i++) {
4343+ const childA = [...from.childNodes].at(i);
4444+ const childB = [...to.childNodes].at(i);
4545+4646+ if (childA && childB) morphChildNode(childA, childB, idMap, from);
4747+ else if (childB) from.appendChild(childB.cloneNode(true));
4848+ }
4949+5050+ console.log("Got here ", from.childNodes.length, to.childNodes.length, from.childNodes[0]);
5151+ while (from.childNodes.length > to.childNodes.length) from.lastChild?.remove();
5252+}
5353+5454+function morphChildNode(from: ChildNode, to: ChildNode, idMap: IdMap, parent: Element): void {
5555+ if (isElement(from) && isElement(to)) {
5656+ let current: ChildNode | null = from;
5757+ let nextBestMatch: ChildNode | null = null;
5858+5959+ while (current && isElement(current)) {
6060+ if (current.id !== "" && current.id === to.id) {
6161+ morphNodes(current, to, idMap, from, parent);
6262+ break;
6363+ } else {
6464+ const setA = idMap.get(current);
6565+ const setB = idMap.get(to);
6666+6767+ if (setA && setB && numberOfItemsInCommon(setA, setB) > 0) {
6868+ return morphNodes(current, to, idMap, from, parent);
6969+ } else if (!nextBestMatch && current.tagName === to.tagName) {
7070+ nextBestMatch = current;
7171+ }
7272+ }
7373+7474+ current = current.nextSibling;
7575+ }
7676+7777+ if (nextBestMatch) morphNodes(nextBestMatch, to, idMap, from, parent);
7878+ else from.replaceWith(to.cloneNode(true));
7979+ } else morphNodes(from, to, idMap);
8080+}
8181+8282+function populateIdMapForNode(node: ParentNode, idMap: IdMap): void {
8383+ const parent: HTMLElement | null = node.parentElement;
8484+ const elements: NodeListOf<Element> = node.querySelectorAll("[id]");
8585+8686+ for (const element of elements) {
8787+ if (element.id === "") continue;
8888+ let current: Element | null = element;
8989+9090+ while (current && current !== parent) {
9191+ const idSet: IdSet | undefined = idMap.get(current);
9292+ idSet ? idSet.add(element.id) : idMap.set(current, new Set([element.id]));
9393+ current = current.parentElement;
9494+ }
9595+ }
9696+}
9797+9898+function numberOfItemsInCommon<T>(a: Set<T>, b: Set<T>): number {
9999+ return [...a].filter((item) => b.has(item)).length;
100100+}
101101+102102+function isElement(node: Node): node is Element {
103103+ return node.nodeType === 1;
104104+}
105105+106106+function isText(node: Node): node is Text {
107107+ return node.nodeType === 3;
108108+}
109109+110110+function isInput(element: Element): element is HTMLInputElement {
111111+ return element.localName === "input";
112112+}
113113+114114+function isOption(element: Element): element is HTMLOptionElement {
115115+ return element.localName === "option";
116116+}
117117+118118+function isTextArea(element: Element): element is HTMLTextAreaElement {
119119+ return element.localName === "textarea";
120120+}