Skip to content

Commit

Permalink
Add render to turbo:before-stream-render event
Browse files Browse the repository at this point in the history
Follow-up to hotwired#479
Related to hotwired/turbo-site#107

As an alternative to exposing an otherwise private and internal
`StreamActions` "class", support custom `<turbo-stream action="...">`
values the same way as custom `<turbo-frame>` rendering or `<body>`
rendering: as part of its `turbo:before-render-stream` event.

To change how Turbo renders the document during page rendering, client
applications declare a `turbo:before-render` event listener that
overrides its `CustomEvent.detail.render` function from its
[default][PageRenderer.renderElement].

Similarly, to change how Turbo renders a frame, client applications
declare a `turbo:before-frame-render` and override its
`CustomEvent.detail.render` function from its [default][].

This commit introduces the `StreamElement.renderElement` function, and
extends the existing `turbo:before-stream-render` event to support the
same pattern with `StreamElement.renderElement` server as the default
`CustomEvent.detail.render` value.

With those changes in place, callers can declare a document-wide event
listener and override based on the value of `StreamElement.action`:

```javascript
const CustomActions = {
  customUpdate: (stream) => { /* ... */ }
  customReplace: (stream) => { /* ... */ }
  // ...
}

document.addEventListener("turbo:before-stream-render", (({ target, detail }) => {
  const defaultRender = detail.render

  detail.render = CustomActions[target.action] || defaultRender
}))
```

[PageRenderer.renderElement]: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/drive/page_renderer.ts#L7-L13
[FrameRenderer.renderElement]: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/frames/frame_renderer.ts#L13-L24
  • Loading branch information
seanpdoyle committed Aug 13, 2022
1 parent e574cea commit 52b4d17
Show file tree
Hide file tree
Showing 4 changed files with 32 additions and 16 deletions.
2 changes: 0 additions & 2 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ export { TurboFrameMissingEvent } from "./frames/frame_controller"
export { TurboBeforeFetchRequestEvent, TurboBeforeFetchResponseEvent } from "../http/fetch_request"
export { TurboBeforeStreamRenderEvent } from "../elements/stream_element"

export { StreamActions } from "./streams/stream_actions"

/**
* Starts the main session.
* This initialises any necessary observers such as those to monitor
Expand Down
16 changes: 12 additions & 4 deletions src/elements/stream_element.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { StreamActions } from "../core/streams/stream_actions"
import { nextAnimationFrame } from "../util"

export type TurboBeforeStreamRenderEvent = CustomEvent<{ newStream: StreamElement }>
type Render = (currentElement: StreamElement) => Promise<void>

export type TurboBeforeStreamRenderEvent = CustomEvent<{ newStream: StreamElement; render: Render }>

// <turbo-stream action=replace target=id><template>...

Expand All @@ -26,6 +28,10 @@ export type TurboBeforeStreamRenderEvent = CustomEvent<{ newStream: StreamElemen
* </turbo-stream>
*/
export class StreamElement extends HTMLElement {
static async renderElement(newElement: StreamElement): Promise<void> {
await newElement.performAction()
}

async connectedCallback() {
try {
await this.render()
Expand All @@ -40,9 +46,11 @@ export class StreamElement extends HTMLElement {

async render() {
return (this.renderPromise ??= (async () => {
if (this.dispatchEvent(this.beforeRenderEvent)) {
const event = this.beforeRenderEvent

if (this.dispatchEvent(event)) {
await nextAnimationFrame()
this.performAction()
await event.detail.render(this)
}
})())
}
Expand Down Expand Up @@ -153,7 +161,7 @@ export class StreamElement extends HTMLElement {
return new CustomEvent("turbo:before-stream-render", {
bubbles: true,
cancelable: true,
detail: { newStream: this },
detail: { newStream: this, render: StreamElement.renderElement },
})
}

Expand Down
1 change: 1 addition & 0 deletions src/tests/fixtures/stream.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<form id="append-target" method="post" action="/__turbo/messages">
<input type="hidden" name="content" value="Hello world!">
<input type="hidden" name="type" value="stream">
<input type="hidden" name="id" value="a-turbo-stream">
<button>Create</button>
</form>

Expand Down
29 changes: 19 additions & 10 deletions src/tests/functional/stream_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ test("test receiving a stream message", async ({ page }) => {

test("test dispatches a turbo:before-stream-render event", async ({ page }) => {
await page.click("#append-target button")
const { newStream } = await nextEventNamed(page, "turbo:before-stream-render")
await nextEventNamed(page, "turbo:submit-end")
const [[type, detail, target]] = await readEventLogs(page, 1)

assert.ok(newStream.includes(`action="append"`))
assert.ok(newStream.includes(`target="messages"`))
assert.equal(type, "turbo:before-stream-render")
assert.equal(target, "a-turbo-stream")
assert.ok(detail.newStream.includes(`action="append"`))
assert.ok(detail.newStream.includes(`target="messages"`))
})

test("test receiving a stream message with css selector target", async ({ page }) => {
Expand Down Expand Up @@ -73,14 +76,20 @@ test("test overriding with custom StreamActions", async ({ page }) => {
const html = "Rendered with Custom Action"

await page.evaluate((html) => {
window.Turbo.StreamActions.customUpdate = function () {
for (const target of this.targetElements) target.innerHTML = html
const CustomActions: Record<string, any> = {
customUpdate(newStream: { targetElements: HTMLElement[] }) {
for (const target of newStream.targetElements) target.innerHTML = html
},
}
window.Turbo.renderStreamMessage(`
<turbo-stream action="customUpdate" target="messages">
<template></template>
</turbo-stream>
`)

addEventListener("turbo:before-stream-render", (({ target, detail }: CustomEvent) => {
const stream = target as unknown as { action: string }

const defaultRender = detail.render
detail.render = CustomActions[stream.action] || defaultRender
}) as EventListener)

window.Turbo.renderStreamMessage(`<turbo-stream action="customUpdate" target="messages"></turbo-stream>`)
}, html)

assert.equal(await page.textContent("#messages"), html, "evaluates custom StreamAction")
Expand Down

0 comments on commit 52b4d17

Please sign in to comment.