From 8cc1cc525282c96895541973d5ea9e6ea2fd7d55 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Mon, 9 Oct 2023 00:09:01 -0400 Subject: [PATCH] Make `Renderer` instance available to `View` delegates The changes proposed in [#1019][] require dispatching the new `Renderer.renderMethod` property as part of the `turbo:render` event. Similarly, there's an opportunity to include that information in the `turbo:before-render`, `turbo:before-frame-render`, and `turbo:frame-render` events. To simplify those chains of object access, this commit changes the `View` class delegate's `allowsImmediateRender` and `viewRenderedSnapshot` methods to accept the `Renderer` instance, instead of individual properties. With access to the instance, the delegate's can read properties like `isPreview` along with the `element` (transitively through the `newSnapshot` property). In order to dispatch the `turbo:frame-render` event closer to the moment in time that the view renders, this commit removes the `Session.frameRendered` callback, and replaces it with a `dispatch("turbo:frame-render")` call inside the `FrameController.viewRenderedSnapshot(renderer)` delegate method. In order to do so, this commit must work around the fact that `turbo:frame-render` events include the `FetchResponse` instance involved in the render. Since that instance isn't available at render time, this commit wraps the `await this.view.render(renderer)` in a utility function that injects the `FetchResponse` instance into the Custom event's `event.detail` object immediately after it's initially dispatched. Ideally, this work around will only be temporary, since the `turbo:frame-load` event also includes `event.detail.fetchResponse`. There's an opportunity to deprecate that property in `turbo:frame-render` events in the future. [#1019]: https://github.com/hotwired/turbo/pull/1019 --- src/core/frames/frame_controller.js | 31 +++++++++++++++++++++++++---- src/core/session.js | 29 ++++++++++----------------- src/core/view.js | 7 +++---- 3 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/core/frames/frame_controller.js b/src/core/frames/frame_controller.js index 1cab2902e..4f556fec4 100644 --- a/src/core/frames/frame_controller.js +++ b/src/core/frames/frame_controller.js @@ -258,7 +258,8 @@ export class FrameController { // View delegate - allowsImmediateRender({ element: newFrame }, _isPreview, options) { + allowsImmediateRender(renderer, options) { + const newFrame = renderer.newSnapshot.element const event = dispatch("turbo:before-frame-render", { target: this.element, detail: { newFrame, ...options }, @@ -276,7 +277,15 @@ export class FrameController { return !defaultPrevented } - viewRenderedSnapshot(_snapshot, _isPreview) {} + viewRenderedSnapshot(renderer) { + const frame = renderer.currentSnapshot.element + + return dispatch("turbo:frame-render", { + detail: {}, + target: frame, + cancelable: true + }) + } preloadOnLoadLinksForView(element) { session.preloadOnLoadLinksForView(element) @@ -311,9 +320,11 @@ export class FrameController { if (this.view.renderPromise) await this.view.renderPromise this.changeHistory() - await this.view.render(renderer) + await mergeIntoNext("turbo:frame-render", { on: this.element, detail: { fetchResponse } }, async () => { + await this.view.render(renderer) + }) + this.complete = true - session.frameRendered(fetchResponse, this.element) session.frameLoaded(this.element) await this.fetchResponseLoaded(fetchResponse) } else if (this.#willHandleFrameMissingFromResponse(fetchResponse)) { @@ -590,3 +601,15 @@ function activateElement(element, currentURL) { } } } + +async function mergeIntoNext(eventName, { on: target, detail }, callback) { + const listener = (event) => Object.assign(event.detail, detail) + const listenerOptions = { once: true, capture: true } + target.addEventListener(eventName, listener, listenerOptions) + + try { + await callback() + } finally { + target.removeEventListener(eventName, listener, listenerOptions) + } +} diff --git a/src/core/session.js b/src/core/session.js index 44edc7856..428ed09cc 100644 --- a/src/core/session.js +++ b/src/core/session.js @@ -249,8 +249,8 @@ export class Session { } } - allowsImmediateRender({ element }, isPreview, options) { - const event = this.notifyApplicationBeforeRender(element, isPreview, options) + allowsImmediateRender(renderer, options) { + const event = this.notifyApplicationBeforeRender(renderer, options) const { defaultPrevented, detail: { render } @@ -263,9 +263,9 @@ export class Session { return !defaultPrevented } - viewRenderedSnapshot(_snapshot, isPreview) { + viewRenderedSnapshot(renderer) { this.view.lastRenderedLocation = this.history.location - this.notifyApplicationAfterRender(isPreview) + this.notifyApplicationAfterRender(renderer) } preloadOnLoadLinksForView(element) { @@ -282,10 +282,6 @@ export class Session { this.notifyApplicationAfterFrameLoad(frame) } - frameRendered(fetchResponse, frame) { - this.notifyApplicationAfterFrameRender(fetchResponse, frame) - } - // Application events applicationAllowsFollowingLinkToLocation(link, location, ev) { @@ -321,14 +317,19 @@ export class Session { return dispatch("turbo:before-cache") } - notifyApplicationBeforeRender(newBody, isPreview, options) { + notifyApplicationBeforeRender(renderer, options) { + const isPreview = renderer.isPreview + const newBody = renderer.newSnapshot.element + return dispatch("turbo:before-render", { detail: { newBody, isPreview, ...options }, cancelable: true }) } - notifyApplicationAfterRender(isPreview) { + notifyApplicationAfterRender(renderer) { + const isPreview = renderer.isPreview + return dispatch("turbo:render", { detail: { isPreview } }) } @@ -351,14 +352,6 @@ export class Session { return dispatch("turbo:frame-load", { target: frame }) } - notifyApplicationAfterFrameRender(fetchResponse, frame) { - return dispatch("turbo:frame-render", { - detail: { fetchResponse }, - target: frame, - cancelable: true - }) - } - // Helpers submissionIsNavigatable(form, submitter) { diff --git a/src/core/view.js b/src/core/view.js index ca81e8bdb..640ca74e8 100644 --- a/src/core/view.js +++ b/src/core/view.js @@ -56,8 +56,7 @@ export class View { // Rendering async render(renderer) { - const { isPreview, shouldRender, newSnapshot: snapshot } = renderer - if (shouldRender) { + if (renderer.shouldRender) { try { this.renderPromise = new Promise((resolve) => (this.#resolveRenderPromise = resolve)) this.renderer = renderer @@ -65,11 +64,11 @@ export class View { const renderInterception = new Promise((resolve) => (this.#resolveInterceptionPromise = resolve)) const options = { resume: this.#resolveInterceptionPromise, render: this.renderer.renderElement } - const immediateRender = this.delegate.allowsImmediateRender(snapshot, isPreview, options) + const immediateRender = this.delegate.allowsImmediateRender(renderer, options) if (!immediateRender) await renderInterception await this.renderSnapshot(renderer) - this.delegate.viewRenderedSnapshot(snapshot, isPreview) + this.delegate.viewRenderedSnapshot(renderer) this.delegate.preloadOnLoadLinksForView(this.element) this.finishRenderingSnapshot(renderer) } finally {