Skip to content

Commit

Permalink
Merge pull request #113 from blopker/feature/allow-duplicate-ids
Browse files Browse the repository at this point in the history
Allow a single stream response to update multiple elements
  • Loading branch information
dhh committed Jun 17, 2021
2 parents a4a21af + 77d6943 commit 045833b
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 32 deletions.
20 changes: 10 additions & 10 deletions src/core/streams/stream_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,35 @@ import { StreamElement } from "../../elements/stream_element"

export const StreamActions: { [action: string]: (this: StreamElement) => void } = {
after() {
this.targetElement?.parentElement?.insertBefore(this.templateContent,this.targetElement.nextSibling)
this.targetElements.forEach(e => e.parentElement?.insertBefore(this.templateContent, e.nextSibling))
},

append() {
this.removeDuplicateTargetChildren()
this.targetElement?.append(this.templateContent)
this.targetElements.forEach(e => e.append(this.templateContent))
},

before() {
this.targetElement?.parentElement?.insertBefore(this.templateContent,this.targetElement)
this.targetElements.forEach(e => e.parentElement?.insertBefore(this.templateContent, e))
},

prepend() {
this.removeDuplicateTargetChildren()
this.targetElement?.prepend(this.templateContent)
this.targetElements.forEach(e => e.prepend(this.templateContent))
},

remove() {
this.targetElement?.remove()
this.targetElements.forEach(e => e.remove())
},

replace() {
this.targetElement?.replaceWith(this.templateContent)
this.targetElements.forEach(e => e.replaceWith(this.templateContent))
},

update() {
if (this.targetElement) {
this.targetElement.innerHTML = ""
this.targetElement.append(this.templateContent)
}
this.targetElements.forEach(e => {
e.innerHTML = ""
e.append(this.templateContent)
})
}
}
51 changes: 38 additions & 13 deletions src/elements/stream_element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,14 @@ export class StreamElement extends HTMLElement {
}

removeDuplicateTargetChildren() {
this.duplicateChildren.forEach(({targetChild}) => {
targetChild.remove();
})
this.duplicateChildren.forEach(c => c.remove())
}

get duplicateChildren() {
return [...this.templateContent?.children].filter(templateChild => !!templateChild.id).map(templateChild => {
let targetChild = [...this.targetElement!.children].filter(c => c.id === templateChild.id)[0]
return { targetChild , templateChild }
}).filter(({targetChild}) => targetChild);
const existingChildren = this.targetElements.flatMap(e => [...e.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))
}

get performAction() {
Expand All @@ -53,15 +51,18 @@ export class StreamElement extends HTMLElement {
this.raise("action attribute is missing")
}

get targetElement() {
if (this.target) {
return this.ownerDocument?.getElementById(this.target)
get targetElements() {
if (this.target) {
return this.targetElementsById
} else if (this.targets) {
return this.targetElementsByQuery
} else {
this.raise("target or targets attribute is missing")
}
this.raise("target attribute is missing")
}

get templateContent() {
return this.templateElement.content
return this.templateElement.content.cloneNode(true)
}

get templateElement() {
Expand All @@ -79,6 +80,10 @@ export class StreamElement extends HTMLElement {
return this.getAttribute("target")
}

get targets() {
return this.getAttribute("targets")
}

private raise(message: string): never {
throw new Error(`${this.description}: ${message}`)
}
Expand All @@ -90,4 +95,24 @@ export class StreamElement extends HTMLElement {
private get beforeRenderEvent() {
return new CustomEvent("turbo:before-stream-render", { bubbles: true, cancelable: true })
}

private get targetElementsById() {
const element = this.ownerDocument?.getElementById(this.target!)

if (element !== null) {
return [ element ]
} else {
return []
}
}

private get targetElementsByQuery() {
const elements = this.ownerDocument?.querySelectorAll(this.targets!)

if (elements.length !== 0) {
return Array.prototype.slice.call(elements)
} else {
return []
}
}
}
17 changes: 15 additions & 2 deletions src/tests/fixtures/stream.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,29 @@
<meta charset="utf-8">
<title>Turbo Streams</title>
<script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
<script>Turbo.connectStreamSource(new EventSource("/__turbo/messages"))</script>
</head>
<script>Turbo.connectStreamSource(new EventSource("/__turbo/messages"))</script></head>
<body>
<form id="create" method="post" action="/__turbo/messages">
<input type="hidden" name="content" value="Hello world!">
<input type="hidden" name="type" value="stream">
<button type="submit">Create</button>
</form>

<form id="replace" method="post" action="/__turbo/messages">
<input type="hidden" name="content" value="Hello CSS!">
<input type="hidden" name="targets" value=".messages">
<input type="hidden" name="type" value="stream">
<button type="submit">Replace</button>
</form>

<div id="messages">
<div class="message">First</div>
</div>
<div class="messages" id="message_2">
<div class="message">Second</div>
</div>
<div class="messages">
<div class="message">Third</div>
</div>
</body>
</html>
16 changes: 16 additions & 0 deletions src/tests/functional/stream_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@ export class StreamTests extends FunctionalTestCase {
element = await this.querySelector(selector)
this.assert.equal(await element.getVisibleText(), "Hello world!")
}

async "test receiving a stream message with css selector target"() {
let element
const selector = ".messages div.message:last-child"

element = await this.querySelectorAll(selector)
this.assert.equal(await element[0].getVisibleText(), "Second")
this.assert.equal(await element[1].getVisibleText(), "Third")

await this.clickSelector("#replace [type=submit]")
await this.nextBeat

element = await this.querySelectorAll(selector)
this.assert.equal(await element[0].getVisibleText(), "Hello CSS!")
this.assert.equal(await element[1].getVisibleText(), "Hello CSS!")
}
}

StreamTests.registerSuite()
4 changes: 4 additions & 0 deletions src/tests/helpers/functional_test_case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export class FunctionalTestCase extends InternTestCase {
return this.remote.findByCssSelector(selector)
}

async querySelectorAll(selector: string) {
return this.remote.findAllByCssSelector(selector)
}

async clickSelector(selector: string): Promise<void> {
return this.remote.findByCssSelector(selector).click()
}
Expand Down
22 changes: 15 additions & 7 deletions src/tests/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ router.post("/reject", (request, response) => {
})

router.post("/messages", (request, response) => {
const { content, status, type } = request.body
const { content, status, type, target, targets } = request.body
if (typeof content == "string") {
receiveMessage(content)
receiveMessage(content, target)
if (type == "stream" && acceptsStreams(request)) {
response.type("text/vnd.turbo-stream.html; charset=utf-8")
response.send(renderMessage(content))
response.send(targets ? renderMessageForTargets(content, targets) : renderMessage(content, target))
} else {
response.sendStatus(parseInt(status || "201", 10))
}
Expand Down Expand Up @@ -91,17 +91,25 @@ router.get("/messages", (request, response) => {
streamResponses.add(response)
})

function receiveMessage(content: string) {
const data = renderSSEData(renderMessage(content))
function receiveMessage(content: string, target?: string) {
const data = renderSSEData(renderMessage(content, target))
for (const response of streamResponses) {
intern.log("delivering message to stream", response.socket?.remotePort)
response.write(data)
}
}

function renderMessage(content: string) {
function renderMessage(content: string, target = "messages") {
return `
<turbo-stream action="append" target="${target}"><template>
<div class="message">${escapeHTML(content)}</div>
</template></turbo-stream>
`
}

function renderMessageForTargets(content: string, targets: string) {
return `
<turbo-stream action="append" target="messages"><template>
<turbo-stream action="append" targets="${targets}"><template>
<div class="message">${escapeHTML(content)}</div>
</template></turbo-stream>
`
Expand Down

0 comments on commit 045833b

Please sign in to comment.