diff --git a/src/core/frames/frame_controller.ts b/src/core/frames/frame_controller.ts index 61a316b85..c1e32ff16 100644 --- a/src/core/frames/frame_controller.ts +++ b/src/core/frames/frame_controller.ts @@ -138,6 +138,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() } } diff --git a/src/http/fetch_request.ts b/src/http/fetch_request.ts index b5cbd4d6a..2d99c28a6 100644 --- a/src/http/fetch_request.ts +++ b/src/http/fetch_request.ts @@ -42,6 +42,7 @@ 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 @@ -49,6 +50,7 @@ export class FetchRequest { 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 { @@ -75,6 +77,7 @@ export class FetchRequest { async perform(): Promise { const { fetchOptions } = this + this.delegate.prepareHeadersForRequest?.(this.headers, this) dispatch("turbo:before-fetch-request", { detail: { fetchOptions } }) try { this.delegate.requestStarted(this) @@ -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 { diff --git a/src/tests/functional/form_submission_tests.ts b/src/tests/functional/form_submission_tests.ts index f39c68c4a..916022bef 100644 --- a/src/tests/functional/form_submission_tests.ts +++ b/src/tests/functional/form_submission_tests.ts @@ -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 @@ -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 { return this.hasSelector("html[data-form-submitted]") }