Precise DOM morphing
morphing typescript dom

Fix infinite loop bug

+98 -5
+3 -5
src/morphlex.ts
··· 279 279 } 280 280 281 281 // Remove any excess nodes from the original 282 - while (from.childNodes.length > toChildNodes.length) { 283 - const lastChild = from.lastChild 284 - if (lastChild) { 285 - this.removeNode(lastChild) 286 - } 282 + // We iterate backwards through excess nodes and attempt to remove each one 283 + for (let i = from.childNodes.length - 1; i >= toChildNodes.length; i--) { 284 + this.removeNode(from.childNodes[i]!) 287 285 } 288 286 } 289 287
+95
test/morphlex-loops.test.ts
··· 253 253 }) 254 254 255 255 describe("Edge case loops", () => { 256 + it("should not infinite loop when beforeNodeRemoved returns false", () => { 257 + const parent = document.createElement("div") 258 + 259 + // Create a custom element parent 260 + const customElement = document.createElement("my-component") 261 + const child1 = document.createElement("span") 262 + child1.textContent = "child1" 263 + const child2 = document.createElement("span") 264 + child2.textContent = "child2" 265 + 266 + customElement.appendChild(child1) 267 + customElement.appendChild(child2) 268 + parent.appendChild(customElement) 269 + 270 + // Reference only has one child in the custom element 271 + const reference = document.createElement("div") 272 + const refCustomElement = document.createElement("my-component") 273 + const refChild1 = document.createElement("span") 274 + refChild1.textContent = "child1" 275 + refCustomElement.appendChild(refChild1) 276 + reference.appendChild(refCustomElement) 277 + 278 + const startTime = Date.now() 279 + 280 + // This should cause an infinite loop if not handled correctly 281 + // because child2 can't be removed (beforeNodeRemoved returns false) 282 + // but the algorithm keeps trying to remove it 283 + morph(parent, reference, { 284 + beforeNodeRemoved: (oldNode: Node) => { 285 + let parent = oldNode.parentElement 286 + 287 + while (parent) { 288 + if (parent.tagName && parent.tagName.includes("-")) return false 289 + parent = parent.parentElement 290 + } 291 + 292 + return true 293 + }, 294 + }) 295 + 296 + const endTime = Date.now() 297 + 298 + // Should complete quickly without infinite loop 299 + expect(endTime - startTime).toBeLessThan(1000) 300 + // child2 should still be there since it couldn't be removed 301 + expect(customElement.children.length).toBe(2) 302 + }) 303 + 304 + it("should remove removable nodes even when some nodes cannot be removed", () => { 305 + const parent = document.createElement("div") 306 + 307 + // Create a custom element parent 308 + const customElement = document.createElement("my-component") 309 + const child1 = document.createElement("span") 310 + child1.textContent = "child1" 311 + const child2 = document.createElement("span") 312 + child2.textContent = "child2" 313 + const child3 = document.createElement("span") 314 + child3.textContent = "child3" 315 + 316 + customElement.appendChild(child1) 317 + customElement.appendChild(child2) 318 + parent.appendChild(customElement) 319 + parent.appendChild(child3) // This one is outside the custom element 320 + 321 + // Reference only has child1 in custom element, no child3 322 + const reference = document.createElement("div") 323 + const refCustomElement = document.createElement("my-component") 324 + const refChild1 = document.createElement("span") 325 + refChild1.textContent = "child1" 326 + refCustomElement.appendChild(refChild1) 327 + reference.appendChild(refCustomElement) 328 + 329 + morph(parent, reference, { 330 + beforeNodeRemoved: (oldNode: Node) => { 331 + let parent = oldNode.parentElement 332 + 333 + while (parent) { 334 + if (parent.tagName && parent.tagName.includes("-")) return false 335 + parent = parent.parentElement 336 + } 337 + 338 + return true 339 + }, 340 + }) 341 + 342 + // child2 should still be there (inside custom element, can't be removed) 343 + expect(customElement.children.length).toBe(2) 344 + expect(customElement.children[1].textContent).toBe("child2") 345 + 346 + // child3 should be removed (outside custom element) 347 + expect(parent.children.length).toBe(1) 348 + expect(parent.children[0]).toBe(customElement) 349 + }) 350 + 256 351 it("should not infinite loop when node equals insertionPoint", () => { 257 352 const parent = document.createElement("div") 258 353 const child = document.createElement("span")