Skip to content

Commit

Permalink
turbo:frame-missing: Do not Turbo.visit by default
Browse files Browse the repository at this point in the history
Restore the existing default behavior when a matching frame is missing
from a response: log an error and blank the frame.

Alongside that original behavior, yield the [Response][] instance and a
`Turbo.visit`-like callback that can transform the `Response` instance
into a `Visit`. It also accepts all the arguments that the `Turbo.visit`
call normally does.

[Response]: https://developer.mozilla.org/en-US/docs/Web/API/Response
  • Loading branch information
seanpdoyle committed Aug 10, 2022
1 parent c4e0aba commit 520e10e
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 25 deletions.
29 changes: 22 additions & 7 deletions src/core/frames/frame_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
import { FormSubmission, FormSubmissionDelegate } from "../drive/form_submission"
import { Snapshot } from "../snapshot"
import { ViewDelegate, ViewRenderOptions } from "../view"
import { getAction, expandURL, urlsAreEqual, locationIsVisitable } from "../url"
import { Locatable, getAction, expandURL, urlsAreEqual, locationIsVisitable } from "../url"
import { FormSubmitObserver, FormSubmitObserverDelegate } from "../../observers/form_submit_observer"
import { FrameView } from "./frame_view"
import { LinkClickObserver, LinkClickObserverDelegate } from "../../observers/link_click_observer"
Expand All @@ -32,7 +32,8 @@ import { VisitOptions } from "../drive/visit"
import { TurboBeforeFrameRenderEvent, TurboFetchRequestErrorEvent } from "../session"
import { StreamMessage } from "../streams/stream_message"

export type TurboFrameMissingEvent = CustomEvent<{ fetchResponse: FetchResponse }>
type VisitFallback = (location: Response | Locatable, options: Partial<VisitOptions>) => Promise<void>
export type TurboFrameMissingEvent = CustomEvent<{ response: Response; visit: VisitFallback }>

export class FrameController
implements
Expand Down Expand Up @@ -169,8 +170,9 @@ export class FrameController
session.frameRendered(fetchResponse, this.element)
session.frameLoaded(this.element)
this.fetchResponseLoaded(fetchResponse)
} else if (this.sessionWillHandleMissingFrame(fetchResponse)) {
await session.frameMissing(this.element, fetchResponse)
} else if (this.willHandleFrameMissingFromResponse(fetchResponse)) {
console.error(`Response has no matching <turbo-frame id="${this.element.id}"> element`)
this.element.innerHTML = ""
}
}
} catch (error) {
Expand Down Expand Up @@ -398,16 +400,29 @@ export class FrameController
}
}

private sessionWillHandleMissingFrame(fetchResponse: FetchResponse) {
private willHandleFrameMissingFromResponse(fetchResponse: FetchResponse): boolean {
this.element.setAttribute("complete", "")

const response = fetchResponse.response
const visit = async (url: Locatable | Response, options: Partial<VisitOptions> = {}) => {
if (url instanceof Response) {
const wrapped = new FetchResponse(url)
const responseHTML = await wrapped.responseHTML
const { location, redirected, statusCode } = wrapped

session.visit(location, { response: { redirected, statusCode, responseHTML }, ...options })
} else {
session.visit(url, options)
}
}

const event = dispatch<TurboFrameMissingEvent>("turbo:frame-missing", {
target: this.element,
detail: { fetchResponse },
detail: { response, visit },
cancelable: true,
})

return !event.defaultPrevented
return event.defaultPrevented
}

private findFrameElement(element: Element, submitter?: HTMLElement) {
Expand Down
5 changes: 0 additions & 5 deletions src/core/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,11 +309,6 @@ export class Session
this.notifyApplicationAfterFrameRender(fetchResponse, frame)
}

frameMissing(frame: FrameElement, fetchResponse: FetchResponse): Promise<void> {
console.warn(`Completing full-page visit as matching frame for #${frame.id} was missing from the response`)
return this.visit(fetchResponse.location)
}

// Application events

applicationAllowsFollowingLinkToLocation(link: Element, location: URL, ev: MouseEvent) {
Expand Down
50 changes: 37 additions & 13 deletions src/tests/functional/frame_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,29 +119,22 @@ test("test following a link to a page without a matching frame dispatches a turb
await page.click("#missing a")
await noNextEventOnTarget(page, "missing", "turbo:frame-render")
await noNextEventOnTarget(page, "missing", "turbo:frame-load")
const { fetchResponse } = await nextEventOnTarget(page, "missing", "turbo:frame-missing")
await nextEventNamed(page, "turbo:load")

assert.ok(fetchResponse, "dispatchs turbo:frame-missing with event.detail.fetchResponse")
assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/frame.html", "navigates the page")

await page.goBack()
await nextEventNamed(page, "turbo:load")
const { response } = await nextEventOnTarget(page, "missing", "turbo:frame-missing")

assert.equal(pathname(page.url()), "/src/tests/fixtures/frames.html")
assert.ok(await innerHTMLForSelector(page, "#missing"))
assert.ok(response, "dispatches turbo:frame-missing with event.detail.response")
assert.equal(pathname(page.url()), "/src/tests/fixtures/frames.html", "stays on the page")
assert.equal(await page.locator("#missing").innerHTML(), "", "blanks the contents when not canceled")
})

test("test following a link to a page without a matching frame dispatches a turbo:frame-missing event that can be cancelled", async ({
test("test the turbo:frame-missing event following a link to a page without a matching frame can be handled", async ({
page,
}) => {
await page.locator("#missing").evaluate((frame) => {
frame.addEventListener(
"turbo:frame-missing",
(event) => {
event.preventDefault()

if (event.target instanceof HTMLElement) {
event.preventDefault()
event.target.textContent = "Overridden"
}
},
Expand All @@ -154,6 +147,37 @@ test("test following a link to a page without a matching frame dispatches a turb
assert.equal(await page.textContent("#missing"), "Overridden")
})

test("test the turbo:frame-missing event following a link to a page without a matching frame can drive a Visit", async ({
page,
}) => {
await page.locator("#missing").evaluate((frame) => {
frame.addEventListener(
"turbo:frame-missing",
(event) => {
if (event instanceof CustomEvent) {
event.preventDefault()
const { response, visit } = event.detail

visit(response)
}
},
{ once: true }
)
})
await page.click("#missing a")
await nextEventOnTarget(page, "missing", "turbo:frame-missing")
await nextEventNamed(page, "turbo:load")

assert.equal(await page.textContent("h1"), "Frames: #frame")
assert.notOk(await hasSelector(page, "turbo-frame#missing"))

await page.goBack()
await nextEventNamed(page, "turbo:load")

assert.equal(pathname(page.url()), "/src/tests/fixtures/frames.html")
assert.equal(await innerHTMLForSelector(page, "#missing"), "")
})

test("test following a link to a page with a matching frame does not dispatch a turbo:frame-missing event", async ({
page,
}) => {
Expand Down

0 comments on commit 520e10e

Please sign in to comment.