Precise DOM morphing
morphing typescript dom
0
fork

Configure Feed

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

Fixes

+39 -25
+1
AGENTS.md
··· 4 4 - Try to maintain 100% test coverage. Use `bun run test --coverage`. 5 5 - I’m using `jj` so you can use that to look at your diff, but please don’t commit unless I ask you to. 6 6 - Make sure you leave things in a good state. No diagnostics warnings. No type errors. 7 + - We use tabs for indentation and spaces for alignment
+26 -16
src/morphlex.ts
··· 35 35 if (typeof to === "string") { 36 36 const fragment = parseString(to) 37 37 38 - if (fragment.firstChild && fragment.childNodes.length === 1) { 38 + if (fragment.firstChild && fragment.childNodes.length === 1 && isElement(fragment.firstChild)) { 39 39 to = fragment.firstChild 40 40 } else { 41 41 throw new Error("[Morphlex] The string was not a valid HTML element.") ··· 46 46 if (isElementPair(pair) && isMatchingElementPair(pair)) { 47 47 new Morph(options).morphChildren(pair) 48 48 } else { 49 - throw new Error("[Morphlex] The nodes are not matching elements.") 49 + throw new Error("[Morphlex] You can only do an inner morph with matching elements.") 50 50 } 51 51 } 52 52 ··· 254 254 255 255 private morphChildNodes([from, to]: PairOfMatchingElements<Element>): void { 256 256 const fromChildNodes = from.childNodes 257 - const toChildNodes = to.childNodes 257 + const toChildNodes = Array.from(to.childNodes) 258 258 259 + // Process all reference nodes 259 260 for (let i = 0; i < toChildNodes.length; i++) { 260 261 const fromChildNode = fromChildNodes[i] 261 262 const toChildNode = toChildNodes[i]! 262 263 263 - if (fromChildNode && toChildNode) { 264 + if (fromChildNode) { 264 265 if (isElement(toChildNode)) { 265 266 this.searchSiblingsToMorphChildElement(fromChildNode, toChildNode, from) 266 267 } else { 267 - // TODO 268 + this.morphOneToOne(fromChildNode, toChildNode) 269 + } 270 + } else { 271 + // Add new node at the end 272 + if (this.options.beforeNodeAdded?.(toChildNode) ?? true) { 273 + moveBefore(from, toChildNode, null) 274 + this.options.afterNodeAdded?.(toChildNode) 268 275 } 269 - } else if (toChildNode) { 270 - this.appendChild(from, toChildNode) 271 - } else if (fromChildNode) { 272 - this.removeNode(fromChildNode) 276 + } 277 + } 278 + 279 + // Remove any excess nodes from the original 280 + while (from.childNodes.length > toChildNodes.length) { 281 + const lastChild = from.lastChild 282 + if (lastChild) { 283 + this.removeNode(lastChild) 273 284 } 274 285 } 275 286 } ··· 311 322 } 312 323 313 324 if (bestMatch) { 314 - if (!(this.options.beforeNodeMorphed?.(bestMatch, to) ?? true)) return 315 - moveBefore(parent, bestMatch, from) 316 - this.options.afterNodeMorphed?.(bestMatch, to) 317 - this.morphMatchingElements([bestMatch, to] as PairOfMatchingElements<Element>) 325 + if (bestMatch !== from) { 326 + moveBefore(parent, bestMatch, from) 327 + } 328 + this.morphOneToOne(bestMatch, to) 318 329 } else { 319 330 this.morphOneToOne(from, to) 320 331 } ··· 331 342 332 343 private replaceNode(node: ChildNode, newNode: ChildNode): void { 333 344 if (this.options.beforeNodeAdded?.(newNode) ?? true) { 334 - moveBefore(node.parentNode || document, node, newNode) 345 + moveBefore(node.parentNode || document, newNode, node) 335 346 this.options.afterNodeAdded?.(newNode) 347 + this.removeNode(node) 336 348 } 337 - 338 - this.removeNode(node) 339 349 } 340 350 341 351 private appendChild(parent: ParentNode, newChild: ChildNode): void {
+12 -9
test/morphlex-edge-cases.test.ts
··· 7 7 const div = document.createElement("div") 8 8 div.innerHTML = "<span>test</span>" 9 9 10 - // Empty string should throw an error because doc.body.firstChild will be null 11 - expect(() => morph(div.firstChild!, "")).toThrow("[Morphlex] The string was not a valid HTML node.") 10 + // Empty string should remove the node (morphing to empty NodeList) 11 + morph(div.firstChild!, "") 12 + expect(div.firstChild).toBeNull() 12 13 }) 13 14 14 15 it("should throw error when morphInner receives empty string", () => { ··· 16 17 div.innerHTML = "<span>content</span>" 17 18 18 19 // morphInner with empty string should throw an error 19 - // because parseChildNodeFromString returns null for empty body 20 - expect(() => morphInner(div, "")).toThrow("[Morphlex] The string was not a valid HTML node.") 20 + // because parseString returns empty fragment 21 + expect(() => morphInner(div, "")).toThrow("[Morphlex] The string was not a valid HTML element.") 21 22 }) 22 23 23 24 it("should work with valid HTML strings", () => { 24 - const div = document.createElement("div") 25 - div.innerHTML = "<span>old</span>" 25 + const parent = document.createElement("div") 26 + const span = document.createElement("span") 27 + span.textContent = "old" 28 + parent.appendChild(span) 26 29 27 - expect(() => morph(div.firstChild!, "<div>new</div>")).not.toThrow() 28 - expect(div.firstChild?.nodeName).toBe("DIV") 29 - expect(div.firstChild?.textContent).toBe("new") 30 + morph(span, "<div>new</div>") 31 + expect(parent.firstChild?.nodeName).toBe("DIV") 32 + expect(parent.firstChild?.textContent).toBe("new") 30 33 }) 31 34 }) 32 35