Skip to content
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

Merged
merged 1 commit into from
Jan 29, 2021

Conversation

seanpdoyle
Copy link
Contributor

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 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

currentUrl.searchParams.delete(name)
url.searchParams.set(name, value.toString())
} else {
url.searchParams.append(name, value.toString())
Copy link
Contributor Author

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?

Copy link
Contributor Author

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)?

Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Contributor Author

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?

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
@sstephenson sstephenson merged commit 898423b into hotwired:main Jan 29, 2021
@seanpdoyle seanpdoyle deleted the form-get-submit branch January 29, 2021 16:31
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Nov 22, 2021
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
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Nov 22, 2021
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
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Nov 22, 2021
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
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Nov 22, 2021
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
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Nov 22, 2021
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
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Nov 22, 2021
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
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Nov 22, 2021
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
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Nov 22, 2021
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
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Nov 22, 2021
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
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Nov 22, 2021
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
dhh pushed a commit that referenced this pull request Nov 22, 2021
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

Drive: GET form (e.g. search form) with NO action="" adds duplicate ?q=foo&q=foo&q=foo... to URL
2 participants