Precise DOM morphing
morphing
typescript
dom
1import { describe, expect, test } from "vitest"
2import { morph } from "../../src/morphlex"
3import { dom } from "./utils"
4
5describe("active element preservation", () => {
6 test("applies focused input attribute updates immediately", () => {
7 const input = dom('<input type="text" value="hello world">') as HTMLInputElement
8 document.body.appendChild(input)
9
10 input.focus()
11 input.setSelectionRange(2, 5)
12
13 const next = dom('<input type="text" value="server value" class="new">') as HTMLInputElement
14
15 morph(input, next, { preserveChanges: false })
16
17 expect(document.activeElement).toBe(input)
18 expect(input.value).toBe("server value")
19 expect(input.getAttribute("value")).toBe("server value")
20 expect(input.className).toBe("new")
21
22 input.remove()
23 })
24
25 test("does not defer focused descendant updates until blur", () => {
26 const wrapper = document.createElement("div")
27 wrapper.innerHTML = '<input id="name" value="hello" class="old"><button id="next">next</button>'
28 document.body.appendChild(wrapper)
29
30 const input = wrapper.querySelector("#name") as HTMLInputElement
31 const nextButton = wrapper.querySelector("#next") as HTMLButtonElement
32
33 input.value = "user typed"
34 input.focus()
35
36 const targetWrapper = document.createElement("div")
37 targetWrapper.innerHTML = '<input id="name" value="server" class="new"><button id="next">next</button>'
38
39 morph(wrapper, targetWrapper, { preserveChanges: false })
40
41 expect(input.value).toBe("server")
42 expect(input.defaultValue).toBe("server")
43 expect(input.className).toBe("new")
44
45 nextButton.focus()
46
47 expect(input.value).toBe("server")
48 expect(input.defaultValue).toBe("server")
49 expect(input.getAttribute("value")).toBe("server")
50 expect(input.className).toBe("new")
51
52 wrapper.remove()
53 })
54
55 test("replaces active contenteditable element", () => {
56 const parent = document.createElement("div")
57 const from = document.createElement("div")
58 from.contentEditable = "true"
59 from.textContent = "user text"
60 parent.appendChild(from)
61 document.body.appendChild(parent)
62
63 from.focus()
64
65 const to = document.createElement("p")
66 to.textContent = "server text"
67
68 morph(from, to)
69
70 expect(parent.firstElementChild).toBe(to)
71 expect(to.textContent).toBe("server text")
72
73 parent.remove()
74 })
75
76 test("updates focused input when preserveChanges is disabled", () => {
77 const input = dom('<input type="text" value="hello world">') as HTMLInputElement
78 document.body.appendChild(input)
79
80 input.focus()
81
82 const next = dom('<input type="text" value="server value">') as HTMLInputElement
83
84 morph(input, next, { preserveChanges: false })
85
86 expect(input.value).toBe("server value")
87
88 input.remove()
89 })
90
91 test("allows moving active element while reordering", () => {
92 const from = document.createElement("div")
93 from.innerHTML = '<input id="active"><p id="sibling">A</p>'
94 document.body.appendChild(from)
95
96 const active = from.querySelector("#active") as HTMLInputElement
97 active.focus()
98
99 const to = document.createElement("div")
100 to.innerHTML = '<p id="sibling">A</p><input id="active">'
101
102 morph(from, to)
103
104 expect(from.querySelector("#active")).toBe(active)
105 expect(from.firstElementChild?.id).toBe("sibling")
106 expect(from.lastElementChild?.id).toBe("active")
107 expect(document.activeElement).toBe(active)
108
109 from.remove()
110 })
111
112 test("allows replacing an ancestor that contains the active element", () => {
113 const host = document.createElement("div")
114 const from = document.createElement("div")
115 from.innerHTML = '<input id="active" value="hello"><span>old</span>'
116 host.appendChild(from)
117 document.body.appendChild(host)
118
119 const active = from.querySelector("#active") as HTMLInputElement
120 active.focus()
121
122 const to = document.createElement("section")
123 to.innerHTML = '<input id="active" value="server"><span>new</span>'
124
125 morph(from, to, { preserveChanges: false })
126
127 expect(host.firstElementChild?.tagName).toBe("SECTION")
128 expect((host.querySelector("#active") as HTMLInputElement).value).toBe("server")
129
130 host.remove()
131 })
132})