Skip to content

Commit

Permalink
Support custom rendering from turbo:before-render
Browse files Browse the repository at this point in the history
The problem
---

The rendering process during a page-wide navigation is opaque, and
cannot be extended. Proposals have been made to use third-party
rendering tools like [morphdom][], or other animation libraries.

Outside of the HTML manipulation, Turbo is also responsible for loading
script, transposing permanent elements, etc.

How might these tools integrate with Turbo in a way that's compliant
with permanent elements.

The solution
---

When publishing a `turbo:before-render` event, dispatch it with a
`render()` function property in addition to the `resume()`.

This way, consumer applications can override rendering:

```html
import morphdom from "morphdom"

addEventListener("turbo:before-render", ({ detail }) => {
  detail.render = (currentElement, newElement) => {
    morphdom(currentElement, newElement)
  }

  // or, more tersely
  detail.render = morphdom
})
```

Potentially Closes [hotwired#197][]
Potentially Closes [hotwired#378][]
Potentially Closes [hotwired#218][]

[morphdom]: https://github.com/patrick-steele-idem/morphdom
[hotwired#218]: hotwired#218
[hotwired#378]: hotwired#378
[hotwired#197]: hotwired#197
  • Loading branch information
seanpdoyle committed Jul 17, 2022
1 parent 8566c16 commit 0a3c8ae
Show file tree
Hide file tree
Showing 5 changed files with 36 additions and 8 deletions.
2 changes: 1 addition & 1 deletion src/core/drive/page_view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { PageSnapshot } from "./page_snapshot"
import { SnapshotCache } from "./snapshot_cache"
import { Visit } from "./visit"

export type PageViewRenderOptions = ViewRenderOptions
export type PageViewRenderOptions = ViewRenderOptions<HTMLBodyElement>

export interface PageViewDelegate extends ViewDelegate<HTMLBodyElement, PageSnapshot> {
viewWillCacheSnapshot(): void
Expand Down
2 changes: 1 addition & 1 deletion src/core/frames/frame_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ export class FrameController

// View delegate

allowsImmediateRender(_snapshot: Snapshot, _options: ViewRenderOptions) {
allowsImmediateRender(_snapshot: Snapshot, _options: ViewRenderOptions<FrameElement>) {
return true
}

Expand Down
13 changes: 11 additions & 2 deletions src/core/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { Preloader, PreloaderDelegate } from "./drive/preloader"

export type TimingData = unknown
export type TurboBeforeCacheEvent = CustomEvent
export type TurboBeforeRenderEvent = CustomEvent<{ newBody: HTMLBodyElement; resume: (value: any) => void }>
export type TurboBeforeRenderEvent = CustomEvent<{ newBody: HTMLBodyElement } & PageViewRenderOptions>
export type TurboBeforeVisitEvent = CustomEvent<{ url: string }>
export type TurboClickEvent = CustomEvent<{ url: string; originalEvent: MouseEvent }>
export type TurboFrameLoadEvent = CustomEvent
Expand Down Expand Up @@ -261,7 +261,16 @@ export class Session

allowsImmediateRender({ element }: PageSnapshot, options: PageViewRenderOptions) {
const event = this.notifyApplicationBeforeRender(element, options)
return !event.defaultPrevented
const {
defaultPrevented,
detail: { render },
} = event

if (this.view.renderer && render) {
this.view.renderer.renderElement = render
}

return !defaultPrevented
}

viewRenderedSnapshot(_snapshot: PageSnapshot, _isPreview: boolean) {
Expand Down
9 changes: 5 additions & 4 deletions src/core/view.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { ReloadReason } from "./native/browser_adapter"
import { Renderer } from "./renderer"
import { Renderer, Render } from "./renderer"
import { Snapshot } from "./snapshot"
import { Position } from "./types"
import { getAnchor } from "./url"

export interface ViewRenderOptions {
export interface ViewRenderOptions<E> {
resume: (value: any) => void
render: Render<E>
}

export interface ViewDelegate<E extends Element, S extends Snapshot<E>> {
allowsImmediateRender(snapshot: S, options: ViewRenderOptions): boolean
allowsImmediateRender(snapshot: S, options: ViewRenderOptions<E>): boolean
preloadOnLoadLinksForView(element: Element): void
viewRenderedSnapshot(snapshot: S, isPreview: boolean): void
viewInvalidated(reason: ReloadReason): void
Expand Down Expand Up @@ -89,7 +90,7 @@ export abstract class View<
this.prepareToRenderSnapshot(renderer)

const renderInterception = new Promise((resolve) => (this.resolveInterceptionPromise = resolve))
const options = { resume: this.resolveInterceptionPromise }
const options = { resume: this.resolveInterceptionPromise, render: this.renderer.renderElement }
const immediateRender = this.delegate.allowsImmediateRender(snapshot, options)
if (!immediateRender) await renderInterception

Expand Down
18 changes: 18 additions & 0 deletions src/tests/functional/rendering_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,24 @@ test("test reloads when tracked elements change", async ({ page }) => {
assert.equal(reason, "tracked_element_mismatch")
})

test("test before-render event supports custom render function", async ({ page }) => {
await page.evaluate(() =>
addEventListener("turbo:before-render", (event) => {
const { detail } = event as CustomEvent
const { render } = detail
detail.render = (currentElement: HTMLBodyElement, newElement: HTMLBodyElement) => {
newElement.insertAdjacentHTML("beforeend", `<span id="custom-rendered">Custom Rendered</span>`)
render(currentElement, newElement)
}
})
)
await page.click("#same-origin-link")
await nextBody(page)

const customRendered = await page.locator("#custom-rendered")
assert.equal(await customRendered.textContent(), "Custom Rendered", "renders with custom function")
})

test("test wont reload when tracked elements has a nonce", async ({ page }) => {
await page.click("#tracked-nonce-tag-link")
await nextBody(page)
Expand Down

0 comments on commit 0a3c8ae

Please sign in to comment.