From a6fe71da3a351210ee21e45ef5014e2edf2ccb60 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Sun, 7 Feb 2021 15:03:51 -0500 Subject: [PATCH] Prepare Frame Form Submission fetch headers Closes https://github.com/hotwired/turbo/issues/86 Closes https://github.com/hotwired/turbo/pull/110 When submitting a Form that is within a `` or targets a ``, ensure that the `Turbo-Frame` header is present. Since the constructive-style `FetchRequestDelegate.additionalHeadersForRequest()` was replaced by the mutative style `FetchRequestDelegate.prepareHeadersForRequest()`, this commit changes the `FetchRequest.headers` property to be `readonly` and created at constructor-time so that its values can be mutated prior to the request's submission. Testing adds listeners for `turbo:before-fetch-request` and `turbo:before-fetch-response` so that the event logs can drain. Co-authored-by: tleish --- src/core/frames/frame_controller.ts | 2 ++ src/http/fetch_request.ts | 20 ++++++++----------- src/tests/fixtures/test.js | 5 +++-- src/tests/functional/form_submission_tests.ts | 16 +++++++++++++++ 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/core/frames/frame_controller.ts b/src/core/frames/frame_controller.ts index d0d507518..bc7079116 100644 --- a/src/core/frames/frame_controller.ts +++ b/src/core/frames/frame_controller.ts @@ -118,6 +118,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..532ea3e08 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 @@ -55,6 +56,7 @@ export class FetchRequest { this.body = body this.url = location } + this.headers = prepareHeadersForRequest(this) } get location(): URL { @@ -116,23 +118,17 @@ export class FetchRequest { return this.method == FetchMethod.get } - get headers() { - const headers = { ...this.defaultHeaders } - if (typeof this.delegate.prepareHeadersForRequest == "function") { - this.delegate.prepareHeadersForRequest(headers, this) - } - return headers - } - get abortSignal() { return this.abortController.signal } +} - get defaultHeaders() { - return { - "Accept": "text/html, application/xhtml+xml" - } +function prepareHeadersForRequest(fetchRequest: FetchRequest) { + const headers = { "Accept": "text/html, application/xhtml+xml" } + if (typeof fetchRequest.delegate.prepareHeadersForRequest == "function") { + fetchRequest.delegate.prepareHeadersForRequest(headers, fetchRequest) } + return headers } function mergeFormDataEntries(url: URL, entries: [string, FormDataEntryValue][]): URL { diff --git a/src/tests/fixtures/test.js b/src/tests/fixtures/test.js index 0cfff6493..9d5fef0eb 100644 --- a/src/tests/fixtures/test.js +++ b/src/tests/fixtures/test.js @@ -16,6 +16,7 @@ "turbo:before-visit", "turbo:load", "turbo:render", - "turbo:request-end", - "turbo:visit" + "turbo:visit", + "turbo:before-fetch-request", + "turbo:before-fetch-response", ]) diff --git a/src/tests/functional/form_submission_tests.ts b/src/tests/functional/form_submission_tests.ts index fe90a07cb..b1adb20e0 100644 --- a/src/tests/functional/form_submission_tests.ts +++ b/src/tests/functional/form_submission_tests.ts @@ -183,6 +183,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 @@ -260,6 +268,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]") }