From 7ef5803f0594ccd195041ce3453fbb8bba943e81 Mon Sep 17 00:00:00 2001 From: Joel Drapper Date: Sun, 9 Nov 2025 13:51:20 +0000 Subject: [PATCH 1/4] Only flag dirty inputs if preserveChanges is disabled --- src/morphlex.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/morphlex.ts b/src/morphlex.ts index 12da403..e9e5945 100644 --- a/src/morphlex.ts +++ b/src/morphlex.ts @@ -129,7 +129,7 @@ export function morphDocument(from: Document, to: Document | string, options?: O export function morph(from: ChildNode, to: ChildNode | NodeListOf | string, options: Options = {}): void { if (typeof to === "string") to = parseFragment(to).childNodes - if (isParentNode(from)) flagDirtyInputs(from) + if (!options.preserveChanges && isParentNode(from)) flagDirtyInputs(from) new Morph(options).morph(from, to) } From 2faefaf53aa57da38c6477cab982f10cdae82cb9 Mon Sep 17 00:00:00 2001 From: Joel Drapper Date: Sun, 9 Nov 2025 14:01:00 +0000 Subject: [PATCH 2/4] Use idSets for one side --- src/morphlex.ts | 79 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 26 deletions(-) diff --git a/src/morphlex.ts b/src/morphlex.ts index e9e5945..dd2c2c4 100644 --- a/src/morphlex.ts +++ b/src/morphlex.ts @@ -9,7 +9,8 @@ const unmatchedNodes: Set = new Set() const unmatchedElements: Set = new Set() const whitespaceNodes: Set = new Set() -type IdMap = WeakMap> +type IdSetMap = WeakMap> +type IdArrayMap = WeakMap> /** * Configuration options for morphing operations. @@ -214,7 +215,8 @@ function moveBefore(parent: ParentNode, node: ChildNode, insertionPoint: ChildNo } class Morph { - readonly #idMap: IdMap = new WeakMap() + readonly #idArrayMap: IdArrayMap = new WeakMap() + readonly #idSetMap: IdSetMap = new WeakMap() readonly #options: Options constructor(options: Options = {}) { @@ -284,10 +286,10 @@ class Morph { } if (to instanceof NodeList) { - this.#mapIdSetsForEach(to) + this.#mapIdArraysForEach(to) this.#morphOneToMany(from, to) } else if (isParentNode(to)) { - this.#mapIdSets(to) + this.#mapIdArrays(to) this.#morphOneToOne(from, to) } } @@ -509,9 +511,9 @@ class Morph { const element = toChildNodes[unmatchedIndex] as Element const id = element.id - const idSet = this.#idMap.get(element) + const idArray = this.#idArrayMap.get(element) - if (id === "" && !idSet) continue + if (id === "" && !idArray) continue candidateLoop: for (const candidateIndex of candidateElements) { const candidate = fromChildNodes[candidateIndex] as Element @@ -525,20 +527,18 @@ class Morph { break candidateLoop } - // Match by idSet - if (idSet) { - const candidateIdSet = this.#idMap.get(candidate) + // Match by idArray (to) against idSet (from) + if (idArray) { + const candidateIdSet = this.#idSetMap.get(candidate) if (candidateIdSet) { - for (let i = 0; i < idSet.length; i++) { - const setId = idSet[i]! - for (let k = 0; k < candidateIdSet.length; k++) { - if (candidateIdSet[k] === setId) { - matches[unmatchedIndex] = candidateIndex - seq[candidateIndex] = unmatchedIndex - candidateElements.delete(candidateIndex) - unmatchedElements.delete(unmatchedIndex) - break candidateLoop - } + for (let i = 0; i < idArray.length; i++) { + const arrayId = idArray[i]! + if (candidateIdSet.has(arrayId)) { + matches[unmatchedIndex] = candidateIndex + seq[candidateIndex] = unmatchedIndex + candidateElements.delete(candidateIndex) + unmatchedElements.delete(unmatchedIndex) + break candidateLoop } } } @@ -687,17 +687,41 @@ class Morph { } } - #mapIdSetsForEach(nodeList: NodeList): void { + #mapIdArraysForEach(nodeList: NodeList): void { for (const childNode of nodeList) { if (isParentNode(childNode)) { - this.#mapIdSets(childNode) + this.#mapIdArrays(childNode) } } } - // For each node with an ID, push that ID into the IdSet on the IdMap, for each of its parent elements. + // For each node with an ID, push that ID into the IdArray on the IdArrayMap, for each of its parent elements. + #mapIdArrays(node: ParentNode): void { + const idArrayMap = this.#idArrayMap + + for (const element of node.querySelectorAll("[id]")) { + const id = element.id + + if (id === "") continue + + let currentElement: Element | null = element + + while (currentElement) { + const idArray = idArrayMap.get(currentElement) + if (idArray) { + idArray.push(id) + } else { + idArrayMap.set(currentElement, [id]) + } + if (currentElement === node) break + currentElement = currentElement.parentElement + } + } + } + + // For each node with an ID, add that ID into the IdSet on the IdSetMap, for each of its parent elements. #mapIdSets(node: ParentNode): void { - const idMap = this.#idMap + const idSetMap = this.#idSetMap for (const element of node.querySelectorAll("[id]")) { const id = element.id @@ -707,9 +731,12 @@ class Morph { let currentElement: Element | null = element while (currentElement) { - const idSet: Array | undefined = idMap.get(currentElement) - if (idSet) idSet.push(id) - else idMap.set(currentElement, [id]) + const idSet = idSetMap.get(currentElement) + if (idSet) { + idSet.add(id) + } else { + idSetMap.set(currentElement, new Set([id])) + } if (currentElement === node) break currentElement = currentElement.parentElement } From 9e66fc855928896e6a3cb49dc80bb4f07a298e49 Mon Sep 17 00:00:00 2001 From: Joel Drapper Date: Sun, 9 Nov 2025 14:04:45 +0000 Subject: [PATCH 3/4] Simplify attribute loops --- src/morphlex.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/morphlex.ts b/src/morphlex.ts index dd2c2c4..8d922e0 100644 --- a/src/morphlex.ts +++ b/src/morphlex.ts @@ -367,9 +367,7 @@ class Morph { } // First pass: update/add attributes from reference (iterate forwards) - const toAttributes = to.attributes - for (let i = 0; i < toAttributes.length; i++) { - const { name, value } = toAttributes[i]! + for (const { name, value } of to.attributes) { if (name === "value") { if (isInputElement(from) && from.value !== value) { if (!this.#options.preserveChanges || from.value === from.defaultValue) { @@ -402,12 +400,8 @@ class Morph { } } - const fromAttrs = from.attributes - - // Second pass: remove excess attributes (iterate backwards for efficiency) - for (let i = fromAttrs.length - 1; i >= 0; i--) { - const { name, value } = fromAttrs[i]! - + // Second pass: remove excess attributes + for (const { name, value } of from.attributes) { if (!to.hasAttribute(name)) { if (name === "selected") { if (isOptionElement(from) && from.selected) { From 2621f10103a692a7b2b79904a3bc665ea78c5ca9 Mon Sep 17 00:00:00 2001 From: Joel Drapper Date: Sun, 9 Nov 2025 14:25:53 +0000 Subject: [PATCH 4/4] Bump deps --- bun.lockb | Bin 65221 -> 65221 bytes package.json | 16 ++++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bun.lockb b/bun.lockb index 5f68f7a198fab6a74b5f7373144a9e7f386ef1b9..92e3ec3101c4390631cb1fa23f0fc8ac34493a5a 100755 GIT binary patch delta 5120 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 diff --git a/package.json b/package.json index a58ec4e..91863a7 100644 --- a/package.json +++ b/package.json @@ -27,17 +27,17 @@ "serve": "bun --bun vite --open /benchmark/" }, "devDependencies": { - "@types/bun": "^1.3.1", - "@typescript/native-preview": "^7.0.0-dev.20251104.1", - "@vitest/browser": "^4.0.6", - "@vitest/browser-playwright": "^4.0.6", - "@vitest/coverage-v8": "^4.0.6", - "@vitest/ui": "^4.0.6", + "@types/bun": "^1.3.2", + "@typescript/native-preview": "^7.0.0-dev.20251109.1", + "@vitest/browser": "^4.0.8", + "@vitest/browser-playwright": "^4.0.8", + "@vitest/coverage-v8": "^4.0.8", + "@vitest/ui": "^4.0.8", "happy-dom": "^20.0.10", - "oxlint": "^1.25.0", + "oxlint": "^1.26.0", "oxlint-tsgolint": "^0.4.0", "prettier": "^3.6.2", "typescript": "^5.9.3", - "vitest": "^4.0.6" + "vitest": "^4.0.8" } }