Skip to content

Commit

Permalink
Push history state from frame navigations
Browse files Browse the repository at this point in the history
hotwired#50
hotwired#361

---

Extend of built-in support for `<a>` elements with [data-turbo-action][]
(with `"replace"` or `"advance"`) to also encompass `<turbo-frame>`
navigations.

Account for the combination of of `[data-turbo-frame]` and
`[data-turbo-action]` to navigate the target `<turbo-frame>` _and_
navigate the page's histroy push state, supporting:

* `turbo-frame[data-turbo-action="..."]`
* `turbo-frame a[data-turbo-action="..."]`
* `a[data-turbo-frame="..."][data-turbo-action="..."]`
* `form[data-turbo-frame="..."][data-turbo-action="..."]`
* `form[data-turbo-frame="..."] button[data-turbo-action="..."]`
* `form button[data-turbo-frame="..."][data-turbo-action="..."]`

Whenever a Turbo Frame response is loaded that was initiated from one of
those submitters, forms, anchors, or turbo-frames annotated with a
`[dat-turbo-action]`, the subsequent firing `turbo:frame-render` event
will create a `Visit` instance that will skip rendering, won't result in
a network request, and will instead only update the snapshot cache and
history.

[data-turbo-action]: https://turbo.hotwired.dev/handbook/drive#application-visits
  • Loading branch information
seanpdoyle committed Sep 17, 2021
1 parent 60759ea commit 2fde275
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 3 deletions.
10 changes: 7 additions & 3 deletions src/core/drive/visit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,16 @@ export enum VisitState {
export type VisitOptions = {
action: Action,
historyChanged: boolean,
willRender: boolean
referrer?: URL,
snapshotHTML?: string,
response?: VisitResponse
}

const defaultOptions: VisitOptions = {
action: "advance",
historyChanged: false
historyChanged: false,
willRender: true
}

export type VisitResponse = {
Expand All @@ -68,6 +70,7 @@ export class Visit implements FetchRequestDelegate {
readonly referrer?: URL
readonly timingMetrics: TimingMetrics = {}

willRender: boolean
followedRedirect = false
frame?: number
historyChanged = false
Expand All @@ -86,7 +89,8 @@ export class Visit implements FetchRequestDelegate {
this.location = location
this.restorationIdentifier = restorationIdentifier || uuid()

const { action, historyChanged, referrer, snapshotHTML, response } = { ...defaultOptions, ...options }
const { action, historyChanged, referrer, snapshotHTML, response, willRender } = { ...defaultOptions, ...options }
this.willRender = willRender
this.action = action
this.historyChanged = historyChanged
this.referrer = referrer
Expand Down Expand Up @@ -200,7 +204,7 @@ export class Visit implements FetchRequestDelegate {
}

loadResponse() {
if (this.response) {
if (this.response && this.willRender) {
const { statusCode, responseHTML } = this.response
this.render(async () => {
this.cacheSnapshot()
Expand Down
23 changes: 23 additions & 0 deletions src/core/frames/frame_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { FrameView } from "./frame_view"
import { LinkInterceptor, LinkInterceptorDelegate } from "./link_interceptor"
import { FrameRenderer } from "./frame_renderer"
import { session } from "../index"
import { isAction } from "../types"

export class FrameController implements AppearanceObserverDelegate, FetchRequestDelegate, FormInterceptorDelegate, FormSubmissionDelegate, FrameElementDelegate, LinkInterceptorDelegate, ViewDelegate<Snapshot<FrameElement>> {
readonly element: FrameElement
Expand Down Expand Up @@ -202,6 +203,9 @@ export class FrameController implements AppearanceObserverDelegate, FetchRequest

formSubmissionSucceededWithResponse(formSubmission: FormSubmission, response: FetchResponse) {
const frame = this.findFrameElement(formSubmission.formElement, formSubmission.submitter)

this.proposeVisitFromFrameResponse(frame, formSubmission.formElement, formSubmission.submitter)

frame.delegate.loadResponse(response)
}

Expand Down Expand Up @@ -246,10 +250,29 @@ export class FrameController implements AppearanceObserverDelegate, FetchRequest

private navigateFrame(element: Element, url: string, submitter?: HTMLElement) {
const frame = this.findFrameElement(element, submitter)

this.proposeVisitFromFrameResponse(frame, element, submitter)

frame.setAttribute("reloadable", "")
frame.src = url
}

private proposeVisitFromFrameResponse(frame: FrameElement, element: Element, submitter?: HTMLElement) {
const action = submitter?.getAttribute("data-turbo-action") || element.getAttribute("data-turbo-action") || frame.getAttribute("data-turbo-action")

if (isAction(action)) {
const proposeVisit = async (event: Event) => {
const { detail: { fetchResponse } } = event as CustomEvent
const responseHTML = await fetchResponse.responseHTML
const statusCode = fetchResponse.statusCode

session.visit(fetchResponse.location, { willRender: false, response: { statusCode, responseHTML }, action })
}

frame.addEventListener("turbo:frame-render", proposeVisit , { once: true })
}
}

private findFrameElement(element: Element, submitter?: HTMLElement) {
const id = submitter?.getAttribute("data-turbo-frame") || element.getAttribute("data-turbo-frame") || this.element.getAttribute("target")
return getFrameElementById(id) ?? this.element
Expand Down
19 changes: 19 additions & 0 deletions src/tests/fixtures/frames.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,27 @@ <h1>Frames</h1>

<turbo-frame id="frame" data-loaded-from="/src/tests/fixtures/frames.html">
<h2>Frames: #frame</h2>

<a id="link-frame" href="/src/tests/fixtures/frames/frame.html">Navigate #frame from within</a>
<a id="link-frame-action-advance" href="/src/tests/fixtures/frames/frame.html">Navigate #frame from within with a[data-turbo-action="advance"]</a>
</turbo-frame>

<a id="link-frame-action-advance" href="/src/tests/fixtures/frames/frame.html" data-turbo-frame="frame" data-turbo-action="advance">Navigate #frame with data-turbo-action="advance"</a>
<form id="form-get-frame-action-advance" action="/__turbo/redirect" data-turbo-frame="frame" data-turbo-action="advance">
<input type="hidden" name="path" value="/src/tests/fixtures/frames/frame.html">
<button>Navigate #frame with GET form[data-turbo-action="advance"]</button>
</form>

<form id="form-post-frame-action-advance" method="post" action="/__turbo/redirect" data-turbo-frame="frame" data-turbo-action="advance">
<input type="hidden" name="path" value="/src/tests/fixtures/frames/frame.html">
<button>Navigate #frame with POST form[data-turbo-action="advance"]</button>
</form>

<form method="post" action="/__turbo/redirect" data-turbo-frame="frame" data-turbo-action="advance">
<input type="hidden" name="path" value="/src/tests/fixtures/frames/frame.html">
<button id="button-frame-action-advance" data-turbo-action="advance">Navigate #frame with button[data-turbo-action="advance"]</button>
</form>

<turbo-frame id="hello" target="frame">
<h2>Frames: #hello</h2>

Expand Down
2 changes: 2 additions & 0 deletions src/tests/fixtures/frames/frame.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
</head>
<body>
<h1>Frames: #frame</h1>

<turbo-frame id="frame" data-loaded-from="/src/tests/fixtures/frames/frame.html">
<h2>Frame: Loaded</h2>
</turbo-frame>
Expand Down
55 changes: 55 additions & 0 deletions src/tests/functional/frame_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,61 @@ export class FrameTests extends TurboDriveTestCase {
this.assert.equal(requestLogs.length, 0)
}

async "test navigating turbo-frame[data-turbo-action=advance] from within pushes URL state"() {
await this.remote.execute(() => document.getElementById("frame")?.setAttribute("data-turbo-action", "advance"))
await this.clickSelector("#link-frame")

const title = await this.querySelector("h1")

this.assert.equal(await title.getVisibleText(), "Frames")
this.assert.equal(await this.pathname, "/src/tests/fixtures/frames.html")
}

async "test navigating turbo-frame from within with a[data-turbo-action=advance] pushes URL state"() {
await this.clickSelector("#link-frame-action-advance")

const title = await this.querySelector("h1")

this.assert.equal(await title.getVisibleText(), "Frames")
this.assert.equal(await this.pathname, "/src/tests/fixtures/frames.html")
}

async "test navigating frame with a[data-turbo-action=advance] pushes URL state"() {
await this.clickSelector("#link-frame-action-advance")

const title = await this.querySelector("h1")

this.assert.equal(await title.getVisibleText(), "Frames")
this.assert.equal(await this.pathname, "/src/tests/fixtures/frames.html")
}

async "test navigating frame with form[method=get][data-turbo-action=advance] pushes URL state"() {
await this.clickSelector("#form-get-frame-action-advance button")

const title = await this.querySelector("h1")

this.assert.equal(await title.getVisibleText(), "Frames")
this.assert.equal(await this.pathname, "/src/tests/fixtures/frames.html")
}

async "test navigating frame with form[method=post][data-turbo-action=advance] pushes URL state"() {
await this.clickSelector("#form-post-frame-action-advance button")

const title = await this.querySelector("h1")

this.assert.equal(await title.getVisibleText(), "Frames")
this.assert.equal(await this.pathname, "/src/tests/fixtures/frames.html")
}

async "test navigating frame with button[data-turbo-action=advance] pushes URL state"() {
await this.clickSelector("#button-frame-action-advance")

const title = await this.querySelector("h1")

this.assert.equal(await title.getVisibleText(), "Frames")
this.assert.equal(await this.pathname, "/src/tests/fixtures/frames.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 Down

0 comments on commit 2fde275

Please sign in to comment.