diff --git a/.changeset/blue-bears-roll.md b/.changeset/blue-bears-roll.md new file mode 100644 index 000000000000..e21b9642879b --- /dev/null +++ b/.changeset/blue-bears-roll.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": patch +--- + +fix: add `null` to `RemoteFormInput` type to allow nullable schema validators diff --git a/.changeset/slow-waves-clean.md b/.changeset/slow-waves-clean.md new file mode 100644 index 000000000000..093af5025849 --- /dev/null +++ b/.changeset/slow-waves-clean.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: `preflight` schemas apply correctly when chained before `for` diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 1bae4ca70b47..1f05380a007c 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -2090,7 +2090,7 @@ type RecursiveFormFields = RemoteFormFieldContainer & { type MaybeArray = T | T[]; export interface RemoteFormInput { - [key: string]: MaybeArray; + [key: string]: MaybeArray; } export interface RemoteFormIssue { 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 a74bbdfe3473..48381eae94c5 100644 --- a/packages/kit/src/runtime/client/remote-functions/form.svelte.js +++ b/packages/kit/src/runtime/client/remote-functions/form.svelte.js @@ -54,6 +54,9 @@ export function form(id) { /** @type {Map }>} */ const instances = new Map(); + /** @type {WeakMap} */ + const preflight_schemas = new WeakMap(); + /** @param {string | number | boolean} [key] */ function create_instance(key) { const action_id_without_key = id; @@ -588,6 +591,7 @@ export function form(id) { /** @type {RemoteForm['preflight']} */ value: (schema) => { preflight_schema = schema; + preflight_schemas.set(instance, schema); return instance; } }, @@ -680,6 +684,11 @@ export function form(id) { value: (key) => { const entry = instances.get(key) ?? { count: 0, instance: create_instance(key) }; + const caller_preflight = preflight_schemas.get(instance); + if (caller_preflight && !preflight_schemas.has(entry.instance)) { + entry.instance.preflight(caller_preflight); + } + try { $effect.pre(() => { return () => { diff --git a/packages/kit/test/apps/async/src/routes/remote/form/preflight-for/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/form/preflight-for/+page.svelte new file mode 100644 index 000000000000..3c4dc44de576 --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/form/preflight-for/+page.svelte @@ -0,0 +1,27 @@ + + +

value.current: {value.current}

+ +
+ {#each form.fields.value.issues() as issue} +

{issue.message}

+ {/each} + + + +
+ +

form.pending: {form.pending}

+

form.result: {form.result}

diff --git a/packages/kit/test/apps/async/src/routes/remote/form/preflight-for/form.remote.ts b/packages/kit/test/apps/async/src/routes/remote/form/preflight-for/form.remote.ts new file mode 100644 index 000000000000..50352d00dff3 --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/form/preflight-for/form.remote.ts @@ -0,0 +1,18 @@ +import { form, query } from '$app/server'; +import * as v from 'valibot'; + +let value = 0; + +export const get_value = query(() => { + return value; +}); + +export const set_value = form( + v.object({ + value: v.pipe(v.number(), v.maxValue(20, 'too big')) + }), + async (data) => { + value = data.value; + get_value().refresh(); + } +); diff --git a/packages/kit/test/apps/async/test/test.js b/packages/kit/test/apps/async/test/test.js index dec1bf6a6348..2d850da3eb14 100644 --- a/packages/kit/test/apps/async/test/test.js +++ b/packages/kit/test/apps/async/test/test.js @@ -382,6 +382,26 @@ test.describe('remote functions', () => { await expect(page.locator('[data-failing-issue]')).toHaveText('async check failed'); }); + test('form preflight before for ordering works', async ({ page, javaScriptEnabled }) => { + if (!javaScriptEnabled) return; + + await page.goto('/remote/form/preflight-for'); + + const form = page.locator('[data-preflight-for]'); + const input = form.locator('input'); + const button = form.locator('button'); + + // Preflight should catch oversized value + await input.fill('21'); + await button.click(); + await form.getByText('too big').waitFor(); + + // After fixing, submission should succeed + await input.fill('5'); + await button.click(); + await expect(page.getByText('value.current')).toHaveText('value.current: 5'); + }); + test('form preflight-only validation works', async ({ page, javaScriptEnabled }) => { if (!javaScriptEnabled) return; diff --git a/packages/kit/test/types/remote.test.ts b/packages/kit/test/types/remote.test.ts index 2d00abec55af..30a89eb09bd5 100644 --- a/packages/kit/test/types/remote.test.ts +++ b/packages/kit/test/types/remote.test.ts @@ -624,6 +624,27 @@ function form_tests() { f11_field2.propA; // @ts-expect-error f11_field2.propB; + + // schema with nullable values (e.g. Zod nullable()) + const f12 = form( + null as any as StandardSchemaV1<{ + nullable_string: string | null; + nested: { nullable_value: number | null }; + }>, + (data) => { + data.nullable_string === '' || data.nullable_string === null; + data.nested.nullable_value === 0 || data.nested.nullable_value === null; + // @ts-expect-error + data.nonexistent; + return { success: true }; + } + ); + // @ts-expect-error + f12.fields.as('text'); + f12.fields.nullable_string.issues(); + f12.fields.nullable_string.value(); + f12.fields.nested.nullable_value.issues(); + f12.fields.nested.nullable_value.value(); } form_tests(); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index cf5f41397366..0188a2e81773 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -2064,7 +2064,7 @@ declare module '@sveltejs/kit' { type MaybeArray = T | T[]; export interface RemoteFormInput { - [key: string]: MaybeArray; + [key: string]: MaybeArray; } export interface RemoteFormIssue {