From b70b0c8d87983b0a7839e03894178d44b1d36d19 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Sun, 31 Oct 2021 13:39:48 -0400 Subject: [PATCH] Introduce `turbo:before-frame-render` event The problem --- Similar to `turbo:before-render` events and the `` element, rendering `` 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. --- src/core/frames/frame_controller.ts | 21 ++++++++++++++++++--- src/core/frames/frame_view.ts | 4 +++- src/core/session.ts | 2 ++ src/tests/functional/rendering_tests.ts | 20 ++++++++++++++++++++ 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/core/frames/frame_controller.ts b/src/core/frames/frame_controller.ts index bb44d47a4..62d30a482 100644 --- a/src/core/frames/frame_controller.ts +++ b/src/core/frames/frame_controller.ts @@ -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" @@ -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 @@ -251,8 +252,22 @@ export class FrameController // View delegate - allowsImmediateRender(_snapshot: Snapshot, _options: ViewRenderOptions) { - return true + allowsImmediateRender({ element: newFrame }: Snapshot, options: ViewRenderOptions) { + const event = dispatch("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) {} diff --git a/src/core/frames/frame_view.ts b/src/core/frames/frame_view.ts index 7dc29dbef..c54dc454f 100644 --- a/src/core/frames/frame_view.ts +++ b/src/core/frames/frame_view.ts @@ -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 export class FrameView extends View { invalidate() { diff --git a/src/core/session.ts b/src/core/session.ts index b4af6992f..7b2bf24e7 100644 --- a/src/core/session.ts +++ b/src/core/session.ts @@ -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" @@ -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 diff --git a/src/tests/functional/rendering_tests.ts b/src/tests/functional/rendering_tests.ts index ce55d6d31..8302da35e 100644 --- a/src/tests/functional/rendering_tests.ts +++ b/src/tests/functional/rendering_tests.ts @@ -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", `Custom Rendered Frame`) + 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")