Skip to content

Commit 3290a0c

Browse files
Rich-Harrisgithub-actions[bot]teemingc
authored
feat: return boolean from submit to indicate submission validity (#15530)
With this PR, a `submit` in an enhanced remote `form` returns a boolean indicating whether it was valid or not: ```js try { if (await submit()) { // success (return or redirect) } else { // validation error } } catch (e) { // something went wrong } ``` The alternative approach we considered was to throw an error on invalid data. But that feels more cumbersome, since now you need to differentiate between different error types: ```js import { isHttpError } from '@sveltejs/kit'; // later try { await submit(); // success (return or redirect) } catch (e) { if (isHttpError(e, 400)) { // validation error } else { // something went wrong } } ``` --- ### Please don't delete this checklist! Before submitting the PR, please make sure you do the following: - [ ] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [x] This message body should clearly illustrate what problems it solves. - [x] Ideally, include a test that fails without this PR but passes with it. ### Tests - [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check` ### Changesets - [x] If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpm changeset` and following the prompts. Changesets that add features should be `minor` and those that fix bugs should be `patch`. Please prefix changeset messages with `feat:`, `fix:`, or `chore:`. ### Edits - [x] Please ensure that 'Allow edits from maintainers' is checked. PRs without this option may be closed. --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Tee Ming <[email protected]>
1 parent a2aa14c commit 3290a0c

8 files changed

Lines changed: 58 additions & 17 deletions

File tree

.changeset/twelve-taxis-move.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': minor
3+
---
4+
5+
feat: return boolean from `submit` to indicate submission validity for enhanced `form` remote functions

documentation/docs/20-core-concepts/60-remote-functions.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -713,10 +713,13 @@ We can customize what happens when the form is submitted with the `enhance` meth
713713

714714
<form {...createPost.enhance(async ({ form, data, submit }) => {
715715
try {
716-
await submit();
717-
form.reset();
716+
if (await submit()) {
717+
form.reset();
718718

719-
showToast('Successfully published!');
719+
showToast('Successfully published!');
720+
} else {
721+
showToast('Invalid data!');
722+
}
720723
} catch (error) {
721724
showToast('Oh no! Something went wrong');
722725
}

packages/kit/src/exports/public.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2075,10 +2075,10 @@ export type RemoteForm<Input extends RemoteFormInput | void, Output> = {
20752075
callback: (opts: {
20762076
form: HTMLFormElement;
20772077
data: Input;
2078-
submit: () => Promise<void> & {
2079-
updates: (...updates: RemoteQueryUpdate[]) => Promise<void>;
2078+
submit: () => Promise<boolean> & {
2079+
updates: (...updates: RemoteQueryUpdate[]) => Promise<boolean>;
20802080
};
2081-
}) => void | Promise<void>
2081+
}) => void
20822082
): {
20832083
method: 'POST';
20842084
action: string;

packages/kit/src/runtime/client/remote-functions/form.svelte.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ export function form(id) {
156156
}
157157

158158
try {
159+
// eslint-disable-next-line @typescript-eslint/await-thenable -- `callback` is typed as returning `void` to allow returning e.g. `Promise<boolean>`
159160
await callback({
160161
form,
161162
data,
@@ -172,7 +173,7 @@ export function form(id) {
172173

173174
/**
174175
* @param {FormData} data
175-
* @returns {Promise<any> & { updates: (...args: any[]) => any }}
176+
* @returns {Promise<boolean> & { updates: (...args: any[]) => Promise<boolean> }}
176177
*/
177178
function submit(data) {
178179
// Store a reference to the current instance and increment the usage count for the duration
@@ -193,7 +194,7 @@ export function form(id) {
193194
/** @type {Error | undefined} */
194195
let updates_error;
195196

196-
/** @type {Promise<any> & { updates: (...args: RemoteQueryUpdate[]) => Promise<any> }} */
197+
/** @type {Promise<boolean> & { updates: (...args: RemoteQueryUpdate[]) => Promise<boolean> }} */
197198
const promise = (async () => {
198199
try {
199200
await Promise.resolve();
@@ -230,21 +231,25 @@ export function form(id) {
230231

231232
if (form_result.type === 'result') {
232233
({ issues: raw_issues = [], result } = devalue.parse(form_result.result, app.decoders));
234+
const succeeded = raw_issues.length === 0;
233235

234-
if (!issues.$) {
236+
if (succeeded) {
235237
if (form_result.refreshes) {
236238
apply_refreshes(form_result.refreshes);
237239
} else {
238240
void invalidateAll();
239241
}
240242
}
243+
244+
return succeeded;
241245
} else if (form_result.type === 'redirect') {
242246
const stringified_refreshes = form_result.refreshes ?? '';
243247
if (stringified_refreshes) {
244248
apply_refreshes(stringified_refreshes);
245249
}
246250
// Use internal version to allow redirects to external URLs
247251
void _goto(form_result.location, { invalidateAll: !stringified_refreshes }, 0);
252+
return true;
248253
} else {
249254
throw new HttpError(form_result.status ?? 500, form_result.error);
250255
}
@@ -460,8 +465,8 @@ export function form(id) {
460465

461466
instance[createAttachmentKey()] = create_attachment(
462467
form_onsubmit(({ submit, form }) =>
463-
submit().then(() => {
464-
if (!issues.$) {
468+
submit().then((succeeded) => {
469+
if (succeeded) {
465470
form.reset();
466471
}
467472
})

packages/kit/test/apps/async/src/routes/remote/form/[test_name]/+page.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
88
const scoped = set_message.for(`scoped:${params.test_name}`);
99
const enhanced = set_message.for(`enhanced:${params.test_name}`);
10+
11+
let submit_result = $state('none');
1012
</script>
1113

1214
<p>message.current: {message.current}</p>
@@ -52,7 +54,9 @@
5254
<form
5355
data-enhanced
5456
{...enhanced.enhance(async ({ data, submit }) => {
55-
await submit().updates(message.withOverride(() => data.message + ' (override)'));
57+
submit_result = String(
58+
await submit().updates(message.withOverride(() => data.message + ' (override)'))
59+
);
5660
})}
5761
>
5862
{#if enhanced.fields.message.issues()}
@@ -67,6 +71,7 @@
6771
<p>enhanced.input.message: {enhanced.fields.message.value()}</p>
6872
<p>enhanced.pending: {enhanced.pending}</p>
6973
<p>enhanced.result: {enhanced.result}</p>
74+
<p>enhanced.submit_result: {submit_result}</p>
7075

7176
<hr />
7277

packages/kit/test/apps/async/test/test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,29 @@ test.describe('remote functions', () => {
284284
);
285285
});
286286

287+
test('form enhance submit returns boolean', async ({ page, javaScriptEnabled }) => {
288+
if (!javaScriptEnabled) return;
289+
290+
await page.goto('/remote/form/enhanced');
291+
292+
await expect(page.getByText('enhanced.submit_result:')).toHaveText(
293+
'enhanced.submit_result: none'
294+
);
295+
296+
await page.fill('[data-enhanced] input', 'hello');
297+
await page.locator('[data-enhanced] span').click();
298+
await page.getByText('resolve deferreds').click();
299+
await expect(page.getByText('enhanced.submit_result:')).toHaveText(
300+
'enhanced.submit_result: true'
301+
);
302+
303+
await page.fill('[data-enhanced] input', 'invalid');
304+
await page.locator('[data-enhanced] span').click();
305+
await expect(page.getByText('enhanced.submit_result:')).toHaveText(
306+
'enhanced.submit_result: false'
307+
);
308+
});
309+
287310
test('form preflight works', async ({ page, javaScriptEnabled }) => {
288311
if (!javaScriptEnabled) return;
289312

packages/kit/test/types/remote.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,9 @@ function form_tests() {
247247
f.result?.success === true;
248248

249249
f.enhance(async ({ submit }) => {
250-
const x: void = await submit();
250+
const x: boolean = await submit();
251251
x;
252-
const y: void = await submit().updates(
252+
const y: boolean = await submit().updates(
253253
q,
254254
q(),
255255
q().withOverride(() => '')

packages/kit/types/index.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2049,10 +2049,10 @@ declare module '@sveltejs/kit' {
20492049
callback: (opts: {
20502050
form: HTMLFormElement;
20512051
data: Input;
2052-
submit: () => Promise<void> & {
2053-
updates: (...updates: RemoteQueryUpdate[]) => Promise<void>;
2052+
submit: () => Promise<boolean> & {
2053+
updates: (...updates: RemoteQueryUpdate[]) => Promise<boolean>;
20542054
};
2055-
}) => void | Promise<void>
2055+
}) => void
20562056
): {
20572057
method: 'POST';
20582058
action: string;

0 commit comments

Comments
 (0)