-
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 form submissions to the current path #108
Conversation
ac4b0f9
to
124ff7d
Compare
124ff7d
to
9554c7c
Compare
src/http/fetch_request.ts
Outdated
currentUrl.searchParams.delete(name) | ||
url.searchParams.set(name, value.toString()) | ||
} else { | ||
url.searchParams.append(name, value.toString()) |
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.
@sstephenson when value
is a File, will coercing to a String break uploads?
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.
Would we be better off guarding these calls with !(value instanceof File)
?
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.
What happens when you submit an <input type="file">
in a <form method="get">
without Turbo? The browser just skips the field, I think?
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 don't think there's anything in FetchRequest's current form that restricts this to idempotent method="get"
requests. Am I missing something?
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.
For what it's worth, this loop is to mutate the URL it's submitted to, and not necessarily the body we're submitting. Would omitting Files entirely avoid the problem of turning a file into a query parameter?
9554c7c
to
8c6274d
Compare
Closes hotwired#107 The [HTML specification][] outlines the algorithm for handling form submissions, and describes the steps involved in handling a `method=get` request with `application/x-www-form-urlencoded` encoding. However, it doesn't explicitly mention how to handle a `<form method="get">` submission when the `action` attribute is omitted and the current URL includes a query parameter that would be included within the `<form>` element fields' values. Determining the test cases to reproduce browser default behavior involved was some trial and error troubleshooting in real browsers with default (JavaScript-less) `<form>` submission handling. The algorithm appears to be as follows: * If there are no query parameters, append all field values as query parameters * If there are existing query parameters whose keys exist as `<form>` fields, override them based on the form field's value * If there are existing query parameters whose keys exist as _multiple_ `<form>` fields, override the first occurrence, and then append subsequent values [HTML specification]: https://html.spec.whatwg.org/#form-submission-algorithm
8c6274d
to
9efa22d
Compare
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][post-submit], on the other hand, encode _both_ the `[action]` value's query parameters and any additionally encoded body data: > Submit as entity body > --- > > … > > 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 `FetchRequest` instance's `body`: ```js addEventListener("turbo:before-fetch-request", ({ target, detail: { fetchOptions } }) => { if (target instanceof HTMLFormElement && target.action == "get") { const searchParams = new URLSearchParams(window.location.search) for (const [ name, value ] of searchParams) { // call `set` or `append`, depending on your application's needs fetchOptions.body.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("formdata", ({ target, formData }) => { if (target.action == "get") { const searchParams = new URLSearchParams(window.location.search) for (const [ name, value ] of searchParams) { // call `set` or `append`, depending on your application's needs formData.set(name, value) } } }) ``` [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
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 `FetchRequest` instance's `body`: ```js addEventListener("turbo:before-fetch-request", ({ target, detail: { fetchOptions } }) => { if (target instanceof HTMLFormElement && target.action == "get") { const searchParams = new URLSearchParams(window.location.search) for (const [ name, value ] of searchParams) { // call `set` or `append`, depending on your application's needs fetchOptions.body.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("formdata", ({ target, formData }) => { if (target.action == "get") { const searchParams = new URLSearchParams(window.location.search) for (const [ name, value ] of searchParams) { // call `set` or `append`, depending on your application's needs formData.set(name, value) } } }) ``` [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
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 `FetchRequest` instance's `body`: ```js addEventListener("turbo:before-fetch-request", ({ target, detail: { fetchOptions } }) => { if (target instanceof HTMLFormElement && target.action == "get") { const searchParams = new URLSearchParams(window.location.search) for (const [ name, value ] of searchParams) { // call `set` or `append`, depending on your application's needs fetchOptions.body.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("formdata", ({ target, formData }) => { if (target.action == "get") { const searchParams = new URLSearchParams(window.location.search) for (const [ name, value ] of searchParams) { // call `set` or `append`, depending on your application's needs formData.set(name, value) } } }) ``` [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
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 `FetchRequest` instance's `body`: ```js addEventListener("turbo:before-fetch-request", ({ target, detail: { fetchOptions } }) => { if (target instanceof HTMLFormElement && target.action == "get") { // call `set` or `append`, depending on your application's needs for (const [ name, value ] of new URLSearchParams(window.location.search)) { fetchOptions.body.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("formdata", ({ target, formData }) => { if (target.action == "get") { // call `set` or `append`, depending on your application's needs for (const [ name, value ] of new URLSearchParams(window.location.search)) { formData.set(name, value) } } }) ``` [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
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 `FetchRequest` instance's `body`: ```js addEventListener("turbo:before-fetch-request", ({ target, detail: { fetchOptions } }) => { if (target instanceof HTMLFormElement && target.method == "get") { // call `set` or `append`, depending on your application's needs for (const [ name, value ] of new URLSearchParams(window.location.search)) { fetchOptions.body.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("formdata", ({ target, formData }) => { if (target.method == "get") { // call `set` or `append`, depending on your application's needs for (const [ name, value ] of new URLSearchParams(window.location.search)) { formData.set(name, value) } } }) ``` [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
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 `FetchRequest` instance's `body`: ```js addEventListener("turbo:before-fetch-request", ({ target, detail: { fetchOptions } }) => { if (target instanceof HTMLFormElement && target.method == "get") { // call `set` or `append`, depending on your application's needs for (const [ name, value ] of new URLSearchParams(window.location.search)) { // conditionally call `set` or `append`, // depending on your application's needs if (fetchOptions.body.has(name)) continue else fetchOptions.body.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 }) } }) ``` [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
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
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
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
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
* Dispatch `turbo:before-fetch-request` with URL Dispatch `turbo:before-fetch-request` with a direct reference to the `FetchRequest` instance's `url: URL` property as part of the `event.detail`. * `GET` Submissions: don't merge into `searchParams` 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 [#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]: 18585fc#diff-68b647dc2963716dc27c070f261d0b942ee9c00be7c4149ecb3a5acd94842d40R135-R142 [#108]: #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
Closes #107
The HTML specification outlines the algorithm for handling form
submissions, and describes the steps involved in handling a
method=get
request with
application/x-www-form-urlencoded
encoding.However, it doesn't explicitly mention how to handle a
<form method="get">
submission when theaction
attribute is omitted and thecurrent URL includes a query parameter that would be included within the
<form>
element fields' values.Determining the test cases to reproduce browser default behavior
involved was some trial and error troubleshooting in real browsers with
default (JavaScript-less)
<form>
submission handling.The algorithm appears to be as follows:
If there are no query parameters, append all field values as query
parameters
If there are existing query parameters whose keys exist as
<form>
fields, override them based on the form field's value
multiple
<form>
fields, override the first occurrence, and thenappend subsequent values