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

Ignore UJS <a> clicks and <form> submissions #744

Merged
merged 4 commits into from
Oct 8, 2022

Conversation

seanpdoyle
Copy link
Contributor

@seanpdoyle seanpdoyle commented Sep 28, 2022

Closes #743

The change in behavior can be traced back to hotwired/turbo#412.
When the overlap between the LinkInterceptor and the
LinkClickObserver were unified, the technique used by the
LinkInterceptor to prevent duplicate event handlers from responding to
the same click event was changed in a subtle way.

In its place, this commit changes the LinkClickObserver and
FormSubmitObserver to ignore <a> clicks and <form> submissions if
they're annotated with [data-remote="true"].

To exercise these edge cases, this commit also adds a ujs.html fixture
file along with a ujs_tests.ts module to cover situations when
@rails/ujs and @hotwired/turbo co-exist.

@seanpdoyle
Copy link
Contributor Author

I've converted this to a draft, since I'm not sure about the intended behavior.

While the test coverage ensures that there is only a single request issued, I'm not entirely sure about how @rails/ujs and @hotwired/turbo should co-exist in this scenario.

The test asserts that clicking <a data-remote="true"> would drive the frame. From a turbo-centric perspective, that feels correct, but the fact that the Turbo event listeners respond to the click event before the UJS handlers feels incidental instead of deliberate.

This covers an edge-case scenario for an application in the midst of a migration from UJS to Turbo. Given that rendering an <a data-remote="true"> element requires opt-in, the intention behind the element are likely at odds with driving a <turbo-frame> element.

To quote the original issue opened by @acetinick:

*the [...] turbo request, which is NOT expected resulting in an UnknownFormat exception which trying to render a .html format, but we only have a .js.erb view
the [...] UJS request, which is expected to return JS to open a modal window

Given those expectations, Turbo should lose the race against UJS for handling the event.

Since 7.1.0 supported the expected behavior, finding a way to revert #412 (incorporating changes made since its merge) might be the most backwards-compatible solution.

@acetinick
Copy link

This amazing, I'll try it out and let you know.

I guess for large applications like ours it's impossible to switch so easily, it's a slow transition screen by screen. We know we need to pull the pin eventually but for a point release of turbo it's hard for us to refactor soo many existing UJS methods, which are in the hundreds.

I think what always was the goal of turbo was move away from UJS which we agree. Baby steps. For a lot of large apps, it's a few years to rid completely. Like a jquery that sticks around

@acetinick
Copy link

To add to your comment about expectations, I believe it would be safe to assume that any link with data-remote should be ignored from turbo link intercepts completely.

If anyone is using data-remote they will not be expecting turbo to do anything with this request.

I believe same expectation will go for all remote situations of links/buttons and forms.

It's either one or the other.

@seanpdoyle seanpdoyle marked this pull request as ready for review September 30, 2022 18:45
Closes hotwired#743

The change in behavior can be traced back to [hotwired#412][].
When the overlap between the `LinkInterceptor` and the
`LinkClickObserver` were unified, the technique used by the
`LinkInterceptor` to prevent duplicate event handlers from responding to
the same `click` event was changed in a subtle way.

In its place, this commit changes the `LinkClickObserver` and
`FormSubmitObserver` to ignore `<a>` clicks and `<form>` submissions if
they're annotated with `[data-remote="true"]`.

To exercise these edge cases, this commit also adds a `ujs.html` fixture
file along with a `ujs_tests.ts` module to cover situations when
`@rails/ujs` and `@hotwired/turbo` co-exist.

[hotwired#412]: hotwired#412
@seanpdoyle seanpdoyle changed the title Prevent double requests from within turbo-frame Ignore UJS <a> clicks and <form> submissions Sep 30, 2022
@acetinick
Copy link

@dhh when you get chance could you review and if possible do a release on this. It's a blocker for anyone still using data-remote to upgrade to 7.2

@kevinmcconnell
Copy link
Collaborator

I think the situation is a little more complicated, because in Turbo 7.1, forms marked with data-remote="true" are submitted by Turbo (and not UJS) when they're inside a <turbo-frame>. It seems like an edge case, but it can be easy to run into if using config.action_view.form_with_generates_remote_forms = true in a Rails app. We've run into a handful of occurrences of this already.

In order to not break existing applications when they upgrade, we'd need to restore that behaviour.

Here's a minimal example to reproduce the original behaviour. On this page, the form submits via Turbo, and the link uses UJS.

<!doctype html>
<html>

<head>
  <meta charset="utf-8">
  <title>Test</title>

  <script src="https://cdn.jsdelivr.net/npm/rails-ujs@5.2.8-1/lib/assets/compiled/rails-ujs.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/@hotwired/turbo@7.1.0/dist/turbo.es2017-umd.min.js"></script>
</head>

<body>
  <turbo-frame id="frame">
    <form data-remote="true">
      <button>Submit</button>
    </form>

    <a data-remote="true" href="/">Click</a>
  </turbo-frame>
</body>

</html>

When the above code is using Turbo 7.2, both the link and the form issue duplicate requests. With this PR, both link & form will use UJS, which would be a change from 7.1.

@jeremy
Copy link

jeremy commented Oct 4, 2022

When the above code is using Turbo 7.2, both the link and the form issue duplicate requests. With this PR, both link & form will use UJS, which would be a change from 7.1.

True, but… seems worth breaking while it's still undefined behavior? Considering the same markup would then behave differently in & out of a frame.

Curious whether turbo-rails could manage Rails UJS integration. If Turbo offered a hook or delegate call for users to make this determination, then turbo-rails could use it to check for UJS conflict (and emit deprecation warnings, throw errors, etc.)

@seanpdoyle
Copy link
Contributor Author

seanpdoyle commented Oct 4, 2022

@jeremy I agree that package is a more appropriate home for UJS-specific code. I've previously explored what that might entail in hotwired/turbo-rails#93 and hotwired/turbo-rails#257.

If Turbo offered a hook or delegate call for users to make this determination

Applications can call event.preventDefault() on turbo:click events to opt-out of Turbo integration. Could a similar mechanism on turbo:submit-start support opting out of Form Submissions?

Then, Turbo Rails could define event listeners like:

// support already exists in Turbo
addEventListener("turbo:click", (event) => {
  const remote = event.target.closest('[data-remote="true"]')

  if (remote) event.preventDefault()
})

// support does not yet exist in Turbo
addEventListener("turbo:submit-start", (event) => {
  const remote = event.target.closest('[data-remote="true"]')

  if (remote) event.preventDefault()
})

@kevinmcconnell
Copy link
Collaborator

True, but… seems worth breaking while it's still undefined behavior?

The downside to breaking it is that upgrading 7.2 would then require people to update any forms inside <turbo-frame>s that had been marked remote: true, as they would now be handled differently. And as I say, that sounds like it would be an edge case, but given that many people will have been adding remote: true to their forms by default, it's likely to come up quite a bit.

I agree that the inconsistency is not great, but we could choose to preserve existing behaviour while also adding a nicer path for people to resolve the conflict on their own time. As you say, adding things like deprecation warnings would be helpful.

We can prevent the duplicate form submission by adding back the stopImmediatePropagation that was in 7.1.

Applications can call event.preventDefault() on turbo:click events to opt-out of Turbo integration

The ability to do this on click events is what changed in #412, I believe, which results in the double submissions from clicks in frames. The event handlers moving to the frame element itself means cancelling the event on document no longer works. But if we restored that behaviour, we could stop the double submissions without having UJS & Turbo need to be aware of each other at all. I think that's a nicer way to do it than Turbo having knowledge of data-remote. It also means that if there's anyone who has Turbo co-existing with some library other than UJS, it's less likely to break for them.

Personally I think restoring the behaviour of both those things would be the best option, since it'll make the upgrade path easier for folks. But I also agree that turbo-rails should provide help here, whether through deprecation warnings or (opt-in) errors where both UJS & Rails are competing, to help people work through updating their forms to be unambiguously one or the other.

@dhh
Copy link
Member

dhh commented Oct 5, 2022

Yeah, I'd very much like to see a quick 7.2.1 shipped with a solution to this, so we don't create an expectation of the new timing being something you can rely on, when we're changing it back to ensure compatibility 👍

@seanpdoyle seanpdoyle force-pushed the fix-issue-743 branch 2 times, most recently from d324cb9 to ab5558a Compare October 6, 2022 20:05
@seanpdoyle
Copy link
Contributor Author

I've pushed up changes that:

@kevinmcconnell
Copy link
Collaborator

This looks great @seanpdoyle -- appreciate you adding it 🙏 I've started testing this out in our apps here and so far it certainly seems to fix the regressions we'd seen related to UJS.

@dhh dhh merged commit 2345af9 into hotwired:main Oct 8, 2022
another-rex referenced this pull request in google/osv.dev Oct 31, 2022
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@hotwired/turbo](https://turbo.hotwired.dev)
([source](https://togithub.com/hotwired/turbo)) | [`7.2.0` ->
`7.2.4`](https://renovatebot.com/diffs/npm/@hotwired%2fturbo/7.2.0/7.2.4)
|
[![age](https://badges.renovateapi.com/packages/npm/@hotwired%2fturbo/7.2.4/age-slim)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://badges.renovateapi.com/packages/npm/@hotwired%2fturbo/7.2.4/adoption-slim)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://badges.renovateapi.com/packages/npm/@hotwired%2fturbo/7.2.4/compatibility-slim/7.2.0)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://badges.renovateapi.com/packages/npm/@hotwired%2fturbo/7.2.4/confidence-slim/7.2.0)](https://docs.renovatebot.com/merge-confidence/)
|
| [lit](https://lit.dev/) ([source](https://togithub.com/lit/lit)) |
[`2.3.1` -> `2.4.0`](https://renovatebot.com/diffs/npm/lit/2.3.1/2.4.0)
|
[![age](https://badges.renovateapi.com/packages/npm/lit/2.4.0/age-slim)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://badges.renovateapi.com/packages/npm/lit/2.4.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://badges.renovateapi.com/packages/npm/lit/2.4.0/compatibility-slim/2.3.1)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://badges.renovateapi.com/packages/npm/lit/2.4.0/confidence-slim/2.3.1)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>hotwired/turbo</summary>

### [`v7.2.4`](https://togithub.com/hotwired/turbo/releases/tag/v7.2.4)

[Compare
Source](https://togithub.com/hotwired/turbo/compare/v7.2.3...v7.2.4)

#### What's Changed

- Update history during same-page Visits by
[@&#8203;seanpdoyle](https://togithub.com/seanpdoyle) in
[https://github.com/hotwired/turbo/pull/763](https://togithub.com/hotwired/turbo/pull/763)
- Check last rendered location for same-page visits by
[@&#8203;kevinmcconnell](https://togithub.com/kevinmcconnell) in
[https://github.com/hotwired/turbo/pull/772](https://togithub.com/hotwired/turbo/pull/772)

**Full Changelog**:
hotwired/turbo@v7.2.2...v7.2.4

###
[`v7.2.3`](https://togithub.com/hotwired/turbo/compare/v7.2.2...v7.2.3)

[Compare
Source](https://togithub.com/hotwired/turbo/compare/v7.2.2...v7.2.3)

### [`v7.2.2`](https://togithub.com/hotwired/turbo/releases/tag/v7.2.2)

[Compare
Source](https://togithub.com/hotwired/turbo/compare/v7.2.1...v7.2.2)

#### What's Changed

- Fix double `before-fetch-request` dispatch during reload by
[@&#8203;seanpdoyle](https://togithub.com/seanpdoyle) in
[https://github.com/hotwired/turbo/pull/740](https://togithub.com/hotwired/turbo/pull/740)
- Ignore UJS `<a>` clicks and `<form>` submissions by
[@&#8203;seanpdoyle](https://togithub.com/seanpdoyle) in
[https://github.com/hotwired/turbo/pull/744](https://togithub.com/hotwired/turbo/pull/744)
- Cache PageSnapshot with original HTML from the frame's page by
[@&#8203;manuelpuyol](https://togithub.com/manuelpuyol) in
[https://github.com/hotwired/turbo/pull/746](https://togithub.com/hotwired/turbo/pull/746)
- Fix data-turbo-action was not respected on requestSubmit() event from
javascript by [@&#8203;seanpdoyle](https://togithub.com/seanpdoyle) in
[https://github.com/hotwired/turbo/pull/749](https://togithub.com/hotwired/turbo/pull/749)
- Reload page if failed form changes tracked content by
[@&#8203;kevinmcconnell](https://togithub.com/kevinmcconnell) in
[https://github.com/hotwired/turbo/pull/759](https://togithub.com/hotwired/turbo/pull/759)

(Note: 7.2.1 got pushed to npm as a copy of 7.2.0 as a mistake, hence
the extra jump in the tiny version).

**Full Changelog**:
hotwired/turbo@v7.2.0...v7.2.2

###
[`v7.2.1`](https://togithub.com/hotwired/turbo/compare/v7.2.0...v7.2.1)

[Compare
Source](https://togithub.com/hotwired/turbo/compare/v7.2.0...v7.2.1)

</details>

<details>
<summary>lit/lit</summary>

###
[`v2.4.0`](https://togithub.com/lit/lit/blob/HEAD/packages/lit/CHANGELOG.md#&#8203;240)

[Compare
Source](https://togithub.com/lit/lit/compare/847f4ba8570eb86d05be4378a3c954a941d2c063...lit@2.4.0)

##### Minor Changes

- [#&#8203;3318](https://togithub.com/lit/lit/pull/3318)
[`21313077`](https://togithub.com/lit/lit/commit/21313077669c19b3d631a50825b8a01dae1dd0d4)
- Adds an `isServer` variable export to `lit` and
`lit-html/is-server.js` which will be `true` in Node and `false` in the
browser. This can be used when authoring components to change behavior
based on whether or not the component is executing in an SSR context.

##### Patch Changes

- [#&#8203;3320](https://togithub.com/lit/lit/pull/3320)
[`305852d4`](https://togithub.com/lit/lit/commit/305852d4a4f51174301720985de98fdbf8674648)
- The `lit` package now specifies and "types" export condition allowing
TypeScript `moduleResolution` to be `nodenext`.

- Updated dependencies
\[[`21313077`](https://togithub.com/lit/lit/commit/21313077669c19b3d631a50825b8a01dae1dd0d4)]:
    -   lit-html@2.4.0

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 6am on monday" in timezone
Australia/Sydney, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config help](https://togithub.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://app.renovatebot.com/dashboard#github/google/osv.dev).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNC45LjEiLCJ1cGRhdGVkSW5WZXIiOiIzNC45LjEifQ==-->

Co-authored-by: Rex P <106129829+another-rex@users.noreply.github.com>
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.

7.2 data-remote inside turbo-frame produces double requests
5 participants