diff --git a/.changeset/khaki-forks-learn.md b/.changeset/khaki-forks-learn.md new file mode 100644 index 000000000000..d7725c8bd66b --- /dev/null +++ b/.changeset/khaki-forks-learn.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: enhance remote form functions with schema support, `input` and `issues` properties diff --git a/.changeset/olive-dodos-flash.md b/.changeset/olive-dodos-flash.md new file mode 100644 index 000000000000..bbc41830987d --- /dev/null +++ b/.changeset/olive-dodos-flash.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +breaking: remote form functions get passed a parsed POJO instead of a `FormData` object now diff --git a/documentation/docs/20-core-concepts/60-remote-functions.md b/documentation/docs/20-core-concepts/60-remote-functions.md index 4af669e42ed1..479e789089fc 100644 --- a/documentation/docs/20-core-concepts/60-remote-functions.md +++ b/documentation/docs/20-core-concepts/60-remote-functions.md @@ -227,7 +227,7 @@ export const getWeather = query.batch(v.string(), async (cities) => { ## form -The `form` function makes it easy to write data to the server. It takes a callback that receives the current [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)... +The `form` function makes it easy to write data to the server. It takes a callback that receives `data` constructed from the submitted [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)... ```ts @@ -259,30 +259,28 @@ export const getPosts = query(async () => { /* ... */ }); export const getPost = query(v.string(), async (slug) => { /* ... */ }); -export const createPost = form(async (data) => { - // Check the user is logged in - const user = await auth.getUser(); - if (!user) error(401, 'Unauthorized'); - - const title = data.get('title'); - const content = data.get('content'); - - // Check the data is valid - if (typeof title !== 'string' || typeof content !== 'string') { - error(400, 'Title and content are required'); +export const createPost = form( + v.object({ + title: v.pipe(v.string(), v.nonEmpty()), + content:v.pipe(v.string(), v.nonEmpty()) + }), + async ({ title, content }) => { + // Check the user is logged in + const user = await auth.getUser(); + if (!user) error(401, 'Unauthorized'); + + const slug = title.toLowerCase().replace(/ /g, '-'); + + // Insert into the database + await db.sql` + INSERT INTO post (slug, title, content) + VALUES (${slug}, ${title}, ${content}) + `; + + // Redirect to the newly created page + redirect(303, `/blog/${slug}`); } - - const slug = title.toLowerCase().replace(/ /g, '-'); - - // Insert into the database - await db.sql` - INSERT INTO post (slug, title, content) - VALUES (${slug}, ${title}, ${content}) - `; - - // Redirect to the newly created page - redirect(303, `/blog/${slug}`); -}); +); ``` ...and returns an object that can be spread onto a `
``` -The form object contains `method` and `action` properties that allow it to work without JavaScript (i.e. it submits data and reloads the page). It also has an `onsubmit` handler that progressively enhances the form when JavaScript is available, submitting data *without* reloading the entire page. +As with `query`, if the callback uses the submitted `data`, it should be [validated](#query-Query-arguments) by passing a [Standard Schema](https://standardschema.dev) as the first argument to `form`. The one difference is to `query` is that the schema inputs must all be of type `string` or `File`, since that's all the original `FormData` provides. You can however coerce the value into a different type — how to do that depends on the validation library you use. + +```ts +/// file: src/routes/count.remote.js +import * as v from 'valibot'; +import { form } from '$app/server'; + +export const setCount = form( + v.object({ + // Valibot: + count: v.pipe(v.string(), v.transform((s) => Number(s)), v.number()), + // Zod: + // count: v.coerce.number() + }), + async ({ count }) => { + // ... + } +); +``` + +The `name` attributes on the form controls must correspond to the properties of the schema — `title` and `content` in this case. If you schema contains objects, use object notation: + +```svelte + + + +{#each jobs as job, idx} + + +{/each} +``` + +To indicate a repeated field, use a `[]` suffix: + +```svelte + + + +``` + +If you'd like type safety and autocomplete when setting `name` attributes, use the form object's `field` method: + +```svelte + +``` + +This will error during typechecking if `title` does not exist on your schema. + +The form object contains `method` and `action` properties that allow it to work without JavaScript (i.e. it submits data and reloads the page). It also has an [attachment](/docs/svelte/@attach) that progressively enhances the form when JavaScript is available, submitting data *without* reloading the entire page. + +### Validation + +If the submitted data doesn't pass the schema, the callback will not run. Instead, the form object's `issues` object will be populated: + +```svelte + +``` + +You don't need to wait until the form is submitted to validate the data — you can call `validate()` programmatically, for example in an `oninput` callback (which will validate the data on every keystroke) or an `onchange` callback: + +```svelte + +``` + +By default, issues will be ignored if they belong to form controls that haven't yet been interacted with. To validate _all_ inputs, call `validate({ includeUntouched: true })`. + +For client-side validation, you can specify a _preflight_ schema which will populate `issues` and prevent data being sent to the server if the data doesn't validate: + +```svelte + + +message.current: {message.current}
+ -{#await current_task then task}{task}{/await}
+{#await message then m} +await get_message(): {m}
+{/await} - -Form pending: {task_one.pending}
- +scoped.input.message: {scoped.input.message}
+scoped.pending: {scoped.pending}
+scoped.result: {scoped.result}
+ +{task_one.result}
-{task_two.result}
+enhanced.input.message: {enhanced.input.message}
+enhanced.pending: {enhanced.pending}
+enhanced.result: {enhanced.result}
- +number.current: {number.current}
+ + +{#await number then n} +await get_number(): {n}
+{/await} + +set_number.input.number: {set_number.input.number}
+set_number.pending: {set_number.pending}
+set_number.result: {set_number.result}
+ +enhanced.input.number: {enhanced.input.number}
+enhanced.pending: {enhanced.pending}
+enhanced.result: {enhanced.result}
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/preflight/form.remote.ts b/packages/kit/test/apps/basics/src/routes/remote/form/preflight/form.remote.ts new file mode 100644 index 000000000000..e4a1f2378587 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/remote/form/preflight/form.remote.ts @@ -0,0 +1,23 @@ +import { form, query } from '$app/server'; +import * as v from 'valibot'; + +let number = 0; + +export const get_number = query(() => { + return number; +}); + +export const set_number = form( + v.object({ + number: v.pipe( + v.string(), + v.regex(/^\d+$/), + v.transform((n) => +n), + v.minValue(10, 'too small') + ) + }), + async (data) => { + number = data.number; + get_number().refresh(); + } +); diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/underscore/+page.svelte b/packages/kit/test/apps/basics/src/routes/remote/form/underscore/+page.svelte new file mode 100644 index 000000000000..620a7d63baef --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/remote/form/underscore/+page.svelte @@ -0,0 +1,28 @@ + + + + +{JSON.stringify(register.issues, null, ' ')}
+
+
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/underscore/form.remote.ts b/packages/kit/test/apps/basics/src/routes/remote/form/underscore/form.remote.ts
new file mode 100644
index 000000000000..ed6f2bff3e8a
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/remote/form/underscore/form.remote.ts
@@ -0,0 +1,10 @@
+import { form } from '$app/server';
+import * as v from 'valibot';
+
+export const register = form(
+ v.object({
+ username: v.pipe(v.string(), v.minLength(8)),
+ _password: v.pipe(v.string(), v.minLength(8))
+ }),
+ async (data) => {}
+);
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/validate/+page.svelte b/packages/kit/test/apps/basics/src/routes/remote/form/validate/+page.svelte
new file mode 100644
index 000000000000..0876bdea0f22
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/remote/form/validate/+page.svelte
@@ -0,0 +1,25 @@
+
+
+
diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/validate/form.remote.ts b/packages/kit/test/apps/basics/src/routes/remote/form/validate/form.remote.ts
new file mode 100644
index 000000000000..c24ed501afc9
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/remote/form/validate/form.remote.ts
@@ -0,0 +1,12 @@
+import { form } from '$app/server';
+import * as v from 'valibot';
+
+export const my_form = form(
+ v.object({
+ foo: v.picklist(['a', 'b', 'c']),
+ bar: v.picklist(['d', 'e', 'f'])
+ }),
+ async (data) => {
+ console.log(data);
+ }
+);
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index b62dd253b5ed..9e69d0e25aef 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -1764,69 +1764,6 @@ test.describe('remote functions', () => {
await expect(page.locator('p')).toHaveText('post');
});
- test('form.enhance works', async ({ page }) => {
- await page.goto('/remote/form');
- await page.fill('#input-task-enhance', 'abort');
- await page.click('#submit-btn-enhance-one');
- await page.waitForTimeout(100); // allow Svelte to update in case this does submit after (which it shouldn't)
- await expect(page.locator('#form-result-1')).toHaveText('');
-
- await page.fill('#input-task-enhance', 'hi');
- await page.click('#submit-btn-enhance-one');
- await expect(page.locator('#form-result-1')).toHaveText('hi');
-
- await page.fill('#input-task-enhance', 'error');
- await page.click('#submit-btn-enhance-one');
- expect(await page.textContent('#message')).toBe(
- 'This is your custom error page saying: "Expected error"'
- );
- });
-
- test('form.buttonProps.enhance works', async ({ page }) => {
- await page.goto('/remote/form');
- await page.fill('#input-task-enhance', 'abort');
- await page.click('#submit-btn-enhance-two');
- await page.waitForTimeout(100); // allow Svelte to update in case this does submit after (which it shouldn't)
- await expect(page.locator('#form-result-2')).toHaveText('');
-
- await page.fill('#input-task-enhance', 'hi');
- await page.click('#submit-btn-enhance-two');
- await expect(page.locator('#form-result-2')).toHaveText('hi');
-
- await page.fill('#input-task-enhance', 'error');
- await page.click('#submit-btn-enhance-two');
- expect(await page.textContent('#message')).toBe(
- 'This is your custom error page saying: "Unexpected error (500 Internal Error)"'
- );
- });
-
- test('form.enhance with override works', async ({ page }) => {
- await page.goto('/remote/form');
- await page.fill('#input-task-override', 'override');
- page.click('#submit-btn-override-one');
- await expect(page.locator('#get-task')).toHaveText('override (overridden)');
- await expect(page.locator('#form-result-1')).toHaveText('override');
- await expect(page.locator('#get-task')).toHaveText('override');
- });
-
- test('form.buttonProps.enhance with override works', async ({ page }) => {
- await page.goto('/remote/form');
- await page.fill('#input-task-override', 'override');
- page.click('#submit-btn-override-one');
- await expect(page.locator('#get-task')).toHaveText('override (overridden)');
- await expect(page.locator('#form-result-1')).toHaveText('override');
- await expect(page.locator('#get-task')).toHaveText('override');
- });
-
- test('form.buttonProps.enhance works with nested elements (issue #14159)', async ({ page }) => {
- await page.goto('/remote/form');
- await page.fill('#input-task-nested', 'nested-test');
-
- // Click on the span inside the button to test the event.target vs event.currentTarget issue
- await page.click('#submit-btn-nested-span span');
- await expect(page.locator('#form-result-2')).toHaveText('nested-test');
- });
-
test('prerendered entries not called in prod', async ({ page }) => {
let request_count = 0;
page.on('request', (r) => (request_count += r.url().includes('/_app/remote') ? 1 : 0));
@@ -1926,33 +1863,6 @@ test.describe('remote functions', () => {
await expect(page.locator('#command-pending')).toHaveText('Command pending: 0');
});
- test('form pending state is tracked correctly', async ({ page }) => {
- await page.goto('/remote/form');
-
- // Initially no pending forms
- await expect(page.locator('#form-pending')).toHaveText('Form pending: 0');
- await expect(page.locator('#form-button-pending')).toHaveText('Button pending: 0');
-
- // Fill form with slow operation
- await page.fill('#input-task', 'deferred');
-
- // Submit form - this will hang until we resolve it
- await page.click('#submit-btn-one');
-
- // Check that pending has incremented to 1
- await expect(page.locator('#form-pending')).toHaveText('Form pending: 1');
-
- // Resolve the deferred form submission
- await page.click('#resolve-deferreds');
-
- // Wait for form submission to complete and verify results
- await expect(page.locator('#get-task')).toHaveText('deferred');
-
- // Verify pending count returns to 0
- await expect(page.locator('#form-pending')).toHaveText('Form pending: 0');
- await expect(page.locator('#form-button-pending')).toHaveText('Button pending: 0');
- });
-
// TODO once we have async SSR adjust the test and move this into test.js
test('query.batch works', async ({ page }) => {
await page.goto('/remote/batch');
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index 3703e45d9ace..da7aec216f71 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -1629,59 +1629,206 @@ test.describe('remote functions', () => {
await expect(page.locator('#redirected')).toHaveText('redirected');
});
- test('form works', async ({ page }) => {
+ test('form works', async ({ page, javaScriptEnabled }) => {
await page.goto('/remote/form');
- await page.fill('#input-task', 'hi');
- await page.click('#submit-btn-one');
- await expect(page.locator('#form-result-1')).toHaveText('hi');
- await expect(page.locator('#input-task')).toHaveValue('');
+
+ if (javaScriptEnabled) {
+ // TODO remove the `if` — once async SSR lands these assertions should always succeed
+ await expect(page.getByText('message.current:')).toHaveText('message.current: initial');
+ await expect(page.getByText('await get_message():')).toHaveText(
+ 'await get_message(): initial'
+ );
+ }
+
+ await page.fill('[data-unscoped] input', 'hello');
+ await page.getByText('set message').click();
+
+ if (javaScriptEnabled) {
+ await expect(page.getByText('set_message.pending:')).toHaveText('set_message.pending: 1');
+ await page.getByText('resolve deferreds').click();
+ await expect(page.getByText('set_message.pending:')).toHaveText('set_message.pending: 0');
+
+ await expect(page.getByText('message.current:')).toHaveText('message.current: hello');
+ await expect(page.getByText('await get_message():')).toHaveText('await get_message(): hello');
+ }
+
+ await expect(page.getByText('set_message.result')).toHaveText('set_message.result: hello');
+ await expect(page.locator('[data-unscoped] input')).toHaveValue('');
});
- test('form error works', async ({ page }) => {
+ test('form updates inputs live', async ({ page, javaScriptEnabled }) => {
await page.goto('/remote/form');
- await page.fill('#input-task', 'error');
- await page.click('#submit-btn-one');
- expect(await page.textContent('h1')).toBe('400');
- expect(await page.textContent('#message')).toBe(
- 'This is your custom error page saying: "Expected error"'
+
+ await page.fill('input', 'hello');
+
+ if (javaScriptEnabled) {
+ await expect(page.getByText('set_message.input.message:')).toHaveText(
+ 'set_message.input.message: hello'
+ );
+ }
+
+ await page.getByText('set message').click();
+
+ if (javaScriptEnabled) {
+ await page.getByText('resolve deferreds').click();
+ }
+
+ await expect(page.getByText('set_message.input.message:')).toHaveText(
+ 'set_message.input.message:'
);
});
- test('form redirect works', async ({ page }) => {
+ test('form reports validation issues', async ({ page }) => {
await page.goto('/remote/form');
- await page.fill('#input-task', 'redirect');
- await page.click('#submit-btn-one');
- expect(await page.textContent('#echo-result')).toBe('Hello world');
+
+ await page.fill('input', 'invalid');
+ await page.getByText('set message').click();
+
+ await page.getByText('message is invalid').waitFor();
});
- test('form.buttonProps works', async ({ page }) => {
+ test('form handles unexpected error', async ({ page }) => {
await page.goto('/remote/form');
- await page.fill('#input-task', 'hi');
- await page.click('#submit-btn-two');
- await expect(page.locator('#form-result-2')).toHaveText('hi');
+
+ await page.fill('input', 'unexpected error');
+ await page.getByText('set message').click();
+
+ await page
+ .getByText('This is your custom error page saying: "oops (500 Internal Error)"')
+ .waitFor();
});
- test('form.buttonProps error works', async ({ page }) => {
+ test('form handles expected error', async ({ page }) => {
await page.goto('/remote/form');
- await page.fill('#input-task', 'error');
- await page.click('#submit-btn-two');
- expect(await page.textContent('h1')).toBe('500');
- expect(await page.textContent('#message')).toBe(
- 'This is your custom error page saying: "Unexpected error (500 Internal Error)"'
+
+ await page.fill('input', 'expected error');
+ await page.getByText('set message').click();
+
+ await page.getByText('This is your custom error page saying: "oops"').waitFor();
+ });
+
+ test('form redirects', async ({ page }) => {
+ await page.goto('/remote/form');
+
+ await page.fill('input', 'redirect');
+ await page.getByText('set message').click();
+
+ await page.waitForURL('/remote');
+ });
+
+ test('form.buttonProps works', async ({ page, javaScriptEnabled }) => {
+ await page.goto('/remote/form');
+
+ await page.fill('[data-unscoped] input', 'backwards');
+ await page.getByText('set reverse message').click();
+
+ if (javaScriptEnabled) {
+ await page.getByText('message.current: sdrawkcab').waitFor();
+ await expect(page.getByText('await get_message():')).toHaveText(
+ 'await get_message(): sdrawkcab'
+ );
+ }
+
+ await expect(page.getByText('set_reverse_message.result')).toHaveText(
+ 'set_reverse_message.result: sdrawkcab'
);
});
- test('form.for(...) scopes form submission', async ({ page, javaScriptEnabled }) => {
+ test('form scoping with for(...) works', async ({ page, javaScriptEnabled }) => {
await page.goto('/remote/form');
- await page.click('#submit-btn-item-foo');
- await expect(page.locator('#form-result-foo')).toHaveText('foo');
- await expect(page.locator('#form-result-bar')).toHaveText('');
- await expect(page.locator('#form-result-1')).toHaveText('');
-
- await page.click('#submit-btn-item-2-foo');
- await expect(page.locator('#form-result-2-foo')).toHaveText('foo2');
- await expect(page.locator('#form-result-foo')).toHaveText(javaScriptEnabled ? 'foo' : '');
- await expect(page.locator('#form-result-2')).toHaveText('');
+
+ await page.fill('[data-scoped] input', 'hello');
+ await page.getByText('set scoped message').click();
+
+ if (javaScriptEnabled) {
+ await expect(page.getByText('scoped.pending:')).toHaveText('scoped.pending: 1');
+ await page.getByText('resolve deferreds').click();
+ await expect(page.getByText('scoped.pending:')).toHaveText('scoped.pending: 0');
+
+ await page.getByText('message.current: hello').waitFor();
+ await expect(page.getByText('await get_message():')).toHaveText('await get_message(): hello');
+ }
+
+ await expect(page.getByText('scoped.result')).toHaveText('scoped.result: hello');
+ await expect(page.locator('[data-scoped] input')).toHaveValue('');
+ });
+
+ test('form enhance(...) works', async ({ page, javaScriptEnabled }) => {
+ await page.goto('/remote/form');
+
+ await page.fill('[data-enhanced] input', 'hello');
+
+ // Click on the span inside the button to test the event.target vs event.currentTarget issue (#14159)
+ await page.locator('[data-enhanced] span').click();
+
+ if (javaScriptEnabled) {
+ await expect(page.getByText('enhanced.pending:')).toHaveText('enhanced.pending: 1');
+
+ await page.getByText('message.current: hello (override)').waitFor();
+
+ await page.getByText('resolve deferreds').click();
+ await expect(page.getByText('enhanced.pending:')).toHaveText('enhanced.pending: 0');
+ await expect(page.getByText('await get_message():')).toHaveText('await get_message(): hello');
+ }
+
+ await expect(page.getByText('enhanced.result')).toHaveText('enhanced.result: hello');
+ await expect(page.locator('[data-enhanced] input')).toHaveValue('');
+ });
+
+ test('form preflight works', async ({ page, javaScriptEnabled }) => {
+ if (!javaScriptEnabled) return;
+
+ await page.goto('/remote/form/preflight');
+
+ for (const enhanced of [true, false]) {
+ const input = page.locator(enhanced ? '[data-enhanced] input' : '[data-default] input');
+ const button = page.getByText(enhanced ? 'set enhanced number' : 'set number');
+
+ await input.fill('21');
+ await button.click();
+ await page.getByText('too big').waitFor();
+
+ await input.fill('9');
+ await button.click();
+ await page.getByText('too small').waitFor();
+
+ await input.fill('15');
+ await button.click();
+ await expect(page.getByText('number.current')).toHaveText('number.current: 15');
+ }
+ });
+
+ test('form validate works', async ({ page, javaScriptEnabled }) => {
+ if (!javaScriptEnabled) return;
+
+ await page.goto('/remote/form/validate');
+
+ const foo = page.locator('input[name="foo"]');
+ const bar = page.locator('input[name="bar"]');
+
+ await foo.fill('a');
+ await expect(page.locator('form')).not.toContainText('Invalid type: Expected');
+
+ await bar.fill('g');
+ await expect(page.locator('form')).toContainText(
+ 'Invalid type: Expected ("d" | "e") but received "g"'
+ );
+
+ await bar.fill('d');
+ await expect(page.locator('form')).not.toContainText('Invalid type: Expected');
+ });
+
+ test('form inputs excludes underscore-prefixed fields', async ({ page, javaScriptEnabled }) => {
+ if (javaScriptEnabled) return;
+
+ await page.goto('/remote/form/underscore');
+
+ await page.fill('input[name="username"]', 'abcdefg');
+ await page.fill('input[name="_password"]', 'pqrstuv');
+ await page.locator('button').click();
+
+ await expect(page.locator('input[name="username"]')).toHaveValue('abcdefg');
+ await expect(page.locator('input[name="_password"]')).toHaveValue('');
});
test('prerendered entries not called in prod', async ({ page, clicknav }) => {
diff --git a/packages/kit/test/apps/options-2/package.json b/packages/kit/test/apps/options-2/package.json
index 818c8a9dc163..83d0e4c62814 100644
--- a/packages/kit/test/apps/options-2/package.json
+++ b/packages/kit/test/apps/options-2/package.json
@@ -19,6 +19,7 @@
"svelte": "^5.38.5",
"svelte-check": "^4.1.1",
"typescript": "^5.5.4",
+ "valibot": "^1.1.0",
"vite": "catalog:"
},
"type": "module"
diff --git a/packages/kit/test/apps/options-2/src/routes/remote/count.remote.js b/packages/kit/test/apps/options-2/src/routes/remote/count.remote.js
index fc93f18dd9c2..52bd1f530250 100644
--- a/packages/kit/test/apps/options-2/src/routes/remote/count.remote.js
+++ b/packages/kit/test/apps/options-2/src/routes/remote/count.remote.js
@@ -1,5 +1,6 @@
import { building, dev } from '$app/environment';
import { command, form, prerender, query } from '$app/server';
+import * as v from 'valibot';
let count = 0;
@@ -21,7 +22,6 @@ export const prerendered = prerender(() => {
return 'yes';
});
-export const set_count_form = form(async (form_data) => {
- const c = /** @type {string} */ (form_data.get('count'));
- return (count = parseInt(c));
+export const set_count_form = form(v.object({ count: v.string() }), async (data) => {
+ return (count = parseInt(data.count));
});
diff --git a/packages/kit/test/types/remote.test.ts b/packages/kit/test/types/remote.test.ts
index 567b75d29f3f..2dea52594198 100644
--- a/packages/kit/test/types/remote.test.ts
+++ b/packages/kit/test/types/remote.test.ts
@@ -159,8 +159,8 @@ command_tests();
function form_tests() {
const q = query(() => '');
- const f = form((f) => {
- f.get('');
+ const f = form('unchecked', (data: { input: string }) => {
+ data.input;
return { success: true };
});
diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts
index 097c03cf9b38..960d072276a7 100644
--- a/packages/kit/types/index.d.ts
+++ b/packages/kit/types/index.d.ts
@@ -1682,28 +1682,105 @@ declare module '@sveltejs/kit' {
restore: (snapshot: T) => void;
}
+ // If T is unknown or RemoteFormInput, the types below will recurse indefinitely and create giant unions that TS can't handle
+ type WillRecurseIndefinitely