Skip to content

Commit 7403004

Browse files
committed
Frame Visits: Cache Snapshot later in process
Follow-up to [hotwired#441][] Depends on [hotwired#487][] Closes hotwired#472 --- When caching Snapshots during a `Visit`, elements are not cached until after the `turbo:before-cache` event fires. This affords client applications with an opportunity to disconnect and deconstruct and JavaScript state, and provides an opportunity to encode that state into the HTML so that it can survive the caching process. The timing of the construction of the `SnapshotSubstitution` instance occurs too early in the frame rendering process: the `<turbo-frame>` descendants have not been disconnected. The handling of the `<turbo-frame>` caching is already an exception from the norm. Unfortunately, the current implementation is caching _too early_ in the process. If the Snapshot were cached too late in the process with the rest of the page (as described in [hotwired#441][]), the `[src]` attribute and descendant content would have already changed, so any previous state would be lost. This commit strikes a balance between the two extremes by introducing the `FrameRendererDelegate` interface and the `frameContentsExtracted()` hook. During `<turbo-frame>` rendering, the `FrameRenderer` instance selects a [Range][] of nodes and removes them by calling [Range.deleteContents][]. The `deleteContents()` method removes the Nodes and discards them. This commit replaces the `deleteContents()` call with one to [Range.extractContents][], so that the Nodes are retained as a [DocumentFragment][] instance. While handling the callback, the `FrameController` retains that instance by setting an internal `previousContents` property. Later on in the Frame rendering-to-Visit-promotion process, the `FrameController` implements the `visitCachedSnapshot()` hook to read from the `previousContents` property and substitute the frame's contents with the `previousContents`, replacing the need for the `SnapshotSubstitution` class. [hotwired#441]: hotwired#441 [hotwired#487]: hotwired#487 [Range]: https://developer.mozilla.org/en-US/docs/Web/API/Range [Range.deleteContents]: https://developer.mozilla.org/en-US/docs/Web/API/range/deleteContents [Range.extractContents]: https://developer.mozilla.org/en-US/docs/Web/API/Range/extractContents [DocumentFragment]: https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment
1 parent 9c26441 commit 7403004

File tree

5 files changed

+38
-21
lines changed

5 files changed

+38
-21
lines changed

src/core/frames/frame_controller.ts

+20-18
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export class FrameController implements AppearanceObserverDelegate, FetchRequest
2929
private connected = false
3030
private hasBeenLoaded = false
3131
private ignoredAttributes: Set<ObservedAttribute> = new Set
32+
private previousContents?: DocumentFragment
3233

3334
constructor(element: FrameElement) {
3435
this.element = element
@@ -112,7 +113,7 @@ export class FrameController implements AppearanceObserverDelegate, FetchRequest
112113
if (html) {
113114
const { body } = parseHTMLDocument(html)
114115
const snapshot = new Snapshot(await this.extractForeignFrameElement(body))
115-
const renderer = new FrameRenderer(this.view.snapshot, snapshot, false, false)
116+
const renderer = new FrameRenderer(this, this.view.snapshot, snapshot, false, false)
116117
if (this.view.renderPromise) await this.view.renderPromise
117118
await this.view.render(renderer)
118119
this.loaded = true
@@ -236,6 +237,22 @@ export class FrameController implements AppearanceObserverDelegate, FetchRequest
236237
viewInvalidated() {
237238
}
238239

240+
// Frame renderer delegate
241+
frameContentsExtracted(fragment: DocumentFragment) {
242+
this.previousContents = fragment
243+
}
244+
245+
visitCachedSnapshot = ({ element }: Snapshot) => {
246+
const frame = element.querySelector("#" + this.element.id)
247+
248+
if (frame && this.previousContents) {
249+
frame.innerHTML = ""
250+
frame.append(this.previousContents)
251+
}
252+
253+
delete this.previousContents
254+
}
255+
239256
// Private
240257

241258
private async visit(url: URL) {
@@ -266,7 +283,8 @@ export class FrameController implements AppearanceObserverDelegate, FetchRequest
266283
const action = getAttribute("data-turbo-action", submitter, element, frame)
267284

268285
if (isAction(action)) {
269-
const { visitCachedSnapshot } = new SnapshotSubstitution(frame)
286+
const { visitCachedSnapshot } = frame.delegate
287+
270288
frame.delegate.fetchResponseLoaded = (fetchResponse: FetchResponse) => {
271289
if (frame.src) {
272290
const { statusCode, redirected } = fetchResponse
@@ -406,22 +424,6 @@ export class FrameController implements AppearanceObserverDelegate, FetchRequest
406424
}
407425
}
408426

409-
class SnapshotSubstitution {
410-
private readonly clone: Node
411-
private readonly id: string
412-
413-
constructor(element: FrameElement) {
414-
this.clone = element.cloneNode(true)
415-
this.id = element.id
416-
}
417-
418-
visitCachedSnapshot = ({ element }: Snapshot) => {
419-
const { id, clone } = this
420-
421-
element.querySelector("#" + id)?.replaceWith(clone)
422-
}
423-
}
424-
425427
function getFrameElementById(id: string | null) {
426428
if (id != null) {
427429
const element = document.getElementById(id)

src/core/frames/frame_renderer.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
import { FrameElement } from "../../elements/frame_element"
22
import { nextAnimationFrame } from "../../util"
33
import { Renderer } from "../renderer"
4+
import { Snapshot } from "../snapshot"
5+
6+
export interface FrameRendererDelegate {
7+
frameContentsExtracted(fragment: DocumentFragment): void
8+
}
49

510
export class FrameRenderer extends Renderer<FrameElement> {
11+
private readonly delegate: FrameRendererDelegate
12+
13+
constructor(delegate: FrameRendererDelegate, currentSnapshot: Snapshot<FrameElement>, newSnapshot: Snapshot<FrameElement>, isPreview: boolean, willRender = true) {
14+
super(currentSnapshot, newSnapshot, isPreview, willRender)
15+
this.delegate = delegate
16+
}
17+
618
get shouldRender() {
719
return true
820
}
@@ -22,7 +34,7 @@ export class FrameRenderer extends Renderer<FrameElement> {
2234
loadFrameElement() {
2335
const destinationRange = document.createRange()
2436
destinationRange.selectNodeContents(this.currentElement)
25-
destinationRange.deleteContents()
37+
this.delegate.frameContentsExtracted(destinationRange.extractContents())
2638

2739
const frameElement = this.newElement
2840
const sourceRange = frameElement.ownerDocument?.createRange()

src/elements/frame_element.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { FetchResponse } from "../http/fetch_response"
2+
import { Snapshot } from "../core/snapshot"
23

34
export enum FrameLoadingStyle { eager = "eager", lazy = "lazy" }
45

@@ -15,6 +16,7 @@ export interface FrameElementDelegate {
1516
linkClickIntercepted(element: Element, url: string): void
1617
loadResponse(response: FetchResponse): void
1718
fetchResponseLoaded: (fetchResponse: FetchResponse) => void
19+
visitCachedSnapshot: (snapshot: Snapshot) => void
1820
isLoading: boolean
1921
}
2022

src/tests/functional/frame_tests.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -521,11 +521,12 @@ export class FrameTests extends TurboDriveTestCase {
521521

522522
const title = await this.querySelector("h1")
523523
const frameTitle = await this.querySelector("#frame h2")
524+
const src = new URL(await this.attributeForSelector("#frame", "src") || "")
524525

525526
this.assert.equal(await title.getVisibleText(), "Frames")
526527
this.assert.equal(await frameTitle.getVisibleText(), "Frames: #frame")
527528
this.assert.equal(await this.pathname, "/src/tests/fixtures/frames.html")
528-
this.assert.equal(await this.propertyForSelector("#frame", "src"), null)
529+
this.assert.equal(src.pathname, "/src/tests/fixtures/frames/frame.html")
529530
}
530531

531532
async "test navigating back then forward after pushing URL state from a turbo-frame[data-turbo-action=advance] restores the frames next contents"() {

src/tests/functional/loading_tests.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ export class LoadingTests extends TurboDriveTestCase {
133133
await this.clickSelector("#one")
134134
await this.nextEventNamed("turbo:load")
135135
await this.goBack()
136-
await this.nextBody
136+
await this.nextEventNamed("turbo:load")
137137
await this.noNextEventNamed("turbo:frame-load")
138138

139139
let src = new URL(await this.attributeForSelector("#hello", "src") || "")

0 commit comments

Comments
 (0)