Skip to content

Commit 3db3efd

Browse files
committedSep 30, 2022
Prevent double requests from within turbo-frame
Closes #743 The change in behavior can be traced back to [#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. [#412]: #412
1 parent 732db00 commit 3db3efd

File tree

4 files changed

+75
-1
lines changed

4 files changed

+75
-1
lines changed
 

‎src/observers/form_submit_observer.ts

+10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { getAttribute } from "../util"
2+
13
export interface FormSubmitObserverDelegate {
24
willSubmitForm(form: HTMLFormElement, submitter?: HTMLElement): boolean
35
formSubmitted(form: HTMLFormElement, submitter?: HTMLElement): void
@@ -41,6 +43,7 @@ export class FormSubmitObserver {
4143
form &&
4244
submissionDoesNotDismissDialog(form, submitter) &&
4345
submissionDoesNotTargetIFrame(form, submitter) &&
46+
submissionDoesNotIntegrateWithUJS(form, submitter) &&
4447
this.delegate.willSubmitForm(form, submitter)
4548
) {
4649
event.preventDefault()
@@ -65,3 +68,10 @@ function submissionDoesNotTargetIFrame(form: HTMLFormElement, submitter?: HTMLEl
6568

6669
return true
6770
}
71+
72+
function submissionDoesNotIntegrateWithUJS(form: HTMLFormElement, submitter?: HTMLElement): boolean {
73+
const value = getAttribute("data-remote", submitter, form)
74+
const remote = /true/i.test(value || "")
75+
76+
return !remote
77+
}

‎src/observers/link_click_observer.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export class LinkClickObserver {
3838
if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
3939
const target = (event.composedPath && event.composedPath()[0]) || event.target
4040
const link = this.findLinkFromClickTarget(target)
41-
if (link && doesNotTargetIFrame(link)) {
41+
if (link && doesNotTargetIFrame(link) && doesNotIntegrateWithUJS(link)) {
4242
const location = this.getLocationForLink(link)
4343
if (this.delegate.willFollowLinkToLocation(link, location, event)) {
4444
event.preventDefault()
@@ -78,3 +78,10 @@ function doesNotTargetIFrame(anchor: HTMLAnchorElement): boolean {
7878

7979
return true
8080
}
81+
82+
function doesNotIntegrateWithUJS(anchor: HTMLAnchorElement): boolean {
83+
const value = anchor.getAttribute("data-remote")
84+
const remote = /true/i.test(value || "")
85+
86+
return !remote
87+
}

‎src/tests/fixtures/ujs.html

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Frame</title>
6+
<script src="/src/tests/fixtures/test.js"></script>
7+
<script type="module">
8+
import Rails from "https://ga.jspm.io/npm:@rails/ujs@7.0.1/lib/assets/compiled/rails-ujs.js"
9+
10+
Rails.start()
11+
</script>
12+
<script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
13+
</head>
14+
<body>
15+
<turbo-frame id="frame">
16+
<h2>Frames: #frame</h2>
17+
18+
<a data-remote="true" href="/src/tests/fixtures/frames/frame.html">navigate #frame to /src/tests/fixtures/frames/frame.html</a>
19+
<form data-remote="true" action="/src/tests/fixtures/frames/frame.html">
20+
<button>navigate #frame to /src/tests/fixtures/frames/frame.html</button>
21+
</form>
22+
</turbo-frame>
23+
</body>
24+
</html>

‎src/tests/functional/ujs_tests.ts

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Page, test } from "@playwright/test"
2+
import { assert } from "chai"
3+
import { noNextEventOnTarget } from "../helpers/page"
4+
5+
test.beforeEach(async ({ page }) => {
6+
await page.goto("/src/tests/fixtures/ujs.html")
7+
})
8+
9+
test("test clicking a [data-remote=true] anchor within a turbo-frame", async ({ page }) => {
10+
await assertRequestLimit(page, 1, async () => {
11+
await page.click("#frame a[data-remote=true]")
12+
await noNextEventOnTarget(page, "frame", "turbo:frame-load")
13+
14+
assert.equal(await page.textContent("#frame h2"), "Frames: #frame", "does not navigate the target frame")
15+
})
16+
})
17+
18+
test("test submitting a [data-remote=true] form within a turbo-frame", async ({ page }) => {
19+
await assertRequestLimit(page, 1, async () => {
20+
await page.click("#frame form[data-remote=true] button")
21+
await noNextEventOnTarget(page, "frame", "turbo:frame-load")
22+
23+
assert.equal(await page.textContent("#frame h2"), "Frames: #frame", "does not navigate the target frame")
24+
})
25+
})
26+
27+
async function assertRequestLimit(page: Page, count: number, callback: () => Promise<void>) {
28+
let requestsStarted = 0
29+
await page.on("request", () => requestsStarted++)
30+
await callback()
31+
32+
assert.equal(requestsStarted, 1, `only submits ${count} requests`)
33+
}

0 commit comments

Comments
 (0)