-
Notifications
You must be signed in to change notification settings - Fork 436
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
GET
Submissions: don't merge into searchParams
#461
Conversation
7f5936d
to
cc03835
Compare
Dispatch `turbo:before-fetch-request` with a direct reference to the `FetchRequest` instance's `url: URL` property as part of the `event.detail`.
The background --- According to the HTML Specification's [§ 4.10.21.3 Form submission algorithm][] section, submissions transmitted as `GET` requests [mutate the `[action]` URL][mutate], overriding any search parameters already encoded into the `[action]` value: > [Mutate action URL][algorithm] > --- > > Let <var>pairs</var> be the result of converting to a list of > name-value pairs with <var>entry list</var>. > > Let <var>query</var> be the result of running the > `application/x-www-form-urlencoded` serializer with <var>pairs</var> > and <var>encoding</var>. > > Set <Var>parsed action</var>'s query component to <var>query</var>. > > Plan to navigate to <var>parsed action</var>. [§ 4.10.21.3 Form submission algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm [algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-mutate-action [mutate]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm:submit-mutate-action Form submissions made with `POST` requests, on the other hand, encode _both_ the `[action]` value's query parameters and any additionally encoded body data: > [Submit as entity body][post-submit] > --- > > … > > Plan to navigate to a new request whose URL is <var>parsed > action</var>, method is <var>method</var>, header list is > « (`Content-Type`, mimeType) », and body is <var>body</var>. [post-submit]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-body The problem --- Ever since [18585fc][] (and subsequently [hotwired#108][]), Turbo's `FetchRequest` has merged submission values from `<form>` elements with `[method="get"]` and without `[action]` attributes _into_ the current URL's query parameters. For example, consider the following forms rendered on the `/articles` page: ```html <form> <label for="q">Term</label> <input id="q" name="q"> <button>Search</button> </form> <!-- elsewhere in the page --> <form> <button name="order" value="asc">Sort ascending</button> <button name="order" value="desc">Sort ascending</button> </form> ``` Without Turbo, entering "Hotwire" as the search term into `input[name="q"]` and clicking the "Search" button would navigate the browser to `/articles?q=Hotwire`. Then, clicking the "Sort ascending" button would navigate the page to `/articles?order=asc`. Without Turbo, entering "Hotwire" as the search term into `input[name="q"]` and clicking the "Search" button navigates the browser to `/articles?q=Hotwire`. Then, clicking the "Sort ascending" button **navigates the page to `/articles?q=Hotwire&order=asc`**, effectively _merging_ values from the page's URL and the `<form>` element's fields. [18585fc]: hotwired@18585fc#diff-68b647dc2963716dc27c070f261d0b942ee9c00be7c4149ecb3a5acd94842d40R135-R142 [hotwired#108]: hotwired#108 The solution --- This commit modifies the way that `FetchRequest` constructs its `url: URL` and `body: FormData | URLSearchParams` properties. First, it _always_ assigns a `body` property, but _conditionally_ encodes that value into the `fetchOptions: RequestInit` property based on whether or not the request is an idempotent `GET`. Next, if constructed with a `body: URLSearchParams` that has entries, **replace** the `url: URL` instance's search params _entirely_ with those values, like the HTML Specification algorithm entails. If constructed with a `body: URLSearchParams` that is empty, pass the `url: URL` through and assign the property **without modifying it**. Additionally, this commit adds test cases to ensure that `POST` requests transmit data in both the body and the URL. While the previous multi-form merging behavior can be desirable, it is not the behavior outlined by the HTML Specification, so Turbo should not provide it out-of-the-box. Having said that, there are two ways for applications to restore that behavior: 1. declare a [turbo:before-fetch-request][] event listener to merge values _into_ the event's `detail.url` instance: ```js addEventListener("turbo:before-fetch-request", ({ target, detail: { url, fetchOptions: { method } } }) => { if (target instanceof HTMLFormElement && method == "GET") { for (const [ name, value ] of new URLSearchParams(window.location.search)) { // conditionally call `set` or `append`, // depending on your application's needs if (url.searchParams.has(name)) continue else url.searchParams.set(name, value) } } }) ``` 2. declare a [formdata][] event listener to merge values _into_ the submitted form's [FormData][] instance prior to entering the Turbo request pipeline: ```js addEventListener("submit", (event) => { if (event.defaultPrevented) return const { target, submitter } = event const action = submitter?.getAttribute("formaction") || target.getAttribute("action") if (target.method == "get" && !action) { target.addEventListener("formdata", ({ formData }) => { for (const [ name, value ] of new URLSearchParams(window.location.search)) { // conditionally call `set` or `append`, // depending on your application's needs if (formData.has(name)) continue else formData.set(name, value) } }, { once: true }) } }) ``` The conditionals in both cases could be omitted if applications controlled the behavior more directly, like by declaring a Stimulus controller and action (e.g. `<form data-controller="form" data-action="formdata->form#mergeSearchParams">`). [turbo:before-fetch-request]: https://turbo.hotwired.dev/reference/events [formdata]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/formdata_event [FormData]: https://developer.mozilla.org/en-US/docs/Web/API/FormData
cc03835
to
6906e57
Compare
<button id="form-action-none-q-a" name="q" value="a">Submit ?q=a to form:not([action])</button> | ||
</form> | ||
<form action="/src/tests/fixtures/form.html?sort=asc"> | ||
<button id="form-action-self-submit">Submit form[action]</button> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've just discovered that this is still slightly incorrect behavior.
I tested with an application without Turbo or any other JavaScript, and submitting a form with an [action]
value that contains query parameters requests that [action]
path without any key-value pairs (that is, with ?
as its the value of URL.search
):
<form action="/src/tests/fixtures/form.html?sort=asc">
<!-- clicking the button navigates to `/src/tests/fixtures/form?` -->
<button>Submit</button>
</form>
I'm in the process of adding test coverage and opening a separate PR.
Follow-up to [hotwired#461][] While the changes made in hotwired#461 added coverage for `<form method="post" action="...">` and `<form>` submissions, it didn't cover `<form action="/?encoded=value">` submissions with search parameters encoded into the `[action]` attribute. Without Turbo or any other JavaScript, submitting a form with an `[action]` value that contains query parameters requests that `[action]` path without any key-value pairs (that is, with `?` as its the value of `URL.search`): ```html <form action="/src/tests/fixtures/form.html?sort=asc"> <!-- clicking the button navigates to `/src/tests/fixtures/form?` --> <button>Submit</button> </form> ``` This commit modifies the `FetchRequest` to always invoke the module-private `mergeFormDataEntries()` function when constructing `GET` requests, and modifies the `mergeFormDataEntries()` to completely discard the `[action]` URL's search parameters before merging-in the `FormData` key-value pairs. [hotwired#461]: hotwired#461
Follow-up to [hotwired#461][] While the changes made in hotwired#461 added coverage for `<form method="post" action="...">` and `<form>` submissions, it didn't cover `<form action="/?encoded=value">` submissions with search parameters encoded into the `[action]` attribute. Without Turbo or any other JavaScript, submitting a form with an `[action]` value that contains query parameters requests that `[action]` path without any key-value pairs (that is, with `?` as its the value of `URL.search`): ```html <form action="/src/tests/fixtures/form.html?sort=asc"> <!-- clicking the button navigates to `/src/tests/fixtures/form?` --> <button>Submit</button> </form> ``` This commit modifies the `FetchRequest` to always invoke the module-private `mergeFormDataEntries()` function when constructing `GET` requests, and modifies the `mergeFormDataEntries()` to completely discard the `[action]` URL's search parameters before merging-in the `FormData` key-value pairs. [hotwired#461]: hotwired#461
Follow-up to [#461][] While the changes made in #461 added coverage for `<form method="post" action="...">` and `<form>` submissions, it didn't cover `<form action="/?encoded=value">` submissions with search parameters encoded into the `[action]` attribute. Without Turbo or any other JavaScript, submitting a form with an `[action]` value that contains query parameters requests that `[action]` path without any key-value pairs (that is, with `?` as its the value of `URL.search`): ```html <form action="/src/tests/fixtures/form.html?sort=asc"> <!-- clicking the button navigates to `/src/tests/fixtures/form?` --> <button>Submit</button> </form> ``` This commit modifies the `FetchRequest` to always invoke the module-private `mergeFormDataEntries()` function when constructing `GET` requests, and modifies the `mergeFormDataEntries()` to completely discard the `[action]` URL's search parameters before merging-in the `FormData` key-value pairs. [#461]: #461
Closes [hotwired#820][] Follow-up to [hotwired#461][] [hotwired#820]: hotwired#820 [hotwired#461]: hotwired#461 The background --- According to the HTML Specification's [§ 4.10.21.3 Form submission algorithm][] section, submissions transmitted as `GET` requests [mutate the `[action]` URL][mutate], overriding any search parameters already encoded into the `[action]` value: > [Mutate action URL][algorithm] > --- > > Let <var>pairs</var> be the result of converting to a list of > name-value pairs with <var>entry list</var>. > > Let <var>query</var> be the result of running the > `application/x-www-form-urlencoded` serializer with <var>pairs</var> > and <var>encoding</var>. > > Set <Var>parsed action</var>'s query component to <var>query</var>. > > Plan to navigate to <var>parsed action</var>. [§ 4.10.21.3 Form submission algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm [algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-mutate-action [mutate]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm:submit-mutate-action Form submissions made with `POST` requests, on the other hand, encode _both_ the `[action]` value's query parameters and any additionally encoded body data: > [Submit as entity body][post-submit] > --- > > … > > Plan to navigate to a new request whose URL is <var>parsed > action</var>, method is <var>method</var>, header list is > « (`Content-Type`, mimeType) », and body is <var>body</var>. [post-submit]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-body The problem --- When navigating through an `<a data-turbo-method="get">` with an `[href]` that contains search parameters, the `<form>` element that Turbo Drive constructs behind the scenes will unexpectedly omit the search parameters during the transformation from `<a>` to `<form>`. For example: ```html <a href="/path-with-search-params?a=one&b=two" data-turbo-method="get"> Submits as a form[method=get] </a> ``` Would be translated to something like: ```html <form method="get" action="/path-with-search-params?a=one&b=two" hidden></form> ``` Then, when submitting the `<form>`, the browser omits the `?one&b=two`. The solution --- This commit extends the current `FormLinkClickObserver` implementation to loop over each entry in [URLSearchParams][] instance derived from the provided [URL][] instance and transforms the name-value pair into an [`<input type="hidden">`][input-hidden] element, which it appends to the `<form>`. Once that translation is complete, it removes any search parameters from the form's `[action]` attribute to avoid duplicate entries in the form's data. While this change resolves the underlying issue, it's worth emphasizing that declaring `[data-turbo-method="get"]` on an `<a>` element is redundant, and should be avoided in the first place. Even though that pattern should be avoided, this commit aims to prevent surprising behavior whenever it occurs. [URLSearchParams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams [URL]: https://developer.mozilla.org/en-US/docs/Web/API/URL [input-hidden]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden
Closes [hotwired#820][] Follow-up to [hotwired#461][] [hotwired#820]: hotwired#820 [hotwired#461]: hotwired#461 The background --- According to the HTML Specification's [§ 4.10.21.3 Form submission algorithm][] section, submissions transmitted as `GET` requests [mutate the `[action]` URL][mutate], overriding any search parameters already encoded into the `[action]` value: > [Mutate action URL][algorithm] > --- > > Let <var>pairs</var> be the result of converting to a list of > name-value pairs with <var>entry list</var>. > > Let <var>query</var> be the result of running the > `application/x-www-form-urlencoded` serializer with <var>pairs</var> > and <var>encoding</var>. > > Set <Var>parsed action</var>'s query component to <var>query</var>. > > Plan to navigate to <var>parsed action</var>. [§ 4.10.21.3 Form submission algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm [algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-mutate-action [mutate]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm:submit-mutate-action Form submissions made with `POST` requests, on the other hand, encode _both_ the `[action]` value's query parameters and any additionally encoded body data: > [Submit as entity body][post-submit] > --- > > … > > Plan to navigate to a new request whose URL is <var>parsed > action</var>, method is <var>method</var>, header list is > « (`Content-Type`, mimeType) », and body is <var>body</var>. [post-submit]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-body The problem --- When navigating through an `<a data-turbo-method="get">` with an `[href]` that contains search parameters, the `<form>` element that Turbo Drive constructs behind the scenes will unexpectedly omit the search parameters during the transformation from `<a>` to `<form>`. For example: ```html <a href="/path-with-search-params?a=one&b=two" data-turbo-method="get"> Submits as a form[method=get] </a> ``` Would be translated to something like: ```html <form method="get" action="/path-with-search-params?a=one&b=two" hidden></form> ``` Then, when submitting the `<form>`, the browser omits the `?one&b=two`. The solution --- This commit extends the current `FormLinkClickObserver` implementation to loop over each entry in [URLSearchParams][] instance derived from the provided [URL][] instance and transforms the name-value pair into an [`<input type="hidden">`][input-hidden] element, which it appends to the `<form>`. Once that translation is complete, it removes any search parameters from the form's `[action]` attribute to avoid duplicate entries in the form's data. While this change resolves the underlying issue, it's worth emphasizing that declaring `[data-turbo-method="get"]` on an `<a>` element is redundant, and should be avoided in the first place. Even though that pattern should be avoided, this commit aims to prevent surprising behavior whenever it occurs. [URLSearchParams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams [URL]: https://developer.mozilla.org/en-US/docs/Web/API/URL [input-hidden]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden
Closes [hotwired#820][] Follow-up to [hotwired#461][] [hotwired#820]: hotwired#820 [hotwired#461]: hotwired#461 The background --- According to the HTML Specification's [§ 4.10.21.3 Form submission algorithm][] section, submissions transmitted as `GET` requests [mutate the `[action]` URL][mutate], overriding any search parameters already encoded into the `[action]` value: > [Mutate action URL][algorithm] > --- > > Let <var>pairs</var> be the result of converting to a list of > name-value pairs with <var>entry list</var>. > > Let <var>query</var> be the result of running the > `application/x-www-form-urlencoded` serializer with <var>pairs</var> > and <var>encoding</var>. > > Set <Var>parsed action</var>'s query component to <var>query</var>. > > Plan to navigate to <var>parsed action</var>. [§ 4.10.21.3 Form submission algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm [algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-mutate-action [mutate]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm:submit-mutate-action Form submissions made with `POST` requests, on the other hand, encode _both_ the `[action]` value's query parameters and any additionally encoded body data: > [Submit as entity body][post-submit] > --- > > … > > Plan to navigate to a new request whose URL is <var>parsed > action</var>, method is <var>method</var>, header list is > « (`Content-Type`, mimeType) », and body is <var>body</var>. [post-submit]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-body The problem --- When navigating through an `<a data-turbo-method="get">` with an `[href]` that contains search parameters, the `<form>` element that Turbo Drive constructs behind the scenes will unexpectedly omit the search parameters during the transformation from `<a>` to `<form>`. For example: ```html <a href="/path-with-search-params?a=one&b=two" data-turbo-method="get"> Submits as a form[method=get] </a> ``` Would be translated to something like: ```html <form method="get" action="/path-with-search-params?a=one&b=two" hidden></form> ``` Then, when submitting the `<form>`, the browser omits the `?one&b=two`. The solution --- This commit extends the current `FormLinkClickObserver` implementation to loop over each entry in [URLSearchParams][] instance derived from the provided [URL][] instance and transforms the name-value pair into an [`<input type="hidden">`][input-hidden] element, which it appends to the `<form>`. Once that translation is complete, it removes any search parameters from the form's `[action]` attribute to avoid duplicate entries in the form's data. While this change resolves the underlying issue, it's worth emphasizing that declaring `[data-turbo-method="get"]` on an `<a>` element is redundant, and should be avoided in the first place. Even though that pattern should be avoided, this commit aims to prevent surprising behavior whenever it occurs. [URLSearchParams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams [URL]: https://developer.mozilla.org/en-US/docs/Web/API/URL [input-hidden]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden
Closes [hotwired#820][] Follow-up to [hotwired#461][] [hotwired#820]: hotwired#820 [hotwired#461]: hotwired#461 The background --- According to the HTML Specification's [§ 4.10.21.3 Form submission algorithm][] section, submissions transmitted as `GET` requests [mutate the `[action]` URL][mutate], overriding any search parameters already encoded into the `[action]` value: > [Mutate action URL][algorithm] > --- > > Let <var>pairs</var> be the result of converting to a list of > name-value pairs with <var>entry list</var>. > > Let <var>query</var> be the result of running the > `application/x-www-form-urlencoded` serializer with <var>pairs</var> > and <var>encoding</var>. > > Set <Var>parsed action</var>'s query component to <var>query</var>. > > Plan to navigate to <var>parsed action</var>. [§ 4.10.21.3 Form submission algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm [algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-mutate-action [mutate]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm:submit-mutate-action Form submissions made with `POST` requests, on the other hand, encode _both_ the `[action]` value's query parameters and any additionally encoded body data: > [Submit as entity body][post-submit] > --- > > … > > Plan to navigate to a new request whose URL is <var>parsed > action</var>, method is <var>method</var>, header list is > « (`Content-Type`, mimeType) », and body is <var>body</var>. [post-submit]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-body The problem --- When navigating through an `<a data-turbo-method="get">` with an `[href]` that contains search parameters, the `<form>` element that Turbo Drive constructs behind the scenes will unexpectedly omit the search parameters during the transformation from `<a>` to `<form>`. For example: ```html <a href="/path-with-search-params?a=one&b=two" data-turbo-method="get"> Submits as a form[method=get] </a> ``` Would be translated to something like: ```html <form method="get" action="/path-with-search-params?a=one&b=two" hidden></form> ``` Then, when submitting the `<form>`, the browser omits the `?one&b=two`. The solution --- This commit extends the current `FormLinkClickObserver` implementation to loop over each entry in [URLSearchParams][] instance derived from the provided [URL][] instance and transforms the name-value pair into an [`<input type="hidden">`][input-hidden] element, which it appends to the `<form>`. Once that translation is complete, it removes any search parameters from the form's `[action]` attribute to avoid duplicate entries in the form's data. While this change resolves the underlying issue, it's worth emphasizing that declaring `[data-turbo-method="get"]` on an `<a>` element is redundant, and should be avoided in the first place. Even though that pattern should be avoided, this commit aims to prevent surprising behavior whenever it occurs. [URLSearchParams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams [URL]: https://developer.mozilla.org/en-US/docs/Web/API/URL [input-hidden]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden
Closes [hotwired#820][] Follow-up to [hotwired#461][] [hotwired#820]: hotwired#820 [hotwired#461]: hotwired#461 The background --- According to the HTML Specification's [§ 4.10.21.3 Form submission algorithm][] section, submissions transmitted as `GET` requests [mutate the `[action]` URL][mutate], overriding any search parameters already encoded into the `[action]` value: > [Mutate action URL][algorithm] > --- > > Let <var>pairs</var> be the result of converting to a list of > name-value pairs with <var>entry list</var>. > > Let <var>query</var> be the result of running the > `application/x-www-form-urlencoded` serializer with <var>pairs</var> > and <var>encoding</var>. > > Set <Var>parsed action</var>'s query component to <var>query</var>. > > Plan to navigate to <var>parsed action</var>. [§ 4.10.21.3 Form submission algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm [algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-mutate-action [mutate]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm:submit-mutate-action Form submissions made with `POST` requests, on the other hand, encode _both_ the `[action]` value's query parameters and any additionally encoded body data: > [Submit as entity body][post-submit] > --- > > … > > Plan to navigate to a new request whose URL is <var>parsed > action</var>, method is <var>method</var>, header list is > « (`Content-Type`, mimeType) », and body is <var>body</var>. [post-submit]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-body The problem --- When navigating through an `<a data-turbo-method="get">` with an `[href]` that contains search parameters, the `<form>` element that Turbo Drive constructs behind the scenes will unexpectedly omit the search parameters during the transformation from `<a>` to `<form>`. For example: ```html <a href="/path-with-search-params?a=one&b=two" data-turbo-method="get"> Submits as a form[method=get] </a> ``` Would be translated to something like: ```html <form method="get" action="/path-with-search-params?a=one&b=two" hidden></form> ``` Then, when submitting the `<form>`, the browser omits the `?one&b=two`. The solution --- This commit extends the current `FormLinkClickObserver` implementation to loop over each entry in [URLSearchParams][] instance derived from the provided [URL][] instance and transforms the name-value pair into an [`<input type="hidden">`][input-hidden] element, which it appends to the `<form>`. Once that translation is complete, it removes any search parameters from the form's `[action]` attribute to avoid duplicate entries in the form's data. While this change resolves the underlying issue, it's worth emphasizing that declaring `[data-turbo-method="get"]` on an `<a>` element is redundant, and should be avoided in the first place. Even though that pattern should be avoided, this commit aims to prevent surprising behavior whenever it occurs. [URLSearchParams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams [URL]: https://developer.mozilla.org/en-US/docs/Web/API/URL [input-hidden]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden
Closes [hotwired#820][] Follow-up to [hotwired#461][] [hotwired#820]: hotwired#820 [hotwired#461]: hotwired#461 The background --- According to the HTML Specification's [§ 4.10.21.3 Form submission algorithm][] section, submissions transmitted as `GET` requests [mutate the `[action]` URL][mutate], overriding any search parameters already encoded into the `[action]` value: > [Mutate action URL][algorithm] > --- > > Let <var>pairs</var> be the result of converting to a list of > name-value pairs with <var>entry list</var>. > > Let <var>query</var> be the result of running the > `application/x-www-form-urlencoded` serializer with <var>pairs</var> > and <var>encoding</var>. > > Set <Var>parsed action</var>'s query component to <var>query</var>. > > Plan to navigate to <var>parsed action</var>. [§ 4.10.21.3 Form submission algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm [algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-mutate-action [mutate]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm:submit-mutate-action Form submissions made with `POST` requests, on the other hand, encode _both_ the `[action]` value's query parameters and any additionally encoded body data: > [Submit as entity body][post-submit] > --- > > … > > Plan to navigate to a new request whose URL is <var>parsed > action</var>, method is <var>method</var>, header list is > « (`Content-Type`, mimeType) », and body is <var>body</var>. [post-submit]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-body The problem --- When navigating through an `<a data-turbo-method="get">` with an `[href]` that contains search parameters, the `<form>` element that Turbo Drive constructs behind the scenes will unexpectedly omit the search parameters during the transformation from `<a>` to `<form>`. For example: ```html <a href="/path-with-search-params?a=one&b=two" data-turbo-method="get"> Submits as a form[method=get] </a> ``` Would be translated to something like: ```html <form method="get" action="/path-with-search-params?a=one&b=two" hidden></form> ``` Then, when submitting the `<form>`, the browser omits the `?one&b=two`. The solution --- This commit extends the current `FormLinkClickObserver` implementation to loop over each entry in [URLSearchParams][] instance derived from the provided [URL][] instance and transforms the name-value pair into an [`<input type="hidden">`][input-hidden] element, which it appends to the `<form>`. Once that translation is complete, it removes any search parameters from the form's `[action]` attribute to avoid duplicate entries in the form's data. While this change resolves the underlying issue, it's worth emphasizing that declaring `[data-turbo-method="get"]` on an `<a>` element is redundant, and should be avoided in the first place. Even though that pattern should be avoided, this commit aims to prevent surprising behavior whenever it occurs. [URLSearchParams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams [URL]: https://developer.mozilla.org/en-US/docs/Web/API/URL [input-hidden]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden
Closes [#820][] Follow-up to [#461][] [#820]: #820 [#461]: #461 The background --- According to the HTML Specification's [§ 4.10.21.3 Form submission algorithm][] section, submissions transmitted as `GET` requests [mutate the `[action]` URL][mutate], overriding any search parameters already encoded into the `[action]` value: > [Mutate action URL][algorithm] > --- > > Let <var>pairs</var> be the result of converting to a list of > name-value pairs with <var>entry list</var>. > > Let <var>query</var> be the result of running the > `application/x-www-form-urlencoded` serializer with <var>pairs</var> > and <var>encoding</var>. > > Set <Var>parsed action</var>'s query component to <var>query</var>. > > Plan to navigate to <var>parsed action</var>. [§ 4.10.21.3 Form submission algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm [algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-mutate-action [mutate]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm:submit-mutate-action Form submissions made with `POST` requests, on the other hand, encode _both_ the `[action]` value's query parameters and any additionally encoded body data: > [Submit as entity body][post-submit] > --- > > … > > Plan to navigate to a new request whose URL is <var>parsed > action</var>, method is <var>method</var>, header list is > « (`Content-Type`, mimeType) », and body is <var>body</var>. [post-submit]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-body The problem --- When navigating through an `<a data-turbo-method="get">` with an `[href]` that contains search parameters, the `<form>` element that Turbo Drive constructs behind the scenes will unexpectedly omit the search parameters during the transformation from `<a>` to `<form>`. For example: ```html <a href="/path-with-search-params?a=one&b=two" data-turbo-method="get"> Submits as a form[method=get] </a> ``` Would be translated to something like: ```html <form method="get" action="/path-with-search-params?a=one&b=two" hidden></form> ``` Then, when submitting the `<form>`, the browser omits the `?one&b=two`. The solution --- This commit extends the current `FormLinkClickObserver` implementation to loop over each entry in [URLSearchParams][] instance derived from the provided [URL][] instance and transforms the name-value pair into an [`<input type="hidden">`][input-hidden] element, which it appends to the `<form>`. Once that translation is complete, it removes any search parameters from the form's `[action]` attribute to avoid duplicate entries in the form's data. While this change resolves the underlying issue, it's worth emphasizing that declaring `[data-turbo-method="get"]` on an `<a>` element is redundant, and should be avoided in the first place. Even though that pattern should be avoided, this commit aims to prevent surprising behavior whenever it occurs. [URLSearchParams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams [URL]: https://developer.mozilla.org/en-US/docs/Web/API/URL [input-hidden]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden
The background
According to the HTML Specification's § 4.10.21.3 Form submission
algorithm section, submissions transmitted as
GET
requests mutatethe
[action]
URL, overriding any search parameters alreadyencoded into the
[action]
value:Form submissions made with
POST
requests, on the other hand, encodeboth the
[action]
value's query parameters and any additionallyencoded body data:
The problem
Ever since 18585fc (and subsequently hotwired/turbo#108),
Turbo's
FetchRequest
has merged submission values from<form>
elements with
[method="get"]
and without[action]
attributes intothe current URL's query parameters.
For example, consider the following forms rendered on the
/articles
page:
Without Turbo, entering "Hotwire" as the search term into
input[name="q"]
and clicking the "Search" button would navigate thebrowser to
/articles?q=Hotwire
. Then, clicking the "Sort ascending"button would navigate the page to
/articles?order=asc
.With Turbo, entering "Hotwire" as the search term into
input[name="q"]
and clicking the "Search" button navigates thebrowser to
/articles?q=Hotwire
. Then, clicking the "Sort ascending"button navigates the page to
/articles?q=Hotwire&order=asc
,effectively merging values from the page's URL and the
<form>
element's fields.
The solution
This commit modifies the way that
FetchRequest
constructs itsurl: URL
andbody: FormData | URLSearchParams
properties.First, it always assigns a
body
property, but conditionallyencodes that value into the
fetchOptions: RequestInit
property basedon whether or not the request is an idempotent
GET
.Next, if constructed with a
body: URLSearchParams
that has entries,replace the
url: URL
instance's search params entirely withthose values, like the HTML Specification algorithm entails.
If constructed with a
body: URLSearchParams
that is empty, pass theurl: URL
through and assign the property without modifying it.Additionally, this commit adds test cases to ensure that
POST
requeststransmit data in both the body and the URL.
While the previous multi-form merging behavior can be desirable, it is
not the behavior outlined by the HTML Specification, so Turbo should not
provide it out-of-the-box.
Having said that, there are two ways for applications to restore that
behavior:
into the
FetchRequest
instance'sbody
:submitted form's FormData instance prior to entering the Turbo
request pipeline:
The conditionals in both cases could be omitted if applications controlled the behavior more directly, like by declaring a Stimulus controller and action (e.g.
<form data-controller="form" data-action="formdata->form#mergeSearchParams">
).Supporting changes
Dispatch
turbo:before-fetch-request
with a direct reference to theFetchRequest
instance'surl: URL
property as part of theevent.detail
.