Skip to content

Commit

Permalink
Prepare Frame Form Submission fetch headers
Browse files Browse the repository at this point in the history
Closes hotwired#86
Closes hotwired#110

When submitting a Form that is within a `<turbo-frame>` or targets a
`<turbo-frame>`, ensure that the `Turbo-Frame` header is present.

Since the constructive-style
`FetchRequestDelegate.additionalHeadersForRequest()` was replaced by the
mutative style `FetchRequestDelegate.prepareHeadersForRequest()`, this
commit introduces a readonly `FetchRequestHeaders` property created at
constructor-time so that its values can be mutated prior to the
request's submission.

Co-authored-by: tleish <tleish@users.noreply.github.com>
  • Loading branch information
seanpdoyle and tleish committed Apr 9, 2021
1 parent 7a84c14 commit 5f9a8e8
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 14 deletions.
2 changes: 2 additions & 0 deletions src/core/frames/frame_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ export class FrameController implements AppearanceObserverDelegate, FetchRequest
if (this.formSubmission.fetchRequest.isIdempotent) {
this.navigateFrame(element, this.formSubmission.fetchRequest.url.href)
} else {
const { fetchRequest } = this.formSubmission
this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest)
this.formSubmission.start()
}
}
Expand Down
23 changes: 9 additions & 14 deletions src/http/fetch_request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,15 @@ export interface FetchRequestOptions {
export class FetchRequest {
readonly delegate: FetchRequestDelegate
readonly method: FetchMethod
readonly headers: FetchRequestHeaders
readonly url: URL
readonly body?: FetchRequestBody
readonly abortController = new AbortController

constructor(delegate: FetchRequestDelegate, method: FetchMethod, location: URL, body: FetchRequestBody = new URLSearchParams) {
this.delegate = delegate
this.method = method
this.headers = this.defaultHeaders
if (this.isIdempotent) {
this.url = mergeFormDataEntries(location, [ ...body.entries() ])
} else {
Expand All @@ -75,6 +77,7 @@ export class FetchRequest {

async perform(): Promise<FetchResponse> {
const { fetchOptions } = this
this.delegate.prepareHeadersForRequest?.(this.headers, this)
dispatch("turbo:before-fetch-request", { detail: { fetchOptions } })
try {
this.delegate.requestStarted(this)
Expand Down Expand Up @@ -112,27 +115,19 @@ export class FetchRequest {
}
}

get isIdempotent() {
return this.method == FetchMethod.get
get defaultHeaders() {
return {
"Accept": "text/html, application/xhtml+xml"
}
}

get headers() {
const headers = { ...this.defaultHeaders }
if (typeof this.delegate.prepareHeadersForRequest == "function") {
this.delegate.prepareHeadersForRequest(headers, this)
}
return headers
get isIdempotent() {
return this.method == FetchMethod.get
}

get abortSignal() {
return this.abortController.signal
}

get defaultHeaders() {
return {
"Accept": "text/html, application/xhtml+xml"
}
}
}

function mergeFormDataEntries(url: URL, entries: [string, FormDataEntryValue][]): URL {
Expand Down
16 changes: 16 additions & 0 deletions src/tests/functional/form_submission_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,14 @@ export class FormSubmissionTests extends TurboDriveTestCase {
this.assert.equal(htmlAfter, htmlBefore)
}

async "test frame form submission within a frame submits the Turbo-Frame header"() {
await this.clickSelector("#frame form.redirect input[type=submit]")

const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request")

this.assert.ok(fetchOptions.headers["Turbo-Frame"], "submits with the Turbo-Frame header")
}

async "test invalid frame form submission with unprocessable entity status"() {
await this.clickSelector("#frame form.unprocessable_entity input[type=submit]")
await this.nextBeat
Expand Down Expand Up @@ -298,6 +306,14 @@ export class FormSubmissionTests extends TurboDriveTestCase {
this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html")
}

async "test form submission targeting a frame submits the Turbo-Frame header"() {
await this.clickSelector('#targets-frame [type="submit"]')

const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request")

this.assert.ok(fetchOptions.headers["Turbo-Frame"], "submits with the Turbo-Frame header")
}

get formSubmitted(): Promise<boolean> {
return this.hasSelector("html[data-form-submitted]")
}
Expand Down

0 comments on commit 5f9a8e8

Please sign in to comment.