From 572e3637ac75b61cca9b6f3ffc98a9481ddf524e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Sat, 28 Mar 2026 09:51:55 +0100 Subject: [PATCH 1/3] fix: don't intercept forms without explicit f-partial inside f-client-nav (#3473) Forms inside f-client-nav were always intercepted as partial requests because the fallback chain for rawPartialUrl ended with el.action, which is always non-empty (defaults to the current URL). This caused normal forms without f-partial to be sent with ?fresh-partial=true, breaking forms that expect a full page re-render after POST. Now only forms (or submitters) with an explicit f-partial or formaction attribute are intercepted for partial navigation. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/fresh/src/runtime/client/partials.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/fresh/src/runtime/client/partials.ts b/packages/fresh/src/runtime/client/partials.ts index bd5d3b5665b..d190070abfb 100644 --- a/packages/fresh/src/runtime/client/partials.ts +++ b/packages/fresh/src/runtime/client/partials.ts @@ -231,12 +231,20 @@ document.addEventListener("submit", async (e) => { return; } + const hasExplicitPartial = e.submitter?.hasAttribute(PARTIAL_ATTR) || + e.submitter?.hasAttribute("formaction") || + el.hasAttribute(PARTIAL_ATTR); + const rawPartialUrl = e.submitter?.getAttribute(PARTIAL_ATTR) ?? e.submitter?.getAttribute("formaction") ?? el.getAttribute(PARTIAL_ATTR) ?? el.action; const rawActionUrl = e.submitter?.getAttribute("formaction") ?? el.action; - if (rawPartialUrl !== "") { + // Only intercept forms that explicitly opt in to partial navigation + // via f-partial or formaction. Without this check, every form inside + // f-client-nav would be intercepted because el.action is always + // non-empty (defaults to the current URL). + if (hasExplicitPartial && rawPartialUrl !== "") { e.preventDefault(); const partialUrl = new URL(rawPartialUrl, location.href); From 349ff3336af73441a8336230a9e119613365ccc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Sun, 29 Mar 2026 20:54:23 +0200 Subject: [PATCH 2/3] fix: also treat explicit action attribute as opt-in for partial interception Forms with an explicit `action` attribute inside `f-client-nav` should still be intercepted as partials. Only forms *without* any of `f-partial`, `formaction`, or `action` attributes are left alone. Add test for forms without action inside f-client-nav to verify they submit normally without partial interception. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/fresh/src/runtime/client/partials.ts | 9 +++-- packages/fresh/tests/partials_test.tsx | 40 +++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/packages/fresh/src/runtime/client/partials.ts b/packages/fresh/src/runtime/client/partials.ts index d190070abfb..31eb59c0fec 100644 --- a/packages/fresh/src/runtime/client/partials.ts +++ b/packages/fresh/src/runtime/client/partials.ts @@ -233,7 +233,8 @@ document.addEventListener("submit", async (e) => { const hasExplicitPartial = e.submitter?.hasAttribute(PARTIAL_ATTR) || e.submitter?.hasAttribute("formaction") || - el.hasAttribute(PARTIAL_ATTR); + el.hasAttribute(PARTIAL_ATTR) || + el.hasAttribute("action"); const rawPartialUrl = e.submitter?.getAttribute(PARTIAL_ATTR) ?? e.submitter?.getAttribute("formaction") ?? @@ -241,9 +242,9 @@ document.addEventListener("submit", async (e) => { const rawActionUrl = e.submitter?.getAttribute("formaction") ?? el.action; // Only intercept forms that explicitly opt in to partial navigation - // via f-partial or formaction. Without this check, every form inside - // f-client-nav would be intercepted because el.action is always - // non-empty (defaults to the current URL). + // via f-partial, formaction, or an explicit action attribute. Without + // this check, every form inside f-client-nav would be intercepted + // because el.action is always non-empty (defaults to the current URL). if (hasExplicitPartial && rawPartialUrl !== "") { e.preventDefault(); diff --git a/packages/fresh/tests/partials_test.tsx b/packages/fresh/tests/partials_test.tsx index aae8420931f..e78250b8eb4 100644 --- a/packages/fresh/tests/partials_test.tsx +++ b/packages/fresh/tests/partials_test.tsx @@ -1857,6 +1857,46 @@ Deno.test({ }, }); +Deno.test({ + name: "partials - form without action inside f-client-nav not intercepted", + fn: async () => { + const app = testApp() + .post("/", (ctx) => { + return ctx.render( + + + , + ); + }) + .get("/", (ctx) => { + return ctx.render( + +
+
+ + +

init

+
+ +
+
+
, + ); + }); + + await withBrowserApp(app, async (page, address) => { + await page.goto(address, { waitUntil: "load" }); + await page.locator(".init").wait(); + + await page.locator(".update").click(); + // Form should do a full page navigation, not a partial update + await page.locator(".submitted").wait(); + }); + }, +}); + Deno.test({ name: "partials - submit form redirect", fn: async () => { From 29683a0481f304ff0755e0850dc21f249466f579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Sun, 29 Mar 2026 21:02:43 +0200 Subject: [PATCH 3/3] fix: wait for navigation in form submission test The test for non-intercepted form submission was racing against the browser's full page navigation. Use Promise.all with waitForNavigation() to properly wait for the POST response before checking for the .submitted element. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/fresh/tests/partials_test.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/fresh/tests/partials_test.tsx b/packages/fresh/tests/partials_test.tsx index e78250b8eb4..03137f91deb 100644 --- a/packages/fresh/tests/partials_test.tsx +++ b/packages/fresh/tests/partials_test.tsx @@ -1890,8 +1890,11 @@ Deno.test({ await page.goto(address, { waitUntil: "load" }); await page.locator(".init").wait(); - await page.locator(".update").click(); // Form should do a full page navigation, not a partial update + await Promise.all([ + page.waitForNavigation(), + page.locator(".update").click(), + ]); await page.locator(".submitted").wait(); }); },