diff --git a/src/runtime/entrypoints/main.ts b/src/runtime/entrypoints/main.ts index 3b0ac261332..792724851c9 100644 --- a/src/runtime/entrypoints/main.ts +++ b/src/runtime/entrypoints/main.ts @@ -556,14 +556,15 @@ function _walkInner( const partialErrorMessage = `Unable to process partial response.`; -async function fetchPartials(url: URL, realUrl: URL, init?: RequestInit) { +async function fetchPartials(url: URL, init?: RequestInit) { url.searchParams.set(PARTIAL_SEARCH_PARAM, "true"); const res = await fetch(url, init); await applyPartials(res); +} - // Update links +function updateLinks(url: URL) { document.querySelectorAll("a").forEach((link) => { - const match = matchesUrl(realUrl.pathname, link.href); + const match = matchesUrl(url.pathname, link.href); if (match === UrlMatchKind.Current) { link.setAttribute(DATA_CURRENT, "true"); @@ -884,6 +885,8 @@ if (!history.state) { document.addEventListener("click", async (e) => { let el = e.target; if (el && el instanceof HTMLElement) { + const originalEl = el; + // Check if we clicked inside an anchor link if (el.nodeName !== "A") { el = el.closest("a"); @@ -947,13 +950,39 @@ document.addEventListener("click", async (e) => { partial ? partial : nextUrl.href, location.origin, ); - await fetchPartials(partialUrl, nextUrl); + await fetchPartials(partialUrl); + updateLinks(nextUrl); scrollTo({ left: 0, top: 0, behavior: "instant" }); } finally { if (indicator !== undefined) { indicator.value = false; } } + } else { + let button: HTMLButtonElement | HTMLElement | null = originalEl; + // Check if we clicked on a button + if (button.nodeName !== "A") { + button = button.closest("button"); + } + + if (button !== null && button instanceof HTMLButtonElement) { + const partial = button.getAttribute(PARTIAL_ATTR); + + // Check if the user opted out of client side navigation. + if ( + partial === null || + !checkClientNavEnabled() || + button.closest(`[${CLIENT_NAV_ATTR}="true"]`) === null + ) { + return; + } + + const partialUrl = new URL( + partial, + location.origin, + ); + await fetchPartials(partialUrl); + } } } }); @@ -986,7 +1015,8 @@ addEventListener("popstate", async (e) => { const url = new URL(location.href, location.origin); try { - await fetchPartials(url, url); + await fetchPartials(url); + updateLinks(url); scrollTo({ left: state.scrollX ?? 0, top: state.scrollY ?? 0, @@ -1013,7 +1043,7 @@ document.addEventListener("submit", async (e) => { e.preventDefault(); const url = new URL(partial, location.origin); - await fetchPartials(url, new URL(location.href)); + await fetchPartials(url); } } }); diff --git a/tests/fixture_partials/fresh.gen.ts b/tests/fixture_partials/fresh.gen.ts index a8d00f63977..05b86a27021 100644 --- a/tests/fixture_partials/fresh.gen.ts +++ b/tests/fixture_partials/fresh.gen.ts @@ -12,80 +12,82 @@ import * as $6 from "./routes/active_nav_partial/foo/bar.tsx"; import * as $7 from "./routes/active_nav_partial/foo/index.tsx"; import * as $8 from "./routes/active_nav_partial/index.tsx"; import * as $9 from "./routes/active_nav_partial/island.tsx"; -import * as $10 from "./routes/client_nav/_layout.tsx"; -import * as $11 from "./routes/client_nav/index.tsx"; -import * as $12 from "./routes/client_nav/injected.tsx"; -import * as $13 from "./routes/client_nav/page-a.tsx"; -import * as $14 from "./routes/client_nav/page-b.tsx"; -import * as $15 from "./routes/client_nav/page-c.tsx"; -import * as $16 from "./routes/client_nav_opt_out/_layout.tsx"; -import * as $17 from "./routes/client_nav_opt_out/index.tsx"; -import * as $18 from "./routes/client_nav_opt_out/injected.tsx"; -import * as $19 from "./routes/client_nav_opt_out/page-a.tsx"; -import * as $20 from "./routes/client_nav_opt_out/page-b.tsx"; -import * as $21 from "./routes/client_nav_opt_out/page-c.tsx"; -import * as $22 from "./routes/deep_partial/index.tsx"; -import * as $23 from "./routes/deep_partial/injected.tsx"; -import * as $24 from "./routes/deep_partial/update.tsx"; -import * as $25 from "./routes/form/index.tsx"; -import * as $26 from "./routes/form/injected.tsx"; -import * as $27 from "./routes/form/update.tsx"; -import * as $28 from "./routes/fragment_nav.tsx"; -import * as $29 from "./routes/head_merge/duplicate.tsx"; -import * as $30 from "./routes/head_merge/index.tsx"; -import * as $31 from "./routes/head_merge/injected.tsx"; -import * as $32 from "./routes/head_merge/update.tsx"; -import * as $33 from "./routes/index.tsx"; -import * as $34 from "./routes/island_instance/index.tsx"; -import * as $35 from "./routes/island_instance/injected.tsx"; -import * as $36 from "./routes/island_instance/partial.tsx"; -import * as $37 from "./routes/island_instance/partial_remove.tsx"; -import * as $38 from "./routes/island_instance/partial_replace.tsx"; -import * as $39 from "./routes/island_instance_multiple/index.tsx"; -import * as $40 from "./routes/island_instance_multiple/injected.tsx"; -import * as $41 from "./routes/island_instance_multiple/partial.tsx"; -import * as $42 from "./routes/island_instance_multiple/partial_both.tsx"; -import * as $43 from "./routes/island_instance_nested/index.tsx"; -import * as $44 from "./routes/island_instance_nested/injected.tsx"; -import * as $45 from "./routes/island_instance_nested/partial.tsx"; -import * as $46 from "./routes/island_instance_nested/replace.tsx"; -import * as $47 from "./routes/island_props/index.tsx"; -import * as $48 from "./routes/island_props/injected.tsx"; -import * as $49 from "./routes/island_props/partial.tsx"; -import * as $50 from "./routes/island_props_signals/index.tsx"; -import * as $51 from "./routes/island_props_signals/injected.tsx"; -import * as $52 from "./routes/island_props_signals/partial.tsx"; -import * as $53 from "./routes/keys/index.tsx"; -import * as $54 from "./routes/keys/injected.tsx"; -import * as $55 from "./routes/keys/swap.tsx"; -import * as $56 from "./routes/keys_components/index.tsx"; -import * as $57 from "./routes/keys_components/injected.tsx"; -import * as $58 from "./routes/keys_components/swap.tsx"; -import * as $59 from "./routes/keys_dom/index.tsx"; -import * as $60 from "./routes/keys_dom/injected.tsx"; -import * as $61 from "./routes/keys_dom/swap.tsx"; -import * as $62 from "./routes/keys_outside/index.tsx"; -import * as $63 from "./routes/loading/index.tsx"; -import * as $64 from "./routes/loading/injected.tsx"; -import * as $65 from "./routes/loading/update.tsx"; -import * as $66 from "./routes/missing_partial/index.tsx"; -import * as $67 from "./routes/missing_partial/injected.tsx"; -import * as $68 from "./routes/missing_partial/update.tsx"; -import * as $69 from "./routes/mode/append.tsx"; -import * as $70 from "./routes/mode/index.tsx"; -import * as $71 from "./routes/mode/injected.tsx"; -import * as $72 from "./routes/mode/prepend.tsx"; -import * as $73 from "./routes/mode/replace.tsx"; -import * as $74 from "./routes/no_islands/index.tsx"; -import * as $75 from "./routes/no_islands/injected.tsx"; -import * as $76 from "./routes/no_islands/update.tsx"; -import * as $77 from "./routes/no_partial_response/index.tsx"; -import * as $78 from "./routes/no_partial_response/injected.tsx"; -import * as $79 from "./routes/no_partial_response/update.tsx"; -import * as $80 from "./routes/partial_slot_inside_island.tsx"; -import * as $81 from "./routes/scroll_restoration/index.tsx"; -import * as $82 from "./routes/scroll_restoration/injected.tsx"; -import * as $83 from "./routes/scroll_restoration/update.tsx"; +import * as $10 from "./routes/button/index.tsx"; +import * as $11 from "./routes/button/update.tsx"; +import * as $12 from "./routes/client_nav/_layout.tsx"; +import * as $13 from "./routes/client_nav/index.tsx"; +import * as $14 from "./routes/client_nav/injected.tsx"; +import * as $15 from "./routes/client_nav/page-a.tsx"; +import * as $16 from "./routes/client_nav/page-b.tsx"; +import * as $17 from "./routes/client_nav/page-c.tsx"; +import * as $18 from "./routes/client_nav_opt_out/_layout.tsx"; +import * as $19 from "./routes/client_nav_opt_out/index.tsx"; +import * as $20 from "./routes/client_nav_opt_out/injected.tsx"; +import * as $21 from "./routes/client_nav_opt_out/page-a.tsx"; +import * as $22 from "./routes/client_nav_opt_out/page-b.tsx"; +import * as $23 from "./routes/client_nav_opt_out/page-c.tsx"; +import * as $24 from "./routes/deep_partial/index.tsx"; +import * as $25 from "./routes/deep_partial/injected.tsx"; +import * as $26 from "./routes/deep_partial/update.tsx"; +import * as $27 from "./routes/form/index.tsx"; +import * as $28 from "./routes/form/injected.tsx"; +import * as $29 from "./routes/form/update.tsx"; +import * as $30 from "./routes/fragment_nav.tsx"; +import * as $31 from "./routes/head_merge/duplicate.tsx"; +import * as $32 from "./routes/head_merge/index.tsx"; +import * as $33 from "./routes/head_merge/injected.tsx"; +import * as $34 from "./routes/head_merge/update.tsx"; +import * as $35 from "./routes/index.tsx"; +import * as $36 from "./routes/island_instance/index.tsx"; +import * as $37 from "./routes/island_instance/injected.tsx"; +import * as $38 from "./routes/island_instance/partial.tsx"; +import * as $39 from "./routes/island_instance/partial_remove.tsx"; +import * as $40 from "./routes/island_instance/partial_replace.tsx"; +import * as $41 from "./routes/island_instance_multiple/index.tsx"; +import * as $42 from "./routes/island_instance_multiple/injected.tsx"; +import * as $43 from "./routes/island_instance_multiple/partial.tsx"; +import * as $44 from "./routes/island_instance_multiple/partial_both.tsx"; +import * as $45 from "./routes/island_instance_nested/index.tsx"; +import * as $46 from "./routes/island_instance_nested/injected.tsx"; +import * as $47 from "./routes/island_instance_nested/partial.tsx"; +import * as $48 from "./routes/island_instance_nested/replace.tsx"; +import * as $49 from "./routes/island_props/index.tsx"; +import * as $50 from "./routes/island_props/injected.tsx"; +import * as $51 from "./routes/island_props/partial.tsx"; +import * as $52 from "./routes/island_props_signals/index.tsx"; +import * as $53 from "./routes/island_props_signals/injected.tsx"; +import * as $54 from "./routes/island_props_signals/partial.tsx"; +import * as $55 from "./routes/keys/index.tsx"; +import * as $56 from "./routes/keys/injected.tsx"; +import * as $57 from "./routes/keys/swap.tsx"; +import * as $58 from "./routes/keys_components/index.tsx"; +import * as $59 from "./routes/keys_components/injected.tsx"; +import * as $60 from "./routes/keys_components/swap.tsx"; +import * as $61 from "./routes/keys_dom/index.tsx"; +import * as $62 from "./routes/keys_dom/injected.tsx"; +import * as $63 from "./routes/keys_dom/swap.tsx"; +import * as $64 from "./routes/keys_outside/index.tsx"; +import * as $65 from "./routes/loading/index.tsx"; +import * as $66 from "./routes/loading/injected.tsx"; +import * as $67 from "./routes/loading/update.tsx"; +import * as $68 from "./routes/missing_partial/index.tsx"; +import * as $69 from "./routes/missing_partial/injected.tsx"; +import * as $70 from "./routes/missing_partial/update.tsx"; +import * as $71 from "./routes/mode/append.tsx"; +import * as $72 from "./routes/mode/index.tsx"; +import * as $73 from "./routes/mode/injected.tsx"; +import * as $74 from "./routes/mode/prepend.tsx"; +import * as $75 from "./routes/mode/replace.tsx"; +import * as $76 from "./routes/no_islands/index.tsx"; +import * as $77 from "./routes/no_islands/injected.tsx"; +import * as $78 from "./routes/no_islands/update.tsx"; +import * as $79 from "./routes/no_partial_response/index.tsx"; +import * as $80 from "./routes/no_partial_response/injected.tsx"; +import * as $81 from "./routes/no_partial_response/update.tsx"; +import * as $82 from "./routes/partial_slot_inside_island.tsx"; +import * as $83 from "./routes/scroll_restoration/index.tsx"; +import * as $84 from "./routes/scroll_restoration/injected.tsx"; +import * as $85 from "./routes/scroll_restoration/update.tsx"; import * as $$0 from "./islands/Counter.tsx"; import * as $$1 from "./islands/CounterA.tsx"; import * as $$2 from "./islands/CounterB.tsx"; @@ -113,80 +115,82 @@ const manifest = { "./routes/active_nav_partial/foo/index.tsx": $7, "./routes/active_nav_partial/index.tsx": $8, "./routes/active_nav_partial/island.tsx": $9, - "./routes/client_nav/_layout.tsx": $10, - "./routes/client_nav/index.tsx": $11, - "./routes/client_nav/injected.tsx": $12, - "./routes/client_nav/page-a.tsx": $13, - "./routes/client_nav/page-b.tsx": $14, - "./routes/client_nav/page-c.tsx": $15, - "./routes/client_nav_opt_out/_layout.tsx": $16, - "./routes/client_nav_opt_out/index.tsx": $17, - "./routes/client_nav_opt_out/injected.tsx": $18, - "./routes/client_nav_opt_out/page-a.tsx": $19, - "./routes/client_nav_opt_out/page-b.tsx": $20, - "./routes/client_nav_opt_out/page-c.tsx": $21, - "./routes/deep_partial/index.tsx": $22, - "./routes/deep_partial/injected.tsx": $23, - "./routes/deep_partial/update.tsx": $24, - "./routes/form/index.tsx": $25, - "./routes/form/injected.tsx": $26, - "./routes/form/update.tsx": $27, - "./routes/fragment_nav.tsx": $28, - "./routes/head_merge/duplicate.tsx": $29, - "./routes/head_merge/index.tsx": $30, - "./routes/head_merge/injected.tsx": $31, - "./routes/head_merge/update.tsx": $32, - "./routes/index.tsx": $33, - "./routes/island_instance/index.tsx": $34, - "./routes/island_instance/injected.tsx": $35, - "./routes/island_instance/partial.tsx": $36, - "./routes/island_instance/partial_remove.tsx": $37, - "./routes/island_instance/partial_replace.tsx": $38, - "./routes/island_instance_multiple/index.tsx": $39, - "./routes/island_instance_multiple/injected.tsx": $40, - "./routes/island_instance_multiple/partial.tsx": $41, - "./routes/island_instance_multiple/partial_both.tsx": $42, - "./routes/island_instance_nested/index.tsx": $43, - "./routes/island_instance_nested/injected.tsx": $44, - "./routes/island_instance_nested/partial.tsx": $45, - "./routes/island_instance_nested/replace.tsx": $46, - "./routes/island_props/index.tsx": $47, - "./routes/island_props/injected.tsx": $48, - "./routes/island_props/partial.tsx": $49, - "./routes/island_props_signals/index.tsx": $50, - "./routes/island_props_signals/injected.tsx": $51, - "./routes/island_props_signals/partial.tsx": $52, - "./routes/keys/index.tsx": $53, - "./routes/keys/injected.tsx": $54, - "./routes/keys/swap.tsx": $55, - "./routes/keys_components/index.tsx": $56, - "./routes/keys_components/injected.tsx": $57, - "./routes/keys_components/swap.tsx": $58, - "./routes/keys_dom/index.tsx": $59, - "./routes/keys_dom/injected.tsx": $60, - "./routes/keys_dom/swap.tsx": $61, - "./routes/keys_outside/index.tsx": $62, - "./routes/loading/index.tsx": $63, - "./routes/loading/injected.tsx": $64, - "./routes/loading/update.tsx": $65, - "./routes/missing_partial/index.tsx": $66, - "./routes/missing_partial/injected.tsx": $67, - "./routes/missing_partial/update.tsx": $68, - "./routes/mode/append.tsx": $69, - "./routes/mode/index.tsx": $70, - "./routes/mode/injected.tsx": $71, - "./routes/mode/prepend.tsx": $72, - "./routes/mode/replace.tsx": $73, - "./routes/no_islands/index.tsx": $74, - "./routes/no_islands/injected.tsx": $75, - "./routes/no_islands/update.tsx": $76, - "./routes/no_partial_response/index.tsx": $77, - "./routes/no_partial_response/injected.tsx": $78, - "./routes/no_partial_response/update.tsx": $79, - "./routes/partial_slot_inside_island.tsx": $80, - "./routes/scroll_restoration/index.tsx": $81, - "./routes/scroll_restoration/injected.tsx": $82, - "./routes/scroll_restoration/update.tsx": $83, + "./routes/button/index.tsx": $10, + "./routes/button/update.tsx": $11, + "./routes/client_nav/_layout.tsx": $12, + "./routes/client_nav/index.tsx": $13, + "./routes/client_nav/injected.tsx": $14, + "./routes/client_nav/page-a.tsx": $15, + "./routes/client_nav/page-b.tsx": $16, + "./routes/client_nav/page-c.tsx": $17, + "./routes/client_nav_opt_out/_layout.tsx": $18, + "./routes/client_nav_opt_out/index.tsx": $19, + "./routes/client_nav_opt_out/injected.tsx": $20, + "./routes/client_nav_opt_out/page-a.tsx": $21, + "./routes/client_nav_opt_out/page-b.tsx": $22, + "./routes/client_nav_opt_out/page-c.tsx": $23, + "./routes/deep_partial/index.tsx": $24, + "./routes/deep_partial/injected.tsx": $25, + "./routes/deep_partial/update.tsx": $26, + "./routes/form/index.tsx": $27, + "./routes/form/injected.tsx": $28, + "./routes/form/update.tsx": $29, + "./routes/fragment_nav.tsx": $30, + "./routes/head_merge/duplicate.tsx": $31, + "./routes/head_merge/index.tsx": $32, + "./routes/head_merge/injected.tsx": $33, + "./routes/head_merge/update.tsx": $34, + "./routes/index.tsx": $35, + "./routes/island_instance/index.tsx": $36, + "./routes/island_instance/injected.tsx": $37, + "./routes/island_instance/partial.tsx": $38, + "./routes/island_instance/partial_remove.tsx": $39, + "./routes/island_instance/partial_replace.tsx": $40, + "./routes/island_instance_multiple/index.tsx": $41, + "./routes/island_instance_multiple/injected.tsx": $42, + "./routes/island_instance_multiple/partial.tsx": $43, + "./routes/island_instance_multiple/partial_both.tsx": $44, + "./routes/island_instance_nested/index.tsx": $45, + "./routes/island_instance_nested/injected.tsx": $46, + "./routes/island_instance_nested/partial.tsx": $47, + "./routes/island_instance_nested/replace.tsx": $48, + "./routes/island_props/index.tsx": $49, + "./routes/island_props/injected.tsx": $50, + "./routes/island_props/partial.tsx": $51, + "./routes/island_props_signals/index.tsx": $52, + "./routes/island_props_signals/injected.tsx": $53, + "./routes/island_props_signals/partial.tsx": $54, + "./routes/keys/index.tsx": $55, + "./routes/keys/injected.tsx": $56, + "./routes/keys/swap.tsx": $57, + "./routes/keys_components/index.tsx": $58, + "./routes/keys_components/injected.tsx": $59, + "./routes/keys_components/swap.tsx": $60, + "./routes/keys_dom/index.tsx": $61, + "./routes/keys_dom/injected.tsx": $62, + "./routes/keys_dom/swap.tsx": $63, + "./routes/keys_outside/index.tsx": $64, + "./routes/loading/index.tsx": $65, + "./routes/loading/injected.tsx": $66, + "./routes/loading/update.tsx": $67, + "./routes/missing_partial/index.tsx": $68, + "./routes/missing_partial/injected.tsx": $69, + "./routes/missing_partial/update.tsx": $70, + "./routes/mode/append.tsx": $71, + "./routes/mode/index.tsx": $72, + "./routes/mode/injected.tsx": $73, + "./routes/mode/prepend.tsx": $74, + "./routes/mode/replace.tsx": $75, + "./routes/no_islands/index.tsx": $76, + "./routes/no_islands/injected.tsx": $77, + "./routes/no_islands/update.tsx": $78, + "./routes/no_partial_response/index.tsx": $79, + "./routes/no_partial_response/injected.tsx": $80, + "./routes/no_partial_response/update.tsx": $81, + "./routes/partial_slot_inside_island.tsx": $82, + "./routes/scroll_restoration/index.tsx": $83, + "./routes/scroll_restoration/injected.tsx": $84, + "./routes/scroll_restoration/update.tsx": $85, }, islands: { "./islands/Counter.tsx": $$0, diff --git a/tests/fixture_partials/routes/button/index.tsx b/tests/fixture_partials/routes/button/index.tsx new file mode 100644 index 00000000000..667bcc1be2f --- /dev/null +++ b/tests/fixture_partials/routes/button/index.tsx @@ -0,0 +1,19 @@ +import { Partial } from "$fresh/runtime.ts"; +import { Fader } from "../../islands/Fader.tsx"; + +export default function ModeDemo() { + return ( +
+ + +

Initial content

+
+
+

+ +

+
+ ); +} diff --git a/tests/fixture_partials/routes/button/update.tsx b/tests/fixture_partials/routes/button/update.tsx new file mode 100644 index 00000000000..34540680f92 --- /dev/null +++ b/tests/fixture_partials/routes/button/update.tsx @@ -0,0 +1,19 @@ +import { defineRoute } from "$fresh/src/server/defines.ts"; +import { RouteConfig } from "$fresh/server.ts"; +import { Partial } from "$fresh/runtime.ts"; +import { Fader } from "../../islands/Fader.tsx"; + +export const config: RouteConfig = { + skipAppWrapper: true, + skipInheritedLayouts: true, +}; + +export default defineRoute((req, ctx) => { + return ( + + +

update

+
+
+ ); +}); diff --git a/tests/partials_test.ts b/tests/partials_test.ts index 5b0ba4287d4..716aa97d284 100644 --- a/tests/partials_test.ts +++ b/tests/partials_test.ts @@ -1088,3 +1088,16 @@ Deno.test("does not merge duplicate content", async () => { }, ); }); + +Deno.test("applies f-partial on