From 09d9fc46e94ff91b648aba390b0d058860f570ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez-Capel?= Date: Mon, 5 Feb 2024 15:21:38 +0000 Subject: [PATCH 1/6] Enable InstaClick by default It will be the new default for Turbo 8. You can always opt out by setting `` in the head of your HTML. --- src/observers/link_prefetch_observer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/observers/link_prefetch_observer.js b/src/observers/link_prefetch_observer.js index 165569dbd..04f93e44a 100644 --- a/src/observers/link_prefetch_observer.js +++ b/src/observers/link_prefetch_observer.js @@ -58,7 +58,7 @@ export class LinkPrefetchObserver { } #tryToPrefetchRequest = (event) => { - if (getMetaContent("turbo-prefetch") !== "true") return + if (getMetaContent("turbo-prefetch") === "false") return const target = event.target const isLink = target.matches && target.matches("a[href]:not([target^=_]):not([download])") From 127fc189691cbbeeaa60b7ee50c611e8bae33fe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez-Capel?= Date: Mon, 5 Feb 2024 15:29:53 +0000 Subject: [PATCH 2/6] Don't prefetch any anchor links --- src/observers/link_prefetch_observer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/observers/link_prefetch_observer.js b/src/observers/link_prefetch_observer.js index 04f93e44a..815cec10c 100644 --- a/src/observers/link_prefetch_observer.js +++ b/src/observers/link_prefetch_observer.js @@ -134,7 +134,7 @@ export class LinkPrefetchObserver { #isPrefetchable(link) { const href = link.getAttribute("href") - if (!href || href === "#" || link.getAttribute("data-turbo") === "false" || link.getAttribute("data-turbo-prefetch") === "false") { + if (!href || href.startsWith("#") || link.getAttribute("data-turbo") === "false" || link.getAttribute("data-turbo-prefetch") === "false") { return false } From 1251953326540f153b79b827b3310423c34ffc14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez-Capel?= Date: Mon, 5 Feb 2024 15:30:36 +0000 Subject: [PATCH 3/6] Don't prefetch UJS links For compatibility with older apps that use UJS, we should not prefetch links that have `data-remote`, `data-behavior`, `data-method`, or `data-confirm` attributes. All of these behaviors are now usually implemented as buttons, but there are still some apps that use them on links. --- src/observers/link_prefetch_observer.js | 4 ++++ src/tests/fixtures/hover_to_prefetch.html | 2 ++ src/tests/functional/link_prefetch_observer_tests.js | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/src/observers/link_prefetch_observer.js b/src/observers/link_prefetch_observer.js index 815cec10c..877881a7e 100644 --- a/src/observers/link_prefetch_observer.js +++ b/src/observers/link_prefetch_observer.js @@ -138,6 +138,10 @@ export class LinkPrefetchObserver { return false } + if (link.hasAttribute("data-remote") || link.hasAttribute("data-behavior") || link.hasAttribute("data-method") || link.hasAttribute("data-confirm")) { + return false + } + if (link.origin !== document.location.origin) { return false } diff --git a/src/tests/fixtures/hover_to_prefetch.html b/src/tests/fixtures/hover_to_prefetch.html index 89b94f1bb..5ee33684d 100644 --- a/src/tests/fixtures/hover_to_prefetch.html +++ b/src/tests/fixtures/hover_to_prefetch.html @@ -27,6 +27,8 @@ >Won't prefetch when hovering me Won't prefetch when hovering me + Won't prefetch when hovering me Won't prefetch when hovering me { + await goTo({ page, path: "/hover_to_prefetch.html" }) + await assertNotPrefetchedOnHover({ page, selector: "#anchor_with_remote_true" }) +}) + test("it doesn't prefetch the page when link has the same location", async ({ page }) => { await goTo({ page, path: "/hover_to_prefetch.html" }) await assertNotPrefetchedOnHover({ page, selector: "#anchor_for_same_location" }) From 5b866dba8e571fe4eb0a2ef3d076255514d29ce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez-Capel?= Date: Mon, 5 Feb 2024 18:58:38 +0000 Subject: [PATCH 4/6] Allow to customize check to opt out of prefetched links --- src/core/index.js | 15 +++++++++++++++ src/observers/link_prefetch_observer.js | 6 ++++-- .../functional/link_prefetch_observer_tests.js | 12 +++++++++++- src/util.js | 4 ++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/core/index.js b/src/core/index.js index a4a4f2d23..85ec412c0 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -107,3 +107,18 @@ export function setConfirmMethod(confirmMethod) { export function setFormMode(mode) { session.setFormMode(mode) } + +/** + * Allows to define additional logic to prevent prefetching of links. + * + * By default Turbo includes a check for compatibility with older apps to prevent + * prefetching unsafe UJS links, such as those that include data-remote, data-behavior, + * data-method or data-confirm attributes, but you can override this behavior by + * providing your own check. + * + * @param checkFn Function that returns a boolean value to indicate whether + * prefetching should be prevented or not + */ +export function preventLinkPrefetch(checkFn) { + session.linkPrefetchObserver.preventLinkPrefetch = checkFn +} diff --git a/src/observers/link_prefetch_observer.js b/src/observers/link_prefetch_observer.js index 877881a7e..9b1122876 100644 --- a/src/observers/link_prefetch_observer.js +++ b/src/observers/link_prefetch_observer.js @@ -2,7 +2,8 @@ import { doesNotTargetIFrame, getLocationForLink, getMetaContent, - findClosestRecursively + findClosestRecursively, + isUJSLink } from "../util" import { StreamMessage } from "../core/streams/stream_message" @@ -17,6 +18,7 @@ export class LinkPrefetchObserver { constructor(delegate, eventTarget) { this.delegate = delegate this.eventTarget = eventTarget + this.preventLinkPrefetch = (link) => isUJSLink(link) } start() { @@ -138,7 +140,7 @@ export class LinkPrefetchObserver { return false } - if (link.hasAttribute("data-remote") || link.hasAttribute("data-behavior") || link.hasAttribute("data-method") || link.hasAttribute("data-confirm")) { + if (this.preventLinkPrefetch(link)) { return false } diff --git a/src/tests/functional/link_prefetch_observer_tests.js b/src/tests/functional/link_prefetch_observer_tests.js index 9710583cf..910681bb2 100644 --- a/src/tests/functional/link_prefetch_observer_tests.js +++ b/src/tests/functional/link_prefetch_observer_tests.js @@ -62,11 +62,21 @@ test("it doesn't prefetch the page when link has data-turbo=false", async ({ pag await assertNotPrefetchedOnHover({ page, selector: "#anchor_with_turbo_false" }) }) -test("it doesn't prefetch the page when link has data-remote=true", async ({ page }) => { +test("it doesn't prefetch the page when link has UJS attributes", async ({ page }) => { await goTo({ page, path: "/hover_to_prefetch.html" }) await assertNotPrefetchedOnHover({ page, selector: "#anchor_with_remote_true" }) }) +test("allows to customize the check for prefetchable links", async ({ page }) => { + await goTo({ page, path: "/hover_to_prefetch.html" }) + + await page.evaluate(() => { + window.Turbo.preventLinkPrefetch(() => false) + }) + + await assertPrefetchedOnHover({ page, selector: "#anchor_with_remote_true" }) +}) + test("it doesn't prefetch the page when link has the same location", async ({ page }) => { await goTo({ page, path: "/hover_to_prefetch.html" }) await assertNotPrefetchedOnHover({ page, selector: "#anchor_for_same_location" }) diff --git a/src/util.js b/src/util.js index dcb15c31b..3f0cf0b2f 100644 --- a/src/util.js +++ b/src/util.js @@ -236,6 +236,10 @@ export function getLocationForLink(link) { return expandURL(link.getAttribute("href") || "") } +export function isUJSLink(link) { + return link.hasAttribute("data-remote") || link.hasAttribute("data-behavior") || link.hasAttribute("data-method") || link.hasAttribute("data-confirm") +} + export function debounce(fn, delay) { let timeoutId = null From bca14e4e6b3a24b8119de37080cc79dd8c1a30be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez-Capel?= Date: Mon, 5 Feb 2024 20:13:44 +0000 Subject: [PATCH 5/6] Tweak documentation --- src/core/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/index.js b/src/core/index.js index 85ec412c0..39cc93f00 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -116,8 +116,8 @@ export function setFormMode(mode) { * data-method or data-confirm attributes, but you can override this behavior by * providing your own check. * - * @param checkFn Function that returns a boolean value to indicate whether - * prefetching should be prevented or not + * @param checkFn Function that takes an anchor element and returns a boolean value + * to indicate whether prefetching should be prevented or not */ export function preventLinkPrefetch(checkFn) { session.linkPrefetchObserver.preventLinkPrefetch = checkFn From 7327a955ea93ef885ea157977acaeb96068f80ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez-Capel?= Date: Mon, 5 Feb 2024 21:34:44 +0000 Subject: [PATCH 6/6] Introduce `turbo:before-prefetch` event To allow for more fine-grained control over when Turbo should prefetch links. This change introduces a new event that can be used to cancel prefetch requests based on custom logic. For example, if you want to prevent Turbo from prefetching links that include UJS attributes, you can do so by adding an event listener for the `turbo:before-prefetch` event and calling `preventDefault` on the event object when the link should not be prefetched. ```javascript document.body.addEventListener("turbo:before-prefetch", (event) => { if (isUJSLink(event.target)) event.preventDefault() }) function isUJSLink(link) { return link.hasAttribute("data-remote") || link.hasAttribute("data-behavior") || link.hasAttribute("data-method") || link.hasAttribute("data-confirm") } ``` --- src/core/index.js | 15 --------------- src/observers/link_prefetch_observer.js | 12 ++++++++---- .../functional/link_prefetch_observer_tests.js | 15 ++++++++------- src/util.js | 4 ---- 4 files changed, 16 insertions(+), 30 deletions(-) diff --git a/src/core/index.js b/src/core/index.js index 39cc93f00..a4a4f2d23 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -107,18 +107,3 @@ export function setConfirmMethod(confirmMethod) { export function setFormMode(mode) { session.setFormMode(mode) } - -/** - * Allows to define additional logic to prevent prefetching of links. - * - * By default Turbo includes a check for compatibility with older apps to prevent - * prefetching unsafe UJS links, such as those that include data-remote, data-behavior, - * data-method or data-confirm attributes, but you can override this behavior by - * providing your own check. - * - * @param checkFn Function that takes an anchor element and returns a boolean value - * to indicate whether prefetching should be prevented or not - */ -export function preventLinkPrefetch(checkFn) { - session.linkPrefetchObserver.preventLinkPrefetch = checkFn -} diff --git a/src/observers/link_prefetch_observer.js b/src/observers/link_prefetch_observer.js index 9b1122876..a63fe6382 100644 --- a/src/observers/link_prefetch_observer.js +++ b/src/observers/link_prefetch_observer.js @@ -1,9 +1,9 @@ import { + dispatch, doesNotTargetIFrame, getLocationForLink, getMetaContent, - findClosestRecursively, - isUJSLink + findClosestRecursively } from "../util" import { StreamMessage } from "../core/streams/stream_message" @@ -18,7 +18,6 @@ export class LinkPrefetchObserver { constructor(delegate, eventTarget) { this.delegate = delegate this.eventTarget = eventTarget - this.preventLinkPrefetch = (link) => isUJSLink(link) } start() { @@ -140,7 +139,12 @@ export class LinkPrefetchObserver { return false } - if (this.preventLinkPrefetch(link)) { + const event = dispatch("turbo:before-prefetch", { + target: link, + cancelable: true + }) + + if (event.defaultPrevented) { return false } diff --git a/src/tests/functional/link_prefetch_observer_tests.js b/src/tests/functional/link_prefetch_observer_tests.js index 910681bb2..56fe77d89 100644 --- a/src/tests/functional/link_prefetch_observer_tests.js +++ b/src/tests/functional/link_prefetch_observer_tests.js @@ -62,19 +62,20 @@ test("it doesn't prefetch the page when link has data-turbo=false", async ({ pag await assertNotPrefetchedOnHover({ page, selector: "#anchor_with_turbo_false" }) }) -test("it doesn't prefetch the page when link has UJS attributes", async ({ page }) => { +test("allows to cancel prefetch requests with custom logic", async ({ page }) => { await goTo({ page, path: "/hover_to_prefetch.html" }) - await assertNotPrefetchedOnHover({ page, selector: "#anchor_with_remote_true" }) -}) -test("allows to customize the check for prefetchable links", async ({ page }) => { - await goTo({ page, path: "/hover_to_prefetch.html" }) + await assertPrefetchedOnHover({ page, selector: "#anchor_with_remote_true" }) await page.evaluate(() => { - window.Turbo.preventLinkPrefetch(() => false) + document.body.addEventListener("turbo:before-prefetch", (event) => { + if (event.target.hasAttribute("data-remote")) { + event.preventDefault() + } + }) }) - await assertPrefetchedOnHover({ page, selector: "#anchor_with_remote_true" }) + await assertNotPrefetchedOnHover({ page, selector: "#anchor_with_remote_true" }) }) test("it doesn't prefetch the page when link has the same location", async ({ page }) => { diff --git a/src/util.js b/src/util.js index 3f0cf0b2f..dcb15c31b 100644 --- a/src/util.js +++ b/src/util.js @@ -236,10 +236,6 @@ export function getLocationForLink(link) { return expandURL(link.getAttribute("href") || "") } -export function isUJSLink(link) { - return link.hasAttribute("data-remote") || link.hasAttribute("data-behavior") || link.hasAttribute("data-method") || link.hasAttribute("data-confirm") -} - export function debounce(fn, delay) { let timeoutId = null