From 5f9a8e87e3d46b6d5e08a0c984c403391d27e2e0 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 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 --- src/core/frames/frame_controller.ts | 2 ++ src/http/fetch_request.ts | 23 ++++++++----------- src/tests/functional/form_submission_tests.ts | 16 +++++++++++++ 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/core/frames/frame_controller.ts b/src/core/frames/frame_controller.ts index dd09a74d5..94bd8fba5 100644 --- a/src/core/frames/frame_controller.ts +++ b/src/core/frames/frame_controller.ts @@ -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() } } 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]") }