Skip to content

Commit

Permalink
Introduce turbo:frame-missing event
Browse files Browse the repository at this point in the history
Closes [#432][]
Follow-up to [#94][]
Follow-up to [#31][]

When a response from _within_ a frame is missing a matching frame, fire
the `turbo:frame-missing` event.

There is an existing [contract][] that dictates a request from within a
frame stays within a frame.

However, if an application is interested in reacting to a response
without a frame, dispatch a `turbo:frame-missing` event. The event's
`target` is the `FrameElement`, and the `detail` contains the `url:`
key, and the `response:` key containing:

* `redirected: boolean`
* `responseHTML: string`
* `statusCode: number`

Event listeners for `turbo:frame-missing` can forward the `detail`
directly to a `Turbo.visit` call:

```js
addEventListener("turbo:frame-missing", ({ target, detail: { url, response } }) => {
  // the details of `shouldRedirectOnMissingFrame(element: FrameElement)`
  // are up to the application to decide
  if (shouldRedirectOnMissingFrame(target)) {
    Turbo.visit(url, { response })
  }
})
```

[contract]: #94 (comment)
[#432]: #432
[#94]: #94
[#31]: #31
  • Loading branch information
seanpdoyle committed Nov 14, 2021
1 parent 59074f0 commit 18c1c64
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 18 deletions.
32 changes: 17 additions & 15 deletions src/core/frames/frame_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { FrameElement, FrameElementDelegate, FrameLoadingStyle } from "../../ele
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 { Visit, VisitDelegate } from "../drive/visit"
import { Snapshot } from "../snapshot"
Expand Down Expand Up @@ -111,8 +111,16 @@ export class FrameController implements AppearanceObserverDelegate, FetchRequest
const renderer = new FrameRenderer(this.view.snapshot, snapshot, false)
if (this.view.renderPromise) await this.view.renderPromise
await this.view.render(renderer)
session.frameRendered(fetchResponse, this.element)
session.frameLoaded(this.element)
if (snapshot.element.delegate.isActive) {
session.frameRendered(fetchResponse, this.element)
session.frameLoaded(this.element)
} else {
const responseHTML = html
const { redirected, statusCode, response: { url } } = fetchResponse
const response = { redirected, responseHTML, statusCode }

dispatch("turbo:frame-missing", { target: this.element, detail: { response, url } })
}
}
} catch (error) {
console.error(error)
Expand Down Expand Up @@ -286,19 +294,13 @@ export class FrameController implements AppearanceObserverDelegate, FetchRequest
let element
const id = CSS.escape(this.id)

try {
if (element = activateElement(container.querySelector(`turbo-frame#${id}`), this.currentURL)) {
return element
}

if (element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`), this.currentURL)) {
await element.loaded
return await this.extractForeignFrameElement(element)
}
if (element = activateElement(container.querySelector(`turbo-frame#${id}`), this.currentURL)) {
return element
}

console.error(`Response has no matching <turbo-frame id="${id}"> element`)
} catch (error) {
console.error(error)
if (element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`), this.currentURL)) {
await element.loaded
return await this.extractForeignFrameElement(element)
}

return new FrameElement()
Expand Down
1 change: 1 addition & 0 deletions src/elements/frame_element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface FrameElementDelegate {
linkClickIntercepted(element: Element, url: string): void
loadResponse(response: FetchResponse): void
isLoading: boolean
isActive: boolean
}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/tests/fixtures/frames.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ <h2>Frames: #nested-child</h2>

<turbo-frame id="missing">
<a href="/src/tests/fixtures/frames/frame.html">Missing frame</a>
<form action="/__turbo/redirect">
<input type="hidden" name="path" value="/src/tests/fixtures/frames/frame.html">
<button>Missing frame</button>
</form>
</turbo-frame>

<turbo-frame id="body-script" target="body-script">
Expand Down
1 change: 1 addition & 0 deletions src/tests/fixtures/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@
"turbo:visit",
"turbo:frame-load",
"turbo:frame-render",
"turbo:frame-missing",
])
42 changes: 39 additions & 3 deletions src/tests/functional/frame_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export class FrameTests extends TurboDriveTestCase {
async "test a frame whose src references itself does not infinitely loop"() {
await this.clickSelector("#frame-self")

await this.nextEventOnTarget("frame", "turbo:frame-render")
await this.nextEventOnTarget("frame", "turbo:frame-load")
await this.nextEventOnTarget("frame", "turbo:before-fetch-request")
await this.nextEventOnTarget("frame", "turbo:before-fetch-response")

const otherEvents = await this.eventLogChannel.read()
this.assert.equal(otherEvents.length, 0, "no more events")
Expand All @@ -37,8 +37,14 @@ export class FrameTests extends TurboDriveTestCase {

async "test following a link to a page without a matching frame results in an empty frame"() {
await this.clickSelector("#missing a")
await this.nextBeat

const { response, url } = await this.nextEventOnTarget("missing", "turbo:frame-missing")

this.assert.notOk(await this.innerHTMLForSelector("#missing"))
this.assert.equal((new URL(url)).pathname, "/src/tests/fixtures/frames/frame.html")
this.assert.equal(response.statusCode, "200")
this.assert.notOk(response.redirected)
this.assert.ok(response.responseHTML)
}

async "test following a link within a frame with a target set navigates the target frame"() {
Expand Down Expand Up @@ -407,6 +413,26 @@ export class FrameTests extends TurboDriveTestCase {
this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html")
}

async "test navigating frame resulting in response without matching frame can be re-purposed to navigate entire page"() {
await this.proposeVisitWhenFrameIsMissingInResponse()
await this.clickSelector("#missing a")
await this.nextBody

this.assert.notOk(await this.hasSelector("#missing"))
this.assert.equal(await (await this.querySelector("h1")).getVisibleText(), "Frames: #frame")
this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html")
}

async "test submitting frame resulting in response without matching frame can be re-purposed to navigate entire page"() {
await this.proposeVisitWhenFrameIsMissingInResponse()
await this.clickSelector("#missing button")
await this.nextBody

this.assert.notOk(await this.hasSelector("#missing"))
this.assert.equal(await (await this.querySelector("h1")).getVisibleText(), "Frames: #frame")
this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html")
}

async "test turbo:before-fetch-request fires on the frame element"() {
await this.clickSelector("#hello a")
this.assert.ok(await this.nextEventOnTarget("frame", "turbo:before-fetch-request"))
Expand All @@ -420,6 +446,16 @@ export class FrameTests extends TurboDriveTestCase {
get frameScriptEvaluationCount(): Promise<number | undefined> {
return this.evaluate(() => window.frameScriptEvaluationCount)
}

proposeVisitWhenFrameIsMissingInResponse(): Promise<void> {
return this.evaluate(() => {
addEventListener("turbo:frame-missing", (event: Event) => {
const { detail: { url, response } } = event as CustomEvent

window.Turbo.visit(url, { response })
})
})
}
}

declare global {
Expand Down

0 comments on commit 18c1c64

Please sign in to comment.