Skip to content

Commit

Permalink
Introduce turbo:before-frame-render event
Browse files Browse the repository at this point in the history
The problem
---

Similar to `turbo:before-render` events and the `<body>` element,
rendering `<turbo-frame>` events is opaque and isn't extensible.

The solution
---

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

This way, consumer applications can override rendering in the same style
as `turbo:before-render` events.
  • Loading branch information
seanpdoyle committed Jul 17, 2022
1 parent 0a3c8ae commit b70b0c8
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 4 deletions.
21 changes: 18 additions & 3 deletions src/core/frames/frame_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
import { FetchMethod, FetchRequest, FetchRequestDelegate, FetchRequestHeaders } from "../../http/fetch_request"
import { FetchResponse } from "../../http/fetch_response"
import { AppearanceObserver, AppearanceObserverDelegate } from "../../observers/appearance_observer"
import { clearBusyState, getAttribute, parseHTMLDocument, markAsBusy } from "../../util"
import { clearBusyState, dispatch, getAttribute, parseHTMLDocument, markAsBusy } from "../../util"
import { FormSubmission, FormSubmissionDelegate } from "../drive/form_submission"
import { Snapshot } from "../snapshot"
import { ViewDelegate, ViewRenderOptions } from "../view"
Expand All @@ -19,6 +19,7 @@ import { FormLinkInterceptor, FormLinkInterceptorDelegate } from "../../observer
import { FrameRenderer } from "./frame_renderer"
import { session } from "../index"
import { isAction } from "../types"
import { TurboBeforeFrameRenderEvent } from "../session"

export class FrameController
implements
Expand Down Expand Up @@ -251,8 +252,22 @@ export class FrameController

// View delegate

allowsImmediateRender(_snapshot: Snapshot, _options: ViewRenderOptions<FrameElement>) {
return true
allowsImmediateRender({ element: newFrame }: Snapshot<FrameElement>, options: ViewRenderOptions<FrameElement>) {
const event = dispatch<TurboBeforeFrameRenderEvent>("turbo:before-frame-render", {
target: this.element,
detail: { newFrame, ...options },
cancelable: true,
})
const {
defaultPrevented,
detail: { render },
} = event

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

return !defaultPrevented
}

viewRenderedSnapshot(_snapshot: Snapshot, _isPreview: boolean) {}
Expand Down
4 changes: 3 additions & 1 deletion src/core/frames/frame_view.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { FrameElement } from "../../elements"
import { Snapshot } from "../snapshot"
import { View } from "../view"
import { View, ViewRenderOptions } from "../view"

export type FrameViewRenderOptions = ViewRenderOptions<FrameElement>

export class FrameView extends View<FrameElement> {
invalidate() {
Expand Down
2 changes: 2 additions & 0 deletions src/core/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { PageView, PageViewDelegate, PageViewRenderOptions } from "./drive/page_
import { Visit, VisitOptions } from "./drive/visit"
import { PageSnapshot } from "./drive/page_snapshot"
import { FrameElement } from "../elements/frame_element"
import { FrameViewRenderOptions } from "./frames/frame_view"
import { FetchResponse } from "../http/fetch_response"
import { Preloader, PreloaderDelegate } from "./drive/preloader"

Expand All @@ -27,6 +28,7 @@ export type TurboBeforeRenderEvent = CustomEvent<{ newBody: HTMLBodyElement } &
export type TurboBeforeVisitEvent = CustomEvent<{ url: string }>
export type TurboClickEvent = CustomEvent<{ url: string; originalEvent: MouseEvent }>
export type TurboFrameLoadEvent = CustomEvent
export type TurboBeforeFrameRenderEvent = CustomEvent<{ newFrame: FrameElement } & FrameViewRenderOptions>
export type TurboFrameRenderEvent = CustomEvent<{ fetchResponse: FetchResponse }>
export type TurboLoadEvent = CustomEvent<{ url: string; timing: TimingData }>
export type TurboRenderEvent = CustomEvent
Expand Down
20 changes: 20 additions & 0 deletions src/tests/functional/rendering_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,26 @@ test("test restores focus during page rendering when transposing an ancestor of
assert.ok(await selectorHasFocus(page, "#permanent-descendant-input"), "restores focus after page loads")
})

test("test before-frame-render event supports custom render function within turbo-frames", async ({ page }) => {
const frame = await page.locator("#frame")
await frame.evaluate((frame) =>
frame.addEventListener("turbo:before-frame-render", (event) => {
const { detail } = event as CustomEvent
const { render } = detail
detail.render = (currentElement: Element, newElement: Element) => {
newElement.insertAdjacentHTML("beforeend", `<span id="custom-rendered">Custom Rendered Frame</span>`)
render(currentElement, newElement)
}
})
)

await page.click("#permanent-in-frame-element-link")
await nextBeat()

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

test("test preserves permanent elements within turbo-frames", async ({ page }) => {
assert.equal(await page.textContent("#permanent-in-frame"), "Rendering")

Expand Down

0 comments on commit b70b0c8

Please sign in to comment.