diff --git a/src/core/streams/stream_actions.js b/src/core/streams/stream_actions.js index 486dc8566..cb16a2435 100644 --- a/src/core/streams/stream_actions.js +++ b/src/core/streams/stream_actions.js @@ -3,6 +3,7 @@ import morph from "./actions/morph" export const StreamActions = { after() { + this.removeDuplicateTargetSiblings() this.targetElements.forEach((e) => e.parentElement?.insertBefore(this.templateContent, e.nextSibling)) }, @@ -12,6 +13,7 @@ export const StreamActions = { }, before() { + this.removeDuplicateTargetSiblings() this.targetElements.forEach((e) => e.parentElement?.insertBefore(this.templateContent, e)) }, diff --git a/src/elements/stream_element.js b/src/elements/stream_element.js index 77f447594..369e87ee4 100644 --- a/src/elements/stream_element.js +++ b/src/elements/stream_element.js @@ -75,6 +75,23 @@ export class StreamElement extends HTMLElement { return existingChildren.filter((c) => newChildrenIds.includes(c.id)) } + /** + * Removes duplicate siblings (by ID) + */ + removeDuplicateTargetSiblings() { + this.duplicateSiblings.forEach((c) => c.remove()) + } + + /** + * Gets the list of duplicate siblings (i.e. those with the same ID) + */ + get duplicateSiblings() { + const existingChildren = this.targetElements.flatMap((e) => [...e.parentElement.children]).filter((c) => !!c.id) + const newChildrenIds = [...(this.templateContent?.children || [])].filter((c) => !!c.id).map((c) => c.id) + + return existingChildren.filter((c) => newChildrenIds.includes(c.id)) + } + /** * Gets the action function to be performed. */ diff --git a/src/tests/unit/stream_element_tests.js b/src/tests/unit/stream_element_tests.js index 0d3e04b61..03d706647 100644 --- a/src/tests/unit/stream_element_tests.js +++ b/src/tests/unit/stream_element_tests.js @@ -157,6 +157,17 @@ test("action=after", async () => { assert.isNull(element.parentElement) }) + +test("action=after with children ID already present in target", async () => { + subject.fixtureHTML = `
Top
Middle
Bottom
` + const element = createStreamElement("after", "top", createTemplateElement('
New Middle
tail1 ')) + + subject.append(element) + await nextAnimationFrame() + + assert.equal(subject.find("#hello")?.textContent, "Top New Middle tail1 Bottom") +}) + test("action=before", async () => { const element = createStreamElement("before", "hello", createTemplateElement(`

Before Turbo

`)) assert.equal(subject.find("#hello")?.textContent, "Hello Turbo") @@ -170,6 +181,17 @@ test("action=before", async () => { assert.isNull(element.parentElement) }) + +test("action=before with children ID already present in target", async () => { + subject.fixtureHTML = `
Top
Middle
Bottom
` + const element = createStreamElement("before", "bottom", createTemplateElement('
New Middle
tail1 ')) + + subject.append(element) + await nextAnimationFrame() + + assert.equal(subject.find("#hello")?.textContent, "Top New Middle tail1 Bottom") +}) + test("test action=refresh", async () => { document.body.setAttribute("data-modified", "") assert.ok(document.body.hasAttribute("data-modified"))