Skip to content

Commit abcd34c

Browse files
committed
WIP: Exception handling for events work, but need correct status.
1 parent 5639f14 commit abcd34c

File tree

5 files changed

+210
-55
lines changed

5 files changed

+210
-55
lines changed

src/lib/client/superForm.ts

Lines changed: 79 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -334,10 +334,7 @@ const initialForms = new WeakMap<
334334
>();
335335

336336
const defaultOnError = (event: { result: { error: unknown } }) => {
337-
console.warn(
338-
'Unhandled error caught by Superforms, use onError event to handle it:',
339-
event.result.error
340-
);
337+
throw event.result.error;
341338
};
342339

343340
const defaultFormOptions = {
@@ -660,6 +657,16 @@ export function superForm<
660657
return options.SPA === true || typeof options.SPA === 'object';
661658
}
662659

660+
function Form_errorStatus(defaultStatus?: number) {
661+
return (
662+
defaultStatus ||
663+
(typeof options.SPA === 'boolean' || typeof options.SPA === 'string'
664+
? undefined
665+
: options.SPA?.failStatus) ||
666+
500
667+
);
668+
}
669+
663670
async function Form_validate(
664671
opts: {
665672
adapter?: FormOptions<T, M>['validators'];
@@ -1633,11 +1640,7 @@ export function superForm<
16331640
function clientValidationResult(validation: SuperFormValidated<T, M, In>) {
16341641
const validationResult = { ...validation, posted: true };
16351642

1636-
const status = validationResult.valid
1637-
? 200
1638-
: ((typeof options.SPA === 'boolean' || typeof options.SPA === 'string'
1639-
? undefined
1640-
: options.SPA?.failStatus) ?? 400);
1643+
const status = validationResult.valid ? 200 : Form_errorStatus();
16411644

16421645
const data = { form: validationResult };
16431646

@@ -1665,6 +1668,46 @@ export function superForm<
16651668
}
16661669
}
16671670

1671+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1672+
async function triggerOnError(result: { type: 'error'; status?: number; error: any }) {
1673+
if (options.applyAction) {
1674+
if (options.onError == 'apply') {
1675+
await applyAction(result);
1676+
} else {
1677+
// Transform to failure, to avoid data loss
1678+
// Set the data to the error result, so it will be
1679+
// picked up in page.subscribe in superForm.
1680+
const failResult = {
1681+
type: 'failure',
1682+
status: Form_errorStatus(Math.floor(result.status ?? NaN)),
1683+
data: result
1684+
} as const;
1685+
await applyAction(failResult);
1686+
}
1687+
}
1688+
1689+
// Check if the error message should be replaced
1690+
if (options.onError !== 'apply') {
1691+
const data = { result, message: Message };
1692+
1693+
for (const onErrorEvent of formEvents.onError) {
1694+
if (
1695+
onErrorEvent !== 'apply' &&
1696+
(onErrorEvent != defaultOnError || !options.flashMessage?.onError)
1697+
) {
1698+
await onErrorEvent(data);
1699+
}
1700+
}
1701+
}
1702+
1703+
if (options.flashMessage && options.flashMessage.onError) {
1704+
await options.flashMessage.onError({
1705+
result,
1706+
flashMessage: options.flashMessage.module.getFlash(page)
1707+
});
1708+
}
1709+
}
1710+
16681711
function cancel(
16691712
opts: { resetTimers?: boolean } = {
16701713
resetTimers: true
@@ -1689,7 +1732,12 @@ export function superForm<
16891732
currentRequest = submit.controller;
16901733

16911734
for (const event of formEvents.onSubmit) {
1692-
await event(submit);
1735+
try {
1736+
await event(submit);
1737+
} catch (error) {
1738+
cancel();
1739+
triggerOnError({ type: 'error', error, status: Form_errorStatus() });
1740+
}
16931741
}
16941742
}
16951743

@@ -1821,7 +1869,7 @@ export function superForm<
18211869
? (event.result as ActionResult)
18221870
: {
18231871
type: 'error',
1824-
status: parseInt(String(event.result.status)) || 500,
1872+
status: Form_errorStatus(parseInt(String(event.result.status))),
18251873
error: event.result.error instanceof Error ? event.result.error : event.result
18261874
};
18271875

@@ -1843,15 +1891,27 @@ export function superForm<
18431891
cancel();
18441892
});
18451893

1894+
function setErrorResult(error: unknown, data: { result: ActionResult }) {
1895+
data.result = {
1896+
type: 'error',
1897+
error,
1898+
status: result.status && result.status >= 400 ? result.status : Form_errorStatus()
1899+
};
1900+
}
1901+
18461902
for (const event of formEvents.onResult) {
1847-
await event(data);
1903+
try {
1904+
await event(data);
1905+
} catch (error) {
1906+
setErrorResult(error, data);
1907+
}
18481908
}
18491909

18501910
// In case it was modified in the event
18511911
result = data.result;
18521912

18531913
if (!cancelled) {
1854-
if ((result.type === 'success' || result.type == 'failure') && result.data) {
1914+
if ((result.type === 'success' || result.type === 'failure') && result.data) {
18551915
const forms = Context_findValidationForms(result.data);
18561916
if (!forms.length) {
18571917
throw new SuperFormError(
@@ -1871,7 +1931,11 @@ export function superForm<
18711931
};
18721932

18731933
for (const event of formEvents.onUpdate) {
1874-
await event(data);
1934+
try {
1935+
await event(data);
1936+
} catch (error) {
1937+
setErrorResult(error, data);
1938+
}
18751939
}
18761940

18771941
// In case it was modified in the event
@@ -1907,46 +1971,7 @@ export function superForm<
19071971
await Form_updateFromActionResult(result);
19081972
}
19091973
} else {
1910-
// Error result
1911-
if (options.applyAction) {
1912-
if (options.onError == 'apply') {
1913-
await applyAction(result);
1914-
} else {
1915-
// Transform to failure, to avoid data loss
1916-
// Set the data to the error result, so it will be
1917-
// picked up in page.subscribe in superForm.
1918-
const failResult = {
1919-
type: 'failure',
1920-
status: Math.floor(result.status || 500),
1921-
data: result
1922-
} as const;
1923-
await applyAction(failResult);
1924-
}
1925-
}
1926-
1927-
// Check if the error message should be replaced
1928-
if (options.onError !== 'apply') {
1929-
const data = { result, message: Message };
1930-
1931-
for (const onErrorEvent of formEvents.onError) {
1932-
if (
1933-
onErrorEvent !== 'apply' &&
1934-
(onErrorEvent != defaultOnError || !options.flashMessage?.onError)
1935-
) {
1936-
await onErrorEvent(data);
1937-
}
1938-
}
1939-
}
1940-
}
1941-
1942-
// Trigger flash message event if there was an error
1943-
if (options.flashMessage) {
1944-
if (result.type == 'error' && options.flashMessage.onError) {
1945-
await options.flashMessage.onError({
1946-
result,
1947-
flashMessage: options.flashMessage.module.getFlash(page)
1948-
});
1949-
}
1974+
await triggerOnError(result);
19501975
}
19511976
}
19521977
}

src/routes/(v2)/v2/Navigation.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@
6363
'discriminated-union',
6464
'simple-tainted',
6565
'validity-objects',
66-
'issue-466'
66+
'issue-466',
67+
'spa-error'
6768
].sort();
6869
</script>
6970

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<script lang="ts">
2+
import { page } from '$app/stores';
3+
import { defaults, superForm } from '$lib/index.js';
4+
import SuperDebug from '$lib/index.js';
5+
import { arktype } from '$lib/adapters/arktype.js';
6+
import { schema } from './schema.js';
7+
import type { ActionResult } from '@sveltejs/kit';
8+
9+
const defaultData = { name: '', email: '' };
10+
const adapter = arktype(schema, { defaults: defaultData });
11+
const data = defaults(adapter);
12+
13+
let options = ['onSubmit', 'onResult', 'onUpdate'];
14+
let option = 'onSubmit';
15+
16+
let error: Extract<ActionResult, { type: 'error' }>;
17+
18+
const {
19+
form,
20+
message,
21+
enhance,
22+
submitting,
23+
options: formOptions
24+
} = superForm(data, {
25+
SPA: true,
26+
validators: arktype(schema, { defaults: defaultData }),
27+
async onSubmit() {
28+
formOptions.SPA = true;
29+
if (option == 'onSubmit') {
30+
throw new Error('onSubmit error');
31+
}
32+
},
33+
async onResult() {
34+
if (option == 'onResult') {
35+
formOptions.SPA = { failStatus: 501 };
36+
throw new Error('onResult error');
37+
}
38+
},
39+
async onUpdate() {
40+
if (option == 'onUpdate') {
41+
formOptions.SPA = { failStatus: 502 };
42+
throw new Error('onUpdate error');
43+
}
44+
},
45+
onError({ result }) {
46+
error = result;
47+
}
48+
});
49+
</script>
50+
51+
<SuperDebug data={{ $form, option }} />
52+
53+
<h3>SPA onError</h3>
54+
55+
{#if error}
56+
<h4>ERROR {error.status}: {error.error}</h4>
57+
{/if}
58+
59+
{#if $message}
60+
<!-- eslint-disable-next-line svelte/valid-compile -->
61+
<div class="status" class:error={$page.status >= 400} class:success={$page.status == 200}>
62+
{$message}
63+
</div>
64+
{/if}
65+
66+
<div>
67+
{#each options as opt}
68+
<input type="radio" bind:group={option} value={opt} /> {opt}<br />
69+
{/each}
70+
</div>
71+
72+
<form method="POST" use:enhance>
73+
<div class="submit">
74+
<button>Submit</button>
75+
{#if $submitting}Loading...{/if}
76+
</div>
77+
</form>
78+
79+
<hr />
80+
<p>
81+
💥 <a target="_blank" href="https://superforms.rocks">Created with Superforms for SvelteKit</a> 💥
82+
</p>
83+
84+
<style>
85+
.submit {
86+
display: flex;
87+
align-items: center;
88+
}
89+
90+
.submit button {
91+
margin: 0 0.5rem;
92+
}
93+
94+
.status {
95+
color: white;
96+
padding: 4px;
97+
padding-left: 8px;
98+
border-radius: 2px;
99+
font-weight: 500;
100+
}
101+
102+
.status.success {
103+
background-color: seagreen;
104+
}
105+
106+
.status.error {
107+
background-color: #ff2a02;
108+
}
109+
110+
a {
111+
text-decoration: underline;
112+
}
113+
114+
hr {
115+
margin-top: 4rem;
116+
}
117+
118+
form {
119+
padding-top: 1rem;
120+
padding-bottom: 1rem;
121+
}
122+
</style>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { type } from 'arktype';
2+
3+
export const schema = type({
4+
name: 'string>=2',
5+
email: 'string.email'
6+
});
Lines changed: 1 addition & 0 deletions
Loading

0 commit comments

Comments
 (0)