Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/stale-poets-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: handles form target attribute in remote form redirects
Original file line number Diff line number Diff line change
Expand Up @@ -298,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 === '_blank') {
return;
}

event.preventDefault();

const form_data = new FormData(form, event.submitter);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script>
import { redirectForm } from './form.remote.ts';
</script>

<form {...redirectForm.for('blank')} target="_blank" data-testid="form-blank">
<button type="submit">Submit blank</button>
</form>

<form {...redirectForm.for('same')} data-testid="form-same">
<button type="submit">Submit same</button>
</form>

<form {...redirectForm.for('input')} data-testid="form-input-blank">
<input type="submit" formtarget="_blank" value="Submit input blank" />
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>destination</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { form } from '$app/server';
import { redirect } from '@sveltejs/kit';
import { object, optional, string } from 'valibot';

export const redirectForm = form(
object({
id: optional(string())
}),
() => {
redirect(303, '/remote/form/redirect-target/destination');
}
);
46 changes: 46 additions & 0 deletions packages/kit/test/apps/async/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,52 @@ test.describe('remote functions', () => {
await page.waitForURL('/remote');
});

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 });

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 }) => {
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('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');

Expand Down
Loading