Skip to content

Commit

Permalink
Make Renderer instance available to View delegates
Browse files Browse the repository at this point in the history
The changes proposed in [hotwired#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.

[hotwired#1019]: hotwired#1019
  • Loading branch information
seanpdoyle committed Oct 9, 2023
1 parent c207f5b commit 8cc1cc5
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 26 deletions.
31 changes: 27 additions & 4 deletions src/core/frames/frame_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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)
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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)
}
}
29 changes: 11 additions & 18 deletions src/core/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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) {
Expand All @@ -282,10 +282,6 @@ export class Session {
this.notifyApplicationAfterFrameLoad(frame)
}

frameRendered(fetchResponse, frame) {
this.notifyApplicationAfterFrameRender(fetchResponse, frame)
}

// Application events

applicationAllowsFollowingLinkToLocation(link, location, ev) {
Expand Down Expand Up @@ -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 } })
}

Expand All @@ -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) {
Expand Down
7 changes: 3 additions & 4 deletions src/core/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,20 +56,19 @@ 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
await this.prepareToRenderSnapshot(renderer)

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 {
Expand Down

0 comments on commit 8cc1cc5

Please sign in to comment.