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 Submissions: don't merge into searchParams #461

Merged
merged 2 commits into from
Nov 22, 2021

Conversation

seanpdoyle
Copy link
Contributor

@seanpdoyle seanpdoyle commented 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
, overriding any search parameters already
encoded into the [action] value:

Mutate action URL

Let pairs be the result of converting to a list of
name-value pairs with entry list.

Let query be the result of running the
application/x-www-form-urlencoded serializer with pairs
and encoding.

Set parsed action's query component to query.

Plan to navigate to parsed 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

Plan to navigate to a new request whose URL is parsed
action
, method is method, header list is
« (Content-Type, mimeType) », and body is body.

The problem

Ever since 18585fc (and subsequently hotwired/turbo#108),
Turbo's FetchRequest has merged submission values from <form>
elements with [method="get"] and without [action] attributes into
the current URL's query parameters.

For example, consider the following forms rendered on the /articles
page:

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

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

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:
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)
    }
  }
})
  1. declare a formdata event listener to merge values into the
    submitted form's FormData instance prior to entering the Turbo
    request pipeline:
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">).

Supporting changes

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

@seanpdoyle seanpdoyle force-pushed the form-get-query-param-merging branch 3 times, most recently from 7f5936d to cc03835 Compare November 22, 2021 03:55
Dispatch `turbo:before-fetch-request` with a direct reference to the
`FetchRequest` instance's `url: URL` property as part of the
`event.detail`.
The background
---

According to the HTML Specification's [§ 4.10.21.3 Form submission
algorithm][] section, submissions transmitted as `GET` requests [mutate
the `[action]` URL][mutate], overriding any search parameters already
encoded into the `[action]` value:

> [Mutate action URL][algorithm]
> ---
>
> Let <var>pairs</var> be the result of converting to a list of
> name-value pairs with <var>entry list</var>.
>
> Let <var>query</var> be the result of running the
> `application/x-www-form-urlencoded` serializer with <var>pairs</var>
> and <var>encoding</var>.
>
> Set <Var>parsed action</var>'s query component to <var>query</var>.
>
> Plan to navigate to <var>parsed action</var>.

[§ 4.10.21.3 Form submission algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
[algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-mutate-action
[mutate]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm:submit-mutate-action

Form submissions made with `POST` requests, on the other hand, encode
_both_ the `[action]` value's query parameters and any additionally
encoded body data:

> [Submit as entity body][post-submit]
> ---
>
> …
>
> Plan to navigate to a new request whose URL is <var>parsed
> action</var>, method is <var>method</var>, header list is
> « (`Content-Type`, mimeType) », and body is <var>body</var>.

[post-submit]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-body

The problem
---

Ever since [18585fc][] (and subsequently [hotwired#108][]),
Turbo's `FetchRequest` has merged submission values from `<form>`
elements with `[method="get"]` and without `[action]` attributes _into_
the current URL's query parameters.

For example, consider the following forms rendered on the `/articles`
page:

```html
<form>
  <label for="q">Term</label>
  <input id="q" name="q">
  <button>Search</button>
</form>

<!-- elsewhere in the page -->
<form>
  <button name="order" value="asc">Sort ascending</button>
  <button name="order" value="desc">Sort ascending</button>
</form>
```

Without Turbo, entering "Hotwire" as the search term into
`input[name="q"]` and clicking the "Search" button would navigate the
browser to `/articles?q=Hotwire`. Then, clicking the "Sort ascending"
button would navigate the page to `/articles?order=asc`.

Without Turbo, entering "Hotwire" as the search term into
`input[name="q"]` and clicking the "Search" button navigates the
browser to `/articles?q=Hotwire`. Then, clicking the "Sort ascending"
button **navigates the page to `/articles?q=Hotwire&order=asc`**,
effectively _merging_ values from the page's URL and the `<form>`
element's fields.

[18585fc]: hotwired@18585fc#diff-68b647dc2963716dc27c070f261d0b942ee9c00be7c4149ecb3a5acd94842d40R135-R142
[hotwired#108]: hotwired#108

The solution
---

This commit modifies the way that `FetchRequest` constructs its `url:
URL` and `body: FormData | URLSearchParams` properties.

First, it _always_ assigns a `body` property, but _conditionally_
encodes that value into the `fetchOptions: RequestInit` property based
on whether or not the request is an idempotent `GET`.

Next, if constructed with a `body: URLSearchParams` that has entries,
**replace** the `url: URL` instance's search params _entirely_ with
those values, like the HTML Specification algorithm entails.

If constructed with a `body: URLSearchParams` that is empty, pass the
`url: URL` through and assign the property **without modifying it**.

Additionally, this commit adds test cases to ensure that `POST` requests
transmit data in both the body and the URL.

While the previous multi-form merging behavior can be desirable, it is
not the behavior outlined by the HTML Specification, so Turbo should not
provide it out-of-the-box.

Having said that, there are two ways for applications to restore that
behavior:

1. declare a [turbo:before-fetch-request][] event listener to merge
   values _into_ the event's `detail.url` instance:

```js
addEventListener("turbo:before-fetch-request", ({ target, detail: { url, fetchOptions: { method } } }) => {
  if (target instanceof HTMLFormElement && method == "GET") {
    for (const [ name, value ] of new URLSearchParams(window.location.search)) {
      // conditionally call `set` or `append`,
      // depending on your application's needs
      if (url.searchParams.has(name)) continue
      else url.searchParams.set(name, value)
    }
  }
})
```

2. declare a [formdata][] event listener to merge values _into_ the
   submitted form's [FormData][] instance prior to entering the Turbo
   request pipeline:

```js
addEventListener("submit", (event) => {
  if (event.defaultPrevented) return

  const { target, submitter } = event
  const action = submitter?.getAttribute("formaction") || target.getAttribute("action")

  if (target.method == "get" && !action) {
    target.addEventListener("formdata", ({ formData }) => {
      for (const [ name, value ] of new URLSearchParams(window.location.search)) {
        // conditionally call `set` or `append`,
        // depending on your application's needs
        if (formData.has(name)) continue
        else formData.set(name, value)
      }
    }, { once: true })
  }
})
```

The conditionals in both cases could be omitted if applications
controlled the behavior more directly, like by declaring a Stimulus
controller and action (e.g. `<form data-controller="form"
data-action="formdata->form#mergeSearchParams">`).

[turbo:before-fetch-request]: https://turbo.hotwired.dev/reference/events
[formdata]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/formdata_event
[FormData]: https://developer.mozilla.org/en-US/docs/Web/API/FormData
@seanpdoyle seanpdoyle force-pushed the form-get-query-param-merging branch from cc03835 to 6906e57 Compare November 22, 2021 04:10
@dhh dhh merged commit 8fc8bca into hotwired:main Nov 22, 2021
@seanpdoyle seanpdoyle deleted the form-get-query-param-merging branch November 22, 2021 12:32
<button id="form-action-none-q-a" name="q" value="a">Submit ?q=a to form:not([action])</button>
</form>
<form action="/src/tests/fixtures/form.html?sort=asc">
<button id="form-action-self-submit">Submit form[action]</button>
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've just discovered that this is still slightly incorrect behavior.

I tested with an application without Turbo or any other JavaScript, and submitting a form with an [action] value that contains query parameters requests that [action] path without any key-value pairs (that is, with ? as its the value of URL.search):

<form action="/src/tests/fixtures/form.html?sort=asc">
  <!-- clicking the button navigates to `/src/tests/fixtures/form?` -->
  <button>Submit</button>
</form>

I'm in the process of adding test coverage and opening a separate PR.

seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Nov 22, 2021
Follow-up to [hotwired#461][]

While the changes made in hotwired#461 added coverage for `<form method="post"
action="...">` and `<form>` submissions, it didn't cover `<form
action="/?encoded=value">` submissions with search parameters encoded
into the `[action]` attribute.

Without Turbo or any other JavaScript, submitting a form with an
`[action]` value that contains query parameters requests that `[action]`
path without any key-value pairs (that is, with `?` as its the value of
`URL.search`):

```html
<form action="/src/tests/fixtures/form.html?sort=asc">
  <!-- clicking the button navigates to `/src/tests/fixtures/form?` -->
  <button>Submit</button>
</form>
```

This commit modifies the `FetchRequest` to always invoke the
module-private `mergeFormDataEntries()` function when constructing `GET`
requests, and modifies the `mergeFormDataEntries()` to completely
discard the `[action]` URL's search parameters before merging-in the
`FormData` key-value pairs.

[hotwired#461]: hotwired#461
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Nov 22, 2021
Follow-up to [hotwired#461][]

While the changes made in hotwired#461 added coverage for `<form method="post"
action="...">` and `<form>` submissions, it didn't cover `<form
action="/?encoded=value">` submissions with search parameters encoded
into the `[action]` attribute.

Without Turbo or any other JavaScript, submitting a form with an
`[action]` value that contains query parameters requests that `[action]`
path without any key-value pairs (that is, with `?` as its the value of
`URL.search`):

```html
<form action="/src/tests/fixtures/form.html?sort=asc">
  <!-- clicking the button navigates to `/src/tests/fixtures/form?` -->
  <button>Submit</button>
</form>
```

This commit modifies the `FetchRequest` to always invoke the
module-private `mergeFormDataEntries()` function when constructing `GET`
requests, and modifies the `mergeFormDataEntries()` to completely
discard the `[action]` URL's search parameters before merging-in the
`FormData` key-value pairs.

[hotwired#461]: hotwired#461
dhh pushed a commit that referenced this pull request Nov 22, 2021
Follow-up to [#461][]

While the changes made in #461 added coverage for `<form method="post"
action="...">` and `<form>` submissions, it didn't cover `<form
action="/?encoded=value">` submissions with search parameters encoded
into the `[action]` attribute.

Without Turbo or any other JavaScript, submitting a form with an
`[action]` value that contains query parameters requests that `[action]`
path without any key-value pairs (that is, with `?` as its the value of
`URL.search`):

```html
<form action="/src/tests/fixtures/form.html?sort=asc">
  <!-- clicking the button navigates to `/src/tests/fixtures/form?` -->
  <button>Submit</button>
</form>
```

This commit modifies the `FetchRequest` to always invoke the
module-private `mergeFormDataEntries()` function when constructing `GET`
requests, and modifies the `mergeFormDataEntries()` to completely
discard the `[action]` URL's search parameters before merging-in the
`FormData` key-value pairs.

[#461]: #461
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Dec 15, 2022
Closes [hotwired#820][]
Follow-up to [hotwired#461][]

[hotwired#820]: hotwired#820
[hotwired#461]: hotwired#461

The background
---

According to the HTML Specification's [§ 4.10.21.3 Form submission
algorithm][] section, submissions transmitted as `GET` requests [mutate
the `[action]` URL][mutate], overriding any search parameters already
encoded into the `[action]` value:

> [Mutate action URL][algorithm]
> ---
>
> Let <var>pairs</var> be the result of converting to a list of
> name-value pairs with <var>entry list</var>.
>
> Let <var>query</var> be the result of running the
> `application/x-www-form-urlencoded` serializer with <var>pairs</var>
> and <var>encoding</var>.
>
> Set <Var>parsed action</var>'s query component to <var>query</var>.
>
> Plan to navigate to <var>parsed action</var>.

[§ 4.10.21.3 Form submission algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
[algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-mutate-action
[mutate]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm:submit-mutate-action

Form submissions made with `POST` requests, on the other hand, encode
_both_ the `[action]` value's query parameters and any additionally
encoded body data:

> [Submit as entity body][post-submit]
> ---
>
> …
>
> Plan to navigate to a new request whose URL is <var>parsed
> action</var>, method is <var>method</var>, header list is
> « (`Content-Type`, mimeType) », and body is <var>body</var>.

[post-submit]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-body

The problem
---

When navigating through an `<a data-turbo-method="get">` with an
`[href]` that contains search parameters, the `<form>` element that
Turbo Drive constructs behind the scenes will unexpectedly omit the
search parameters during the transformation from `<a>` to `<form>`.

For example:

```html
<a href="/path-with-search-params?a=one&b=two"
   data-turbo-method="get">
  Submits as a form[method=get]
</a>
```

Would be translated to something like:

```html
<form method="get" action="/path-with-search-params?a=one&b=two" hidden></form>
```

Then, when submitting the `<form>`, the browser omits the `?one&b=two`.

The solution
---

This commit extends the current `FormLinkClickObserver` implementation
to loop over each entry in [URLSearchParams][] instance derived from the
provided [URL][] instance and transforms the name-value pair into an
[`<input type="hidden">`][input-hidden] element, which it appends to the
`<form>`.

Once that translation is complete, it removes any search parameters from
the form's `[action]` attribute to avoid duplicate entries in the form's
data.

While this change resolves the underlying issue, it's worth emphasizing
that declaring `[data-turbo-method="get"]` on an `<a>` element is
redundant, and should be avoided in the first place. Even though that
pattern should be avoided, this commit aims to prevent surprising
behavior whenever it occurs.

[URLSearchParams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
[URL]: https://developer.mozilla.org/en-US/docs/Web/API/URL
[input-hidden]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Dec 23, 2022
Closes [hotwired#820][]
Follow-up to [hotwired#461][]

[hotwired#820]: hotwired#820
[hotwired#461]: hotwired#461

The background
---

According to the HTML Specification's [§ 4.10.21.3 Form submission
algorithm][] section, submissions transmitted as `GET` requests [mutate
the `[action]` URL][mutate], overriding any search parameters already
encoded into the `[action]` value:

> [Mutate action URL][algorithm]
> ---
>
> Let <var>pairs</var> be the result of converting to a list of
> name-value pairs with <var>entry list</var>.
>
> Let <var>query</var> be the result of running the
> `application/x-www-form-urlencoded` serializer with <var>pairs</var>
> and <var>encoding</var>.
>
> Set <Var>parsed action</var>'s query component to <var>query</var>.
>
> Plan to navigate to <var>parsed action</var>.

[§ 4.10.21.3 Form submission algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
[algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-mutate-action
[mutate]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm:submit-mutate-action

Form submissions made with `POST` requests, on the other hand, encode
_both_ the `[action]` value's query parameters and any additionally
encoded body data:

> [Submit as entity body][post-submit]
> ---
>
> …
>
> Plan to navigate to a new request whose URL is <var>parsed
> action</var>, method is <var>method</var>, header list is
> « (`Content-Type`, mimeType) », and body is <var>body</var>.

[post-submit]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-body

The problem
---

When navigating through an `<a data-turbo-method="get">` with an
`[href]` that contains search parameters, the `<form>` element that
Turbo Drive constructs behind the scenes will unexpectedly omit the
search parameters during the transformation from `<a>` to `<form>`.

For example:

```html
<a href="/path-with-search-params?a=one&b=two"
   data-turbo-method="get">
  Submits as a form[method=get]
</a>
```

Would be translated to something like:

```html
<form method="get" action="/path-with-search-params?a=one&b=two" hidden></form>
```

Then, when submitting the `<form>`, the browser omits the `?one&b=two`.

The solution
---

This commit extends the current `FormLinkClickObserver` implementation
to loop over each entry in [URLSearchParams][] instance derived from the
provided [URL][] instance and transforms the name-value pair into an
[`<input type="hidden">`][input-hidden] element, which it appends to the
`<form>`.

Once that translation is complete, it removes any search parameters from
the form's `[action]` attribute to avoid duplicate entries in the form's
data.

While this change resolves the underlying issue, it's worth emphasizing
that declaring `[data-turbo-method="get"]` on an `<a>` element is
redundant, and should be avoided in the first place. Even though that
pattern should be avoided, this commit aims to prevent surprising
behavior whenever it occurs.

[URLSearchParams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
[URL]: https://developer.mozilla.org/en-US/docs/Web/API/URL
[input-hidden]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Dec 23, 2022
Closes [hotwired#820][]
Follow-up to [hotwired#461][]

[hotwired#820]: hotwired#820
[hotwired#461]: hotwired#461

The background
---

According to the HTML Specification's [§ 4.10.21.3 Form submission
algorithm][] section, submissions transmitted as `GET` requests [mutate
the `[action]` URL][mutate], overriding any search parameters already
encoded into the `[action]` value:

> [Mutate action URL][algorithm]
> ---
>
> Let <var>pairs</var> be the result of converting to a list of
> name-value pairs with <var>entry list</var>.
>
> Let <var>query</var> be the result of running the
> `application/x-www-form-urlencoded` serializer with <var>pairs</var>
> and <var>encoding</var>.
>
> Set <Var>parsed action</var>'s query component to <var>query</var>.
>
> Plan to navigate to <var>parsed action</var>.

[§ 4.10.21.3 Form submission algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
[algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-mutate-action
[mutate]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm:submit-mutate-action

Form submissions made with `POST` requests, on the other hand, encode
_both_ the `[action]` value's query parameters and any additionally
encoded body data:

> [Submit as entity body][post-submit]
> ---
>
> …
>
> Plan to navigate to a new request whose URL is <var>parsed
> action</var>, method is <var>method</var>, header list is
> « (`Content-Type`, mimeType) », and body is <var>body</var>.

[post-submit]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-body

The problem
---

When navigating through an `<a data-turbo-method="get">` with an
`[href]` that contains search parameters, the `<form>` element that
Turbo Drive constructs behind the scenes will unexpectedly omit the
search parameters during the transformation from `<a>` to `<form>`.

For example:

```html
<a href="/path-with-search-params?a=one&b=two"
   data-turbo-method="get">
  Submits as a form[method=get]
</a>
```

Would be translated to something like:

```html
<form method="get" action="/path-with-search-params?a=one&b=two" hidden></form>
```

Then, when submitting the `<form>`, the browser omits the `?one&b=two`.

The solution
---

This commit extends the current `FormLinkClickObserver` implementation
to loop over each entry in [URLSearchParams][] instance derived from the
provided [URL][] instance and transforms the name-value pair into an
[`<input type="hidden">`][input-hidden] element, which it appends to the
`<form>`.

Once that translation is complete, it removes any search parameters from
the form's `[action]` attribute to avoid duplicate entries in the form's
data.

While this change resolves the underlying issue, it's worth emphasizing
that declaring `[data-turbo-method="get"]` on an `<a>` element is
redundant, and should be avoided in the first place. Even though that
pattern should be avoided, this commit aims to prevent surprising
behavior whenever it occurs.

[URLSearchParams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
[URL]: https://developer.mozilla.org/en-US/docs/Web/API/URL
[input-hidden]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Dec 30, 2022
Closes [hotwired#820][]
Follow-up to [hotwired#461][]

[hotwired#820]: hotwired#820
[hotwired#461]: hotwired#461

The background
---

According to the HTML Specification's [§ 4.10.21.3 Form submission
algorithm][] section, submissions transmitted as `GET` requests [mutate
the `[action]` URL][mutate], overriding any search parameters already
encoded into the `[action]` value:

> [Mutate action URL][algorithm]
> ---
>
> Let <var>pairs</var> be the result of converting to a list of
> name-value pairs with <var>entry list</var>.
>
> Let <var>query</var> be the result of running the
> `application/x-www-form-urlencoded` serializer with <var>pairs</var>
> and <var>encoding</var>.
>
> Set <Var>parsed action</var>'s query component to <var>query</var>.
>
> Plan to navigate to <var>parsed action</var>.

[§ 4.10.21.3 Form submission algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
[algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-mutate-action
[mutate]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm:submit-mutate-action

Form submissions made with `POST` requests, on the other hand, encode
_both_ the `[action]` value's query parameters and any additionally
encoded body data:

> [Submit as entity body][post-submit]
> ---
>
> …
>
> Plan to navigate to a new request whose URL is <var>parsed
> action</var>, method is <var>method</var>, header list is
> « (`Content-Type`, mimeType) », and body is <var>body</var>.

[post-submit]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-body

The problem
---

When navigating through an `<a data-turbo-method="get">` with an
`[href]` that contains search parameters, the `<form>` element that
Turbo Drive constructs behind the scenes will unexpectedly omit the
search parameters during the transformation from `<a>` to `<form>`.

For example:

```html
<a href="/path-with-search-params?a=one&b=two"
   data-turbo-method="get">
  Submits as a form[method=get]
</a>
```

Would be translated to something like:

```html
<form method="get" action="/path-with-search-params?a=one&b=two" hidden></form>
```

Then, when submitting the `<form>`, the browser omits the `?one&b=two`.

The solution
---

This commit extends the current `FormLinkClickObserver` implementation
to loop over each entry in [URLSearchParams][] instance derived from the
provided [URL][] instance and transforms the name-value pair into an
[`<input type="hidden">`][input-hidden] element, which it appends to the
`<form>`.

Once that translation is complete, it removes any search parameters from
the form's `[action]` attribute to avoid duplicate entries in the form's
data.

While this change resolves the underlying issue, it's worth emphasizing
that declaring `[data-turbo-method="get"]` on an `<a>` element is
redundant, and should be avoided in the first place. Even though that
pattern should be avoided, this commit aims to prevent surprising
behavior whenever it occurs.

[URLSearchParams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
[URL]: https://developer.mozilla.org/en-US/docs/Web/API/URL
[input-hidden]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Dec 30, 2022
Closes [hotwired#820][]
Follow-up to [hotwired#461][]

[hotwired#820]: hotwired#820
[hotwired#461]: hotwired#461

The background
---

According to the HTML Specification's [§ 4.10.21.3 Form submission
algorithm][] section, submissions transmitted as `GET` requests [mutate
the `[action]` URL][mutate], overriding any search parameters already
encoded into the `[action]` value:

> [Mutate action URL][algorithm]
> ---
>
> Let <var>pairs</var> be the result of converting to a list of
> name-value pairs with <var>entry list</var>.
>
> Let <var>query</var> be the result of running the
> `application/x-www-form-urlencoded` serializer with <var>pairs</var>
> and <var>encoding</var>.
>
> Set <Var>parsed action</var>'s query component to <var>query</var>.
>
> Plan to navigate to <var>parsed action</var>.

[§ 4.10.21.3 Form submission algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
[algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-mutate-action
[mutate]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm:submit-mutate-action

Form submissions made with `POST` requests, on the other hand, encode
_both_ the `[action]` value's query parameters and any additionally
encoded body data:

> [Submit as entity body][post-submit]
> ---
>
> …
>
> Plan to navigate to a new request whose URL is <var>parsed
> action</var>, method is <var>method</var>, header list is
> « (`Content-Type`, mimeType) », and body is <var>body</var>.

[post-submit]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-body

The problem
---

When navigating through an `<a data-turbo-method="get">` with an
`[href]` that contains search parameters, the `<form>` element that
Turbo Drive constructs behind the scenes will unexpectedly omit the
search parameters during the transformation from `<a>` to `<form>`.

For example:

```html
<a href="/path-with-search-params?a=one&b=two"
   data-turbo-method="get">
  Submits as a form[method=get]
</a>
```

Would be translated to something like:

```html
<form method="get" action="/path-with-search-params?a=one&b=two" hidden></form>
```

Then, when submitting the `<form>`, the browser omits the `?one&b=two`.

The solution
---

This commit extends the current `FormLinkClickObserver` implementation
to loop over each entry in [URLSearchParams][] instance derived from the
provided [URL][] instance and transforms the name-value pair into an
[`<input type="hidden">`][input-hidden] element, which it appends to the
`<form>`.

Once that translation is complete, it removes any search parameters from
the form's `[action]` attribute to avoid duplicate entries in the form's
data.

While this change resolves the underlying issue, it's worth emphasizing
that declaring `[data-turbo-method="get"]` on an `<a>` element is
redundant, and should be avoided in the first place. Even though that
pattern should be avoided, this commit aims to prevent surprising
behavior whenever it occurs.

[URLSearchParams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
[URL]: https://developer.mozilla.org/en-US/docs/Web/API/URL
[input-hidden]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Dec 30, 2022
Closes [hotwired#820][]
Follow-up to [hotwired#461][]

[hotwired#820]: hotwired#820
[hotwired#461]: hotwired#461

The background
---

According to the HTML Specification's [§ 4.10.21.3 Form submission
algorithm][] section, submissions transmitted as `GET` requests [mutate
the `[action]` URL][mutate], overriding any search parameters already
encoded into the `[action]` value:

> [Mutate action URL][algorithm]
> ---
>
> Let <var>pairs</var> be the result of converting to a list of
> name-value pairs with <var>entry list</var>.
>
> Let <var>query</var> be the result of running the
> `application/x-www-form-urlencoded` serializer with <var>pairs</var>
> and <var>encoding</var>.
>
> Set <Var>parsed action</var>'s query component to <var>query</var>.
>
> Plan to navigate to <var>parsed action</var>.

[§ 4.10.21.3 Form submission algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
[algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-mutate-action
[mutate]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm:submit-mutate-action

Form submissions made with `POST` requests, on the other hand, encode
_both_ the `[action]` value's query parameters and any additionally
encoded body data:

> [Submit as entity body][post-submit]
> ---
>
> …
>
> Plan to navigate to a new request whose URL is <var>parsed
> action</var>, method is <var>method</var>, header list is
> « (`Content-Type`, mimeType) », and body is <var>body</var>.

[post-submit]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-body

The problem
---

When navigating through an `<a data-turbo-method="get">` with an
`[href]` that contains search parameters, the `<form>` element that
Turbo Drive constructs behind the scenes will unexpectedly omit the
search parameters during the transformation from `<a>` to `<form>`.

For example:

```html
<a href="/path-with-search-params?a=one&b=two"
   data-turbo-method="get">
  Submits as a form[method=get]
</a>
```

Would be translated to something like:

```html
<form method="get" action="/path-with-search-params?a=one&b=two" hidden></form>
```

Then, when submitting the `<form>`, the browser omits the `?one&b=two`.

The solution
---

This commit extends the current `FormLinkClickObserver` implementation
to loop over each entry in [URLSearchParams][] instance derived from the
provided [URL][] instance and transforms the name-value pair into an
[`<input type="hidden">`][input-hidden] element, which it appends to the
`<form>`.

Once that translation is complete, it removes any search parameters from
the form's `[action]` attribute to avoid duplicate entries in the form's
data.

While this change resolves the underlying issue, it's worth emphasizing
that declaring `[data-turbo-method="get"]` on an `<a>` element is
redundant, and should be avoided in the first place. Even though that
pattern should be avoided, this commit aims to prevent surprising
behavior whenever it occurs.

[URLSearchParams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
[URL]: https://developer.mozilla.org/en-US/docs/Web/API/URL
[input-hidden]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden
dhh pushed a commit that referenced this pull request Dec 31, 2022
Closes [#820][]
Follow-up to [#461][]

[#820]: #820
[#461]: #461

The background
---

According to the HTML Specification's [§ 4.10.21.3 Form submission
algorithm][] section, submissions transmitted as `GET` requests [mutate
the `[action]` URL][mutate], overriding any search parameters already
encoded into the `[action]` value:

> [Mutate action URL][algorithm]
> ---
>
> Let <var>pairs</var> be the result of converting to a list of
> name-value pairs with <var>entry list</var>.
>
> Let <var>query</var> be the result of running the
> `application/x-www-form-urlencoded` serializer with <var>pairs</var>
> and <var>encoding</var>.
>
> Set <Var>parsed action</var>'s query component to <var>query</var>.
>
> Plan to navigate to <var>parsed action</var>.

[§ 4.10.21.3 Form submission algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
[algorithm]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-mutate-action
[mutate]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm:submit-mutate-action

Form submissions made with `POST` requests, on the other hand, encode
_both_ the `[action]` value's query parameters and any additionally
encoded body data:

> [Submit as entity body][post-submit]
> ---
>
> …
>
> Plan to navigate to a new request whose URL is <var>parsed
> action</var>, method is <var>method</var>, header list is
> « (`Content-Type`, mimeType) », and body is <var>body</var>.

[post-submit]: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#submit-body

The problem
---

When navigating through an `<a data-turbo-method="get">` with an
`[href]` that contains search parameters, the `<form>` element that
Turbo Drive constructs behind the scenes will unexpectedly omit the
search parameters during the transformation from `<a>` to `<form>`.

For example:

```html
<a href="/path-with-search-params?a=one&b=two"
   data-turbo-method="get">
  Submits as a form[method=get]
</a>
```

Would be translated to something like:

```html
<form method="get" action="/path-with-search-params?a=one&b=two" hidden></form>
```

Then, when submitting the `<form>`, the browser omits the `?one&b=two`.

The solution
---

This commit extends the current `FormLinkClickObserver` implementation
to loop over each entry in [URLSearchParams][] instance derived from the
provided [URL][] instance and transforms the name-value pair into an
[`<input type="hidden">`][input-hidden] element, which it appends to the
`<form>`.

Once that translation is complete, it removes any search parameters from
the form's `[action]` attribute to avoid duplicate entries in the form's
data.

While this change resolves the underlying issue, it's worth emphasizing
that declaring `[data-turbo-method="get"]` on an `<a>` element is
redundant, and should be avoided in the first place. Even though that
pattern should be avoided, this commit aims to prevent surprising
behavior whenever it occurs.

[URLSearchParams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
[URL]: https://developer.mozilla.org/en-US/docs/Web/API/URL
[input-hidden]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/hidden
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.

2 participants