-
Notifications
You must be signed in to change notification settings - Fork 429
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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 * Revert "Replace LinkInterceptor with LinkClickObserver" This reverts commit 9bef653. * adjust after reverting #412 * Restore `<form data-remote="true">`+`<turbo-frame>` behavior from 7.1.0 [comment]: #744 (comment) [FormInterceptor]: https://github.com/hotwired/turbo/blob/v7.1.0/src/core/frames/form_interceptor.ts#L31
- Loading branch information
1 parent
732db00
commit 2345af9
Showing
10 changed files
with
165 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { TurboClickEvent, TurboBeforeVisitEvent } from "../session" | ||
|
||
export interface LinkInterceptorDelegate { | ||
shouldInterceptLinkClick(element: Element, url: string, originalEvent: MouseEvent): boolean | ||
linkClickIntercepted(element: Element, url: string, originalEvent: MouseEvent): void | ||
} | ||
|
||
export class LinkInterceptor { | ||
readonly delegate: LinkInterceptorDelegate | ||
readonly element: Element | ||
private clickEvent?: Event | ||
|
||
constructor(delegate: LinkInterceptorDelegate, element: Element) { | ||
this.delegate = delegate | ||
this.element = element | ||
} | ||
|
||
start() { | ||
this.element.addEventListener("click", this.clickBubbled) | ||
document.addEventListener("turbo:click", this.linkClicked) | ||
document.addEventListener("turbo:before-visit", this.willVisit) | ||
} | ||
|
||
stop() { | ||
this.element.removeEventListener("click", this.clickBubbled) | ||
document.removeEventListener("turbo:click", this.linkClicked) | ||
document.removeEventListener("turbo:before-visit", this.willVisit) | ||
} | ||
|
||
clickBubbled = (event: Event) => { | ||
if (this.respondsToEventTarget(event.target)) { | ||
this.clickEvent = event | ||
} else { | ||
delete this.clickEvent | ||
} | ||
} | ||
|
||
linkClicked = <EventListener>((event: TurboClickEvent) => { | ||
if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) { | ||
if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url, event.detail.originalEvent)) { | ||
this.clickEvent.preventDefault() | ||
event.preventDefault() | ||
this.delegate.linkClickIntercepted(event.target, event.detail.url, event.detail.originalEvent) | ||
} | ||
} | ||
delete this.clickEvent | ||
}) | ||
|
||
willVisit = <EventListener>((_event: TurboBeforeVisitEvent) => { | ||
delete this.clickEvent | ||
}) | ||
|
||
respondsToEventTarget(target: EventTarget | null) { | ||
const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null | ||
return element && element.closest("turbo-frame, html") == this.element | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset="utf-8"> | ||
<title>Frame</title> | ||
<script src="/src/tests/fixtures/test.js"></script> | ||
<script type="module"> | ||
import Rails from "https://ga.jspm.io/npm:@rails/ujs@7.0.1/lib/assets/compiled/rails-ujs.js" | ||
|
||
Rails.start() | ||
</script> | ||
<script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script> | ||
</head> | ||
<body> | ||
<turbo-frame id="frame"> | ||
<h2>Frames: #frame</h2> | ||
|
||
<a data-remote="true" href="/src/tests/fixtures/frames/frame.html">navigate #frame to /src/tests/fixtures/frames/frame.html</a> | ||
<form data-remote="true" action="/src/tests/fixtures/frames/frame.html"> | ||
<button>navigate #frame to /src/tests/fixtures/frames/frame.html</button> | ||
</form> | ||
</turbo-frame> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { Page, test } from "@playwright/test" | ||
import { assert } from "chai" | ||
import { noNextEventOnTarget } from "../helpers/page" | ||
|
||
test.beforeEach(async ({ page }) => { | ||
await page.goto("/src/tests/fixtures/ujs.html") | ||
}) | ||
|
||
test("test clicking a [data-remote=true] anchor within a turbo-frame", async ({ page }) => { | ||
await assertRequestLimit(page, 1, async () => { | ||
assert.equal(await page.textContent("#frame h2"), "Frames: #frame") | ||
|
||
await page.click("#frame a[data-remote=true]") | ||
await noNextEventOnTarget(page, "frame", "turbo:frame-load") | ||
|
||
assert.equal(await page.textContent("#frame h2"), "Frames: #frame", "does not navigate the target frame") | ||
}) | ||
}) | ||
|
||
test("test submitting a [data-remote=true] form within a turbo-frame", async ({ page }) => { | ||
await assertRequestLimit(page, 1, async () => { | ||
assert.equal(await page.textContent("#frame h2"), "Frames: #frame") | ||
|
||
await page.click("#frame form[data-remote=true] button") | ||
await noNextEventOnTarget(page, "frame", "turbo:frame-load") | ||
|
||
assert.equal(await page.textContent("#frame h2"), "Frame: Loaded", "navigates the target frame") | ||
}) | ||
}) | ||
|
||
async function assertRequestLimit(page: Page, count: number, callback: () => Promise<void>) { | ||
let requestsStarted = 0 | ||
await page.on("request", () => requestsStarted++) | ||
await callback() | ||
|
||
assert.equal(requestsStarted, count, `only submits ${count} requests`) | ||
} |
Oops, something went wrong.