diff --git a/.changeset/purple-hotels-prove.md b/.changeset/purple-hotels-prove.md new file mode 100644 index 000000000000..dabdad0f3ef7 --- /dev/null +++ b/.changeset/purple-hotels-prove.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: add `pending` property to forms and commands diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index 1982000eb2f9..c323bc0fd76d 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -1565,6 +1565,8 @@ export type RemoteForm = { for(key: string | number | boolean): Omit, 'for'>; /** The result of the form submission */ get result(): Result | undefined; + /** The number of pending submissions */ + get pending(): number; /** Spread this onto a ` @@ -51,6 +55,10 @@

{task_one.result}

{task_two.result}

+ + + + {#each ['foo', 'bar'] as item}
{task_one.for(item).result} diff --git a/packages/kit/test/apps/basics/src/routes/remote/form/form.remote.js b/packages/kit/test/apps/basics/src/routes/remote/form/form.remote.js index 213b525b6880..b28aa69addf7 100644 --- a/packages/kit/test/apps/basics/src/routes/remote/form/form.remote.js +++ b/packages/kit/test/apps/basics/src/routes/remote/form/form.remote.js @@ -2,11 +2,20 @@ import { form, query } from '$app/server'; import { error, redirect } from '@sveltejs/kit'; let task; +const deferreds = []; export const get_task = query(() => { return task; }); +export const resolve_deferreds = form(async () => { + for (const deferred of deferreds) { + deferred.resolve(); + } + deferreds.length = 0; + return 'resolved'; +}); + export const task_one = form(async (form_data) => { task = /** @type {string} */ (form_data.get('task')); @@ -16,7 +25,11 @@ export const task_one = form(async (form_data) => { if (task === 'redirect') { redirect(303, '/remote'); } - if (task === 'override') { + if (task === 'deferred') { + const deferred = Promise.withResolvers(); + deferreds.push(deferred); + await deferred.promise; + } else if (task === 'override') { await new Promise((resolve) => setTimeout(resolve, 500)); } @@ -29,7 +42,11 @@ export const task_two = form(async (form_data) => { if (task === 'error') { throw new Error('Unexpected error'); } - if (task === 'override') { + if (task === 'deferred') { + const deferred = Promise.withResolvers(); + deferreds.push(deferred); + await deferred.promise; + } else if (task === 'override') { await new Promise((resolve) => setTimeout(resolve, 500)); } diff --git a/packages/kit/test/apps/basics/src/routes/remote/query-command.remote.js b/packages/kit/test/apps/basics/src/routes/remote/query-command.remote.js index f1eb258b84e0..13d8ccbf9e74 100644 --- a/packages/kit/test/apps/basics/src/routes/remote/query-command.remote.js +++ b/packages/kit/test/apps/basics/src/routes/remote/query-command.remote.js @@ -4,16 +4,28 @@ export const echo = query('unchecked', (value) => value); export const add = query('unchecked', ({ a, b }) => a + b); let count = 0; +const deferreds = []; export const get_count = query(() => count); -export const set_count = command('unchecked', async ({ c, slow = false }) => { - if (slow) { +export const set_count = command('unchecked', async ({ c, slow = false, deferred = false }) => { + if (deferred) { + const deferred = Promise.withResolvers(); + deferreds.push(deferred); + await deferred.promise; + } else if (slow) { await new Promise((resolve) => setTimeout(resolve, 500)); } return (count = c); }); +export const resolve_deferreds = command(() => { + for (const deferred of deferreds) { + deferred.resolve(); + } + deferreds.length = 0; +}); + export const set_count_server = command('unchecked', async (c) => { count = c; await get_count().refresh(); diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 1c4db540aef4..cc92292a5b3e 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -1815,6 +1815,25 @@ test.describe('remote functions', () => { expect(request_count).toBe(2); }); + test('command tracks pending state', async ({ page }) => { + await page.goto('/remote'); + + // Initial pending should be 0 + await expect(page.locator('#command-pending')).toHaveText('Command pending: 0'); + + // Start a slow command - this will hang until we resolve it + await page.click('#command-deferred-btn'); + + // Check that pending has incremented to 1 + await expect(page.locator('#command-pending')).toHaveText('Command pending: 1'); + + // Resolve the deferred command + await page.click('#resolve-deferreds'); + + // Wait for the command to complete and pending to go back to 0 + await expect(page.locator('#command-pending')).toHaveText('Command pending: 0'); + }); + test('validation works', async ({ page }) => { await page.goto('/remote/validation'); await expect(page.locator('p')).toHaveText('pending'); @@ -1834,4 +1853,53 @@ test.describe('remote functions', () => { await page.click('button:nth-of-type(4)'); await expect(page.locator('p')).toHaveText('success'); }); + + test('command pending state is tracked correctly', async ({ page }) => { + await page.goto('/remote'); + + // Initially no pending commands + await expect(page.locator('#command-pending')).toHaveText('Command pending: 0'); + + // Start a slow command - this will hang until we resolve it + await page.click('#command-deferred-btn'); + + // Check that pending has incremented to 1 + await expect(page.locator('#command-pending')).toHaveText('Command pending: 1'); + + // Resolve the deferred command + await page.click('#resolve-deferreds'); + + // Wait for the command to complete and verify results + await expect(page.locator('#command-result')).toHaveText('7'); + + // Verify pending count returns to 0 + 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'); + }); }); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 5458c54e9715..9dbb552a51a0 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -1542,6 +1542,8 @@ declare module '@sveltejs/kit' { for(key: string | number | boolean): Omit, 'for'>; /** The result of the form submission */ get result(): Result | undefined; + /** The number of pending submissions */ + get pending(): number; /** Spread this onto a `