From 702c751178157c4b213e0756b0e0ec10e10c0aa6 Mon Sep 17 00:00:00 2001 From: Aiman Date: Sat, 28 Feb 2026 17:21:43 +0530 Subject: [PATCH 1/5] fix: handle target attribute on remote form redirects --- .../client/remote-functions/form.svelte.js | 7 +++- .../remote/form/redirect-target/+page.svelte | 11 +++++ .../redirect-target/destination/+page.svelte | 1 + .../form/redirect-target/form.remote.ts | 12 ++++++ packages/kit/test/apps/async/test/test.js | 41 +++++++++++++++++++ 5 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 packages/kit/test/apps/async/src/routes/remote/form/redirect-target/+page.svelte create mode 100644 packages/kit/test/apps/async/src/routes/remote/form/redirect-target/destination/+page.svelte create mode 100644 packages/kit/test/apps/async/src/routes/remote/form/redirect-target/form.remote.ts diff --git a/packages/kit/src/runtime/client/remote-functions/form.svelte.js b/packages/kit/src/runtime/client/remote-functions/form.svelte.js index cc192c00f546..bff8da5944ee 100644 --- a/packages/kit/src/runtime/client/remote-functions/form.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/form.svelte.js @@ -238,7 +238,12 @@ export function form(id) { refresh_queries(refreshes, updates); } // Use internal version to allow redirects to external URLs - void _goto(form_result.location, { invalidateAll }, 0); + const form_target = element?.getAttribute('target'); + if (form_target === '_blank' || form_target === '_new') { + window.open(form_result.location, form_target); + } else { + void _goto(form_result.location, { invalidateAll }, 0); + } } else { throw new HttpError(form_result.status ?? 500, form_result.error); } diff --git a/packages/kit/test/apps/async/src/routes/remote/form/redirect-target/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/form/redirect-target/+page.svelte new file mode 100644 index 000000000000..adb083163302 --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/form/redirect-target/+page.svelte @@ -0,0 +1,11 @@ + + +
+ +
+ +
+ +
diff --git a/packages/kit/test/apps/async/src/routes/remote/form/redirect-target/destination/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/form/redirect-target/destination/+page.svelte new file mode 100644 index 000000000000..3c832468812a --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/form/redirect-target/destination/+page.svelte @@ -0,0 +1 @@ +

destination

diff --git a/packages/kit/test/apps/async/src/routes/remote/form/redirect-target/form.remote.ts b/packages/kit/test/apps/async/src/routes/remote/form/redirect-target/form.remote.ts new file mode 100644 index 000000000000..e7bfdca2a3d4 --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/form/redirect-target/form.remote.ts @@ -0,0 +1,12 @@ +import { form } from '$app/server'; +import { redirect } from '@sveltejs/kit'; +import * as v from 'valibot'; + +export const redirectForm = form( + v.object({ + id: v.optional(v.string()) + }), + () => { + redirect(307, '/remote/form/redirect-target/destination'); + } +); diff --git a/packages/kit/test/apps/async/test/test.js b/packages/kit/test/apps/async/test/test.js index ca63821f51ee..101121f3ce45 100644 --- a/packages/kit/test/apps/async/test/test.js +++ b/packages/kit/test/apps/async/test/test.js @@ -154,6 +154,47 @@ test.describe('remote functions', () => { await page.waitForURL('/remote'); }); + test('remote form redirect opens in new tab when target=_blank', async ({ + page, + javaScriptEnabled + }) => { + test.skip(!javaScriptEnabled, 'remote forms require JavaScript'); + + await page.goto('/remote/form/redirect-target'); + + const popup_promise = page.waitForEvent('popup', { timeout: 5000 }); + + await page.locator('[data-testid="form-blank"] button').click(); + + const popup = await popup_promise; + await popup.waitForLoadState(); + + expect(popup.url()).toContain('/remote/form/redirect-target/destination'); + + expect(page.url()).toContain('/remote/form/redirect-target'); + expect(page.url()).not.toContain('/destination'); + }); + + test('remote form redirect navigates same tab without target=_blank', async ({ + page, + javaScriptEnabled + }) => { + test.skip(!javaScriptEnabled, 'remote forms require JavaScript'); + + await page.goto('/remote/form/redirect-target'); + + let popup_opened = false; + page.on('popup', () => { + popup_opened = true; + }); + + await page.locator('form:not([target]) button').click(); + await page.waitForURL('**/remote/form/redirect-target/destination'); + + expect(popup_opened).toBe(false); + expect(page.url()).toContain('/remote/form/redirect-target/destination'); + }); + test('form multiple submit buttons work', async ({ page, javaScriptEnabled }) => { await page.goto('/remote/form/multiple-submit'); From e0169e75cc2c25a199b30736b2f717c0c088c472 Mon Sep 17 00:00:00 2001 From: Aiman Date: Mon, 2 Mar 2026 21:40:21 +0530 Subject: [PATCH 2/5] added changeset --- .changeset/stale-poets-clap.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/stale-poets-clap.md diff --git a/.changeset/stale-poets-clap.md b/.changeset/stale-poets-clap.md new file mode 100644 index 000000000000..c9d67aeeee0f --- /dev/null +++ b/.changeset/stale-poets-clap.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': major +--- + +fix: handles form target attribute in remote form redirects From ee1a10bc133439ae033d67df49576d5a52dbe398 Mon Sep 17 00:00:00 2001 From: Aiman Date: Mon, 2 Mar 2026 21:46:58 +0530 Subject: [PATCH 3/5] Update .changeset/stale-poets-clap.md Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> --- .changeset/stale-poets-clap.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/stale-poets-clap.md b/.changeset/stale-poets-clap.md index c9d67aeeee0f..d3430c0337d4 100644 --- a/.changeset/stale-poets-clap.md +++ b/.changeset/stale-poets-clap.md @@ -1,5 +1,5 @@ --- -'@sveltejs/kit': major +'@sveltejs/kit': patch --- fix: handles form target attribute in remote form redirects From 5a1ef8487bbd298c44afac8f5a30de2ca8b20243 Mon Sep 17 00:00:00 2001 From: Aiman Date: Thu, 5 Mar 2026 23:58:34 +0530 Subject: [PATCH 4/5] improved window opening and added test for formtarget attr on input tag --- .../client/remote-functions/form.svelte.js | 15 ++++++---- .../remote/form/redirect-target/+page.svelte | 4 +++ .../form/redirect-target/form.remote.ts | 8 ++--- packages/kit/test/apps/async/test/test.js | 29 +++++++++++-------- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/packages/kit/src/runtime/client/remote-functions/form.svelte.js b/packages/kit/src/runtime/client/remote-functions/form.svelte.js index bff8da5944ee..41be3b883adf 100644 --- a/packages/kit/src/runtime/client/remote-functions/form.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/form.svelte.js @@ -238,12 +238,7 @@ export function form(id) { refresh_queries(refreshes, updates); } // Use internal version to allow redirects to external URLs - const form_target = element?.getAttribute('target'); - if (form_target === '_blank' || form_target === '_new') { - window.open(form_result.location, form_target); - } else { - void _goto(form_result.location, { invalidateAll }, 0); - } + void _goto(form_result.location, { invalidateAll }, 0); } else { throw new HttpError(form_result.status ?? 500, form_result.error); } @@ -303,6 +298,14 @@ export function form(id) { return; } + const target = event.submitter?.hasAttribute('formtarget') + ? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formTarget + : clone(form).target; + + if (target && target !== '_self') { + return; + } + event.preventDefault(); const form_data = new FormData(form, event.submitter); diff --git a/packages/kit/test/apps/async/src/routes/remote/form/redirect-target/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/form/redirect-target/+page.svelte index adb083163302..95de0b093475 100644 --- a/packages/kit/test/apps/async/src/routes/remote/form/redirect-target/+page.svelte +++ b/packages/kit/test/apps/async/src/routes/remote/form/redirect-target/+page.svelte @@ -9,3 +9,7 @@
+ +
+ +
diff --git a/packages/kit/test/apps/async/src/routes/remote/form/redirect-target/form.remote.ts b/packages/kit/test/apps/async/src/routes/remote/form/redirect-target/form.remote.ts index e7bfdca2a3d4..1977f00fd86c 100644 --- a/packages/kit/test/apps/async/src/routes/remote/form/redirect-target/form.remote.ts +++ b/packages/kit/test/apps/async/src/routes/remote/form/redirect-target/form.remote.ts @@ -1,12 +1,12 @@ import { form } from '$app/server'; import { redirect } from '@sveltejs/kit'; -import * as v from 'valibot'; +import { object, optional, string } from 'valibot'; export const redirectForm = form( - v.object({ - id: v.optional(v.string()) + object({ + id: optional(string()) }), () => { - redirect(307, '/remote/form/redirect-target/destination'); + redirect(303, '/remote/form/redirect-target/destination'); } ); diff --git a/packages/kit/test/apps/async/test/test.js b/packages/kit/test/apps/async/test/test.js index c490a4a1f7b3..c5af41c86d31 100644 --- a/packages/kit/test/apps/async/test/test.js +++ b/packages/kit/test/apps/async/test/test.js @@ -154,12 +154,7 @@ test.describe('remote functions', () => { await page.waitForURL('/remote'); }); - test('remote form redirect opens in new tab when target=_blank', async ({ - page, - javaScriptEnabled - }) => { - test.skip(!javaScriptEnabled, 'remote forms require JavaScript'); - + test('remote form redirect opens in new tab when target=_blank', async ({ page }) => { await page.goto('/remote/form/redirect-target'); const popup_promise = page.waitForEvent('popup', { timeout: 5000 }); @@ -175,12 +170,7 @@ test.describe('remote functions', () => { expect(page.url()).not.toContain('/destination'); }); - test('remote form redirect navigates same tab without target=_blank', async ({ - page, - javaScriptEnabled - }) => { - test.skip(!javaScriptEnabled, 'remote forms require JavaScript'); - + test('remote form redirect navigates same tab without target=_blank', async ({ page }) => { await page.goto('/remote/form/redirect-target'); let popup_opened = false; @@ -195,6 +185,21 @@ test.describe('remote functions', () => { expect(page.url()).toContain('/remote/form/redirect-target/destination'); }); + test('remote form redirect opens in new tab when formtarget=_blank on input', async ({ + page + }) => { + await page.goto('/remote/form/redirect-target'); + + const popup_promise = page.waitForEvent('popup', { timeout: 5000 }); + await page.locator('[data-testid="form-input-blank"] input').click(); + const popup = await popup_promise; + await popup.waitForLoadState(); + + expect(popup.url()).toContain('/remote/form/redirect-target/destination'); + expect(page.url()).toContain('/remote/form/redirect-target'); + expect(page.url()).not.toContain('/destination'); + }); + test('form multiple submit buttons work', async ({ page, javaScriptEnabled }) => { await page.goto('/remote/form/multiple-submit'); From 9bbef0e4a4815e3b1b8d9dd5412c191613c98e3a Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Fri, 6 Mar 2026 07:24:37 +0800 Subject: [PATCH 5/5] Apply suggestion from @teemingc --- packages/kit/src/runtime/client/remote-functions/form.svelte.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/client/remote-functions/form.svelte.js b/packages/kit/src/runtime/client/remote-functions/form.svelte.js index 41be3b883adf..090fd273f971 100644 --- a/packages/kit/src/runtime/client/remote-functions/form.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/form.svelte.js @@ -302,7 +302,7 @@ export function form(id) { ? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formTarget : clone(form).target; - if (target && target !== '_self') { + if (target === '_blank') { return; }