Precise DOM morphing
morphing typescript dom

tests passing again

+57 -19
+7 -10
src/morphlex.ts
··· 185 } 186 187 private visitAttributes([from, to]: PairOfMatchingElements<Element>): void { 188 - const fromAttrs = from.attributes 189 190 // First pass: update/add attributes from reference (iterate forwards) 191 for (const { name, value } of to.attributes) { 192 - console.log(from, name) 193 - const oldValue = from.getAttribute(name) 194 - 195 - // This attribute was only added to trigger attribute morphing. 196 - if (name === "morphlex-dirty") { 197 - from.removeAttribute(name) 198 - continue 199 - } 200 - 201 if (name === "value") { 202 if (isInputElement(from) && from.value !== value) { 203 if (!this.options.preserveModified || from.value === from.defaultValue) { ··· 222 } 223 } 224 225 if (oldValue !== value && (this.options.beforeAttributeUpdated?.(from, name, value) ?? true)) { 226 from.setAttribute(name, value) 227 this.options.afterAttributeUpdated?.(from, name, oldValue) 228 } 229 } 230 231 // Second pass: remove excess attributes (iterate backwards for efficiency) 232 for (let i = fromAttrs.length - 1; i >= 0; i--) {
··· 185 } 186 187 private visitAttributes([from, to]: PairOfMatchingElements<Element>): void { 188 + if (from.hasAttribute("morphlex-dirty")) { 189 + from.removeAttribute("morphlex-dirty") 190 + } 191 192 // First pass: update/add attributes from reference (iterate forwards) 193 for (const { name, value } of to.attributes) { 194 if (name === "value") { 195 if (isInputElement(from) && from.value !== value) { 196 if (!this.options.preserveModified || from.value === from.defaultValue) { ··· 215 } 216 } 217 218 + const oldValue = from.getAttribute(name) 219 + 220 if (oldValue !== value && (this.options.beforeAttributeUpdated?.(from, name, value) ?? true)) { 221 from.setAttribute(name, value) 222 this.options.afterAttributeUpdated?.(from, name, oldValue) 223 } 224 } 225 + 226 + const fromAttrs = from.attributes 227 228 // Second pass: remove excess attributes (iterate backwards for efficiency) 229 for (let i = fromAttrs.length - 1; i >= 0; i--) {
+5 -5
test/morphlex-morphdom.test.ts
··· 115 116 morph(from, to) 117 118 - // Input values are no longer updated by morphlex 119 - expect((from as HTMLInputElement).value).toBe("Hello") 120 }) 121 122 it("should add disabled attribute to input", () => { ··· 182 183 morph(from, to) 184 185 - // Selected options are no longer updated by morphlex 186 const select = from as HTMLSelectElement 187 - expect(select.value).toBe("1") 188 - expect(select.options[1].selected).toBe(false) 189 }) 190 }) 191
··· 115 116 morph(from, to) 117 118 + // Input values are updated by default when not modified 119 + expect((from as HTMLInputElement).value).toBe("World") 120 }) 121 122 it("should add disabled attribute to input", () => { ··· 182 183 morph(from, to) 184 185 + // Selected options are updated by default when not modified 186 const select = from as HTMLSelectElement 187 + expect(select.value).toBe("2") 188 + expect(select.options[1].selected).toBe(true) 189 }) 190 }) 191
+5 -4
test/morphlex.browser.test.ts
··· 601 602 morph(select, referenceSelect) 603 604 - // Selected attributes are no longer updated 605 - expect(select.selectedOptions.length).toBe(1) 606 expect(select.selectedOptions[0].value).toBe("2") 607 }) 608 609 it("should handle script tags safely", () => { ··· 688 morph(form, referenceForm) 689 690 const radioB = form.querySelector("#radio-b") as HTMLInputElement 691 - // Checked attribute is removed but not added - radioA loses its checked state 692 expect(radioA.checked).toBe(false) 693 - expect(radioB.checked).toBe(false) 694 }) 695 696 it("should handle contenteditable elements", () => {
··· 601 602 morph(select, referenceSelect) 603 604 + // Selected attributes are updated by default when not modified 605 + expect(select.selectedOptions.length).toBe(2) 606 expect(select.selectedOptions[0].value).toBe("2") 607 + expect(select.selectedOptions[1].value).toBe("3") 608 }) 609 610 it("should handle script tags safely", () => { ··· 689 morph(form, referenceForm) 690 691 const radioB = form.querySelector("#radio-b") as HTMLInputElement 692 + // Checked attributes are updated by default when not modified 693 expect(radioA.checked).toBe(false) 694 + expect(radioB.checked).toBe(true) 695 }) 696 697 it("should handle contenteditable elements", () => {
+40
test/new.browser.test.ts
···
··· 1 + import { test, expect } from "vitest" 2 + import { morph } from "../src/morphlex" 3 + 4 + function parseHTML(html: string): HTMLElement { 5 + const tmp = document.createElement("div") 6 + tmp.innerHTML = html.trim() 7 + return tmp.firstChild as HTMLElement 8 + } 9 + 10 + test("morphing a modified input value with preserveModified enabled", () => { 11 + const a = parseHTML(`<input type="text" value="a">`) as HTMLInputElement 12 + const b = parseHTML(`<input type="text" value="b">`) as HTMLInputElement 13 + 14 + a.value = "new" 15 + morph(a, b, { preserveModified: true }) 16 + 17 + expect(a.outerHTML).toBe(`<input type="text" value="b">`) 18 + expect(a.value).toBe("new") 19 + }) 20 + 21 + test("morphing a modified input value preserveModified disabled", () => { 22 + const a = parseHTML(`<input type="text" value="a">`) as HTMLInputElement 23 + const b = parseHTML(`<input type="text" value="b">`) as HTMLInputElement 24 + 25 + a.value = "new" 26 + morph(a, b, { preserveModified: false }) 27 + 28 + expect(a.outerHTML).toBe(`<input type="text" value="b">`) 29 + expect(a.value).toBe("b") 30 + }) 31 + 32 + test("morphing an unmodified input value with preserveModified enabled", () => { 33 + const a = parseHTML(`<input type="text" value="a">`) as HTMLInputElement 34 + const b = parseHTML(`<input type="text" value="b">`) as HTMLInputElement 35 + 36 + morph(a, b, { preserveModified: true }) 37 + 38 + expect(a.outerHTML).toBe(`<input type="text" value="b">`) 39 + expect(a.value).toBe("b") 40 + })