Skip to content

Commit 84b0a89

Browse files
kapantzakJohn KapantzakisDavid Heinemeier Hanssonseanpdoyle
authored
Fire 'turbo:frame-render' event after turbo frame renders the view (#327)
* Fire 'turbo:after-fetch-render' event after turbo frame renders the view * fixup! Fire 'turbo:after-fetch-render' event after turbo frame renders the view * Change 'turbo:after-fetch-render' event to 'turbo:frame-render' * Implement notifyApplicationAfterFrameRender in order to dispatch the 'turbo:frame-render' event * Dispatch `turbo:frame-load` on turbo-frame Closes #54 Closes hotwired/turbo-rails#56 Dispatch `turbo:frame-load` lifecycle event when `<turbo-frame>` element is navigated and finishes loading. The events bubble up, with the `<turbo-frame>` element as the target. Originally, this pull request involved numerous events, but in the spirit of experimentation, we'll start with the one and see if others are necessary. * fixup! Dispatch `turbo:frame-load` on turbo-frame Co-authored-by: John Kapantzakis <kapantzak@192.168.2.2> Co-authored-by: David Heinemeier Hansson <david@loudthinking.com> Co-authored-by: Sean Doyle <sean.p.doyle24@gmail.com>
1 parent e7a0b91 commit 84b0a89

10 files changed

Lines changed: 97 additions & 7 deletions

File tree

src/core/frames/frame_controller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export class FrameController implements AppearanceObserverDelegate, FetchRequest
8787
this.appearanceObserver.stop()
8888
await this.element.loaded
8989
this.hasBeenLoaded = true
90+
session.frameLoaded(this.element)
9091
} catch (error) {
9192
this.currentURL = previousURL
9293
throw error
@@ -108,6 +109,7 @@ export class FrameController implements AppearanceObserverDelegate, FetchRequest
108109
const renderer = new FrameRenderer(this.view.snapshot, snapshot, false)
109110
if (this.view.renderPromise) await this.view.renderPromise
110111
await this.view.render(renderer)
112+
session.frameRendered(fetchResponse, this.element);
111113
}
112114
} catch (error) {
113115
console.error(error)
@@ -223,11 +225,9 @@ export class FrameController implements AppearanceObserverDelegate, FetchRequest
223225
}
224226

225227
viewRenderedSnapshot(snapshot: Snapshot, isPreview: boolean) {
226-
227228
}
228229

229230
viewInvalidated() {
230-
231231
}
232232

233233
// Private

src/core/session.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { dispatch } from "../util"
1616
import { PageView, PageViewDelegate } from "./drive/page_view"
1717
import { Visit, VisitOptions } from "./drive/visit"
1818
import { PageSnapshot } from "./drive/page_snapshot"
19+
import { FrameElement } from "../elements/frame_element"
20+
import { FetchResponse } from "../http/fetch_response"
1921

2022
export type TimingData = {}
2123

@@ -235,6 +237,16 @@ export class Session implements FormSubmitObserverDelegate, HistoryDelegate, Lin
235237
this.adapter.pageInvalidated()
236238
}
237239

240+
// Frame element
241+
242+
frameLoaded(frame: FrameElement) {
243+
this.notifyApplicationAfterFrameLoad(frame)
244+
}
245+
246+
frameRendered(fetchResponse: FetchResponse, frame: FrameElement) {
247+
this.notifyApplicationAfterFrameRender(fetchResponse, frame);
248+
}
249+
238250
// Application events
239251

240252
applicationAllowsFollowingLinkToLocation(link: Element, location: URL) {
@@ -279,6 +291,14 @@ export class Session implements FormSubmitObserverDelegate, HistoryDelegate, Lin
279291
dispatchEvent(new HashChangeEvent("hashchange", { oldURL: oldURL.toString(), newURL: newURL.toString() }))
280292
}
281293

294+
notifyApplicationAfterFrameLoad(frame: FrameElement) {
295+
return dispatch("turbo:frame-load", { target: frame })
296+
}
297+
298+
notifyApplicationAfterFrameRender(fetchResponse: FetchResponse, frame: FrameElement) {
299+
return dispatch("turbo:frame-render", { detail: { fetchResponse }, target: frame, cancelable: true })
300+
}
301+
282302
// Helpers
283303

284304
elementDriveEnabled(element?: Element) {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Turbo</title>
6+
<script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
7+
<script src="/src/tests/fixtures/test.js"></script>
8+
</head>
9+
<body>
10+
<div id="container">
11+
<a id="outside" href="/src/tests/fixtures/frame_navigation.html" data-turbo-frame="frame">Outside Frame</a>
12+
13+
<turbo-frame id="frame">
14+
<h2>Frame Navigation</h2>
15+
16+
<a id="inside" href="/src/tests/fixtures/frame_navigation.html">Inside Frame</a>
17+
<a id="top" href="/src/tests/fixtures/frame_navigation.html" data-turbo-frame="_top">Top</a>
18+
</turbo-frame>
19+
</div>
20+
</body>
21+
</html>

src/tests/fixtures/frames.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,9 @@ <h2>Frames: #nested-child</h2>
5151

5252
<a id="navigate-form-redirect" href="/src/tests/fixtures/frames/form-redirect.html" data-turbo-frame="form-redirect">Visit form-redirect.html</a>
5353
<turbo-frame id="form-redirect"></turbo-frame>
54+
55+
<turbo-frame id="part">
56+
<a id="frame-part" href="/src/tests/fixtures/frames/part.html">Load #part</a>
57+
</turbo-frame>
5458
</body>
5559
</html>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<turbo-frame id="part">
2+
<h2>Frames: #frame-part</h2>
3+
</turbo-frame>

src/tests/fixtures/test.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
}
88

99
function eventListener(event) {
10-
eventLogs.push([event.type, event.detail])
10+
eventLogs.push([event.type, event.detail, event.target.id])
1111
}
1212
window.mutationLogs = []
1313

@@ -26,5 +26,7 @@
2626
"turbo:render",
2727
"turbo:before-fetch-request",
2828
"turbo:before-fetch-response",
29-
"turbo:visit"
29+
"turbo:visit",
30+
"turbo:frame-load",
31+
"turbo:frame-render",
3032
])
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { TurboDriveTestCase } from "../helpers/turbo_drive_test_case"
2+
3+
export class FrameNavigationTests extends TurboDriveTestCase {
4+
async setup() {
5+
await this.goToLocation("/src/tests/fixtures/frame_navigation.html")
6+
}
7+
8+
async "test frame navigation with descendant link"() {
9+
await this.clickSelector("#inside")
10+
11+
await this.nextEventOnTarget("frame", "turbo:frame-load")
12+
}
13+
14+
async "test frame navigation with exterior link"() {
15+
await this.clickSelector("#outside")
16+
17+
await this.nextEventOnTarget("frame", "turbo:frame-load")
18+
}
19+
}
20+
21+
FrameNavigationTests.registerSuite()

src/tests/functional/frame_tests.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export class FrameTests extends TurboDriveTestCase {
1919
async "test a frame whose src references itself does not infinitely loop"() {
2020
await this.clickSelector("#frame-self")
2121

22-
await this.nextEventNamed("turbo:before-fetch-response")
22+
await this.nextEventOnTarget("frame", "turbo:frame-load")
2323

2424
const otherEvents = await this.eventLogChannel.read()
2525
this.assert.equal(otherEvents.length, 0, "no more events")
@@ -152,7 +152,16 @@ export class FrameTests extends TurboDriveTestCase {
152152
this.assert.ok(await this.querySelector("#form-redirect-header"))
153153
}
154154

155-
async "test following a link reloads frame on every click"() {
155+
async "test 'turbo:frame-render' is triggered after frame has finished rendering"() {
156+
await this.clickSelector("#frame-part")
157+
158+
await this.nextEventNamed("turbo:frame-render") // recursive
159+
const { fetchResponse } = await this.nextEventNamed("turbo:frame-render")
160+
161+
this.assert.include(fetchResponse.response.url, "/src/tests/fixtures/frames/part.html")
162+
}
163+
164+
async "test following a link reloads frame on every click"() {
156165
await this.clickSelector("#hello a")
157166
await this.nextEventNamed("turbo:before-fetch-request")
158167

src/tests/functional/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from "./drive_tests"
66
export * from "./form_submission_tests"
77
export * from "./frame_tests"
88
export * from "./import_tests"
9+
export * from "./frame_navigation_tests"
910
export * from "./loading_tests"
1011
export * from "./navigation_tests"
1112
export * from "./pausable_rendering_tests"

src/tests/helpers/turbo_drive_test_case.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { FunctionalTestCase } from "./functional_test_case"
22
import { RemoteChannel } from "./remote_channel"
33
import { Element } from "@theintern/leadfoot"
44

5-
type EventLog = [string, any]
5+
type EventLog = [string, any, string | null]
66
type MutationLog = [string, string | null, string | null]
77

88
export class TurboDriveTestCase extends FunctionalTestCase {
@@ -40,6 +40,15 @@ export class TurboDriveTestCase extends FunctionalTestCase {
4040
return !records.some(([name]) => name == eventName)
4141
}
4242

43+
async nextEventOnTarget(elementId: string, eventName: string): Promise<any> {
44+
let record: EventLog | undefined
45+
while (!record) {
46+
const records = await this.eventLogChannel.read(1)
47+
record = records.find(([name, _, id]) => name == eventName && id == elementId)
48+
}
49+
return record[1]
50+
}
51+
4352
async nextAttributeMutationNamed(elementId: string, attributeName: string): Promise<string | null> {
4453
let record: MutationLog | undefined
4554
while (!record) {

0 commit comments

Comments
 (0)