Skip to content

Commit 2a1d691

Browse files
committed
fix(qwik-router): submit() always rejects on abort; submitcompleted carries detail.aborted
The rejection closes failures-look-like-successes for SubmitEvent-based submissions (patch). submitcompleted now also fires on aborts with the error on detail.aborted (minor).
1 parent fb1415c commit 2a1d691

11 files changed

Lines changed: 55 additions & 12 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/router': patch
3+
---
4+
5+
`action.submit()`/`run()` now always rejects when the submission aborts (a thrown `error()` or unexpected server error) — previously a `SubmitEvent`-based submission resolved with an empty result, letting failures look like successes. `<Form>` handles the rejection internally.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/router': minor
3+
---
4+
5+
feat: `submitcompleted` now also fires when a `<Form>` submission aborts, with the error on `detail.aborted`.

e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/actions/abort/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ export default component$(() => {
3434
<Form
3535
action={action}
3636
onSubmitCompleted$={(ev) => {
37-
formDetail.value = `completed:${ev.detail.status}`;
37+
formDetail.value = ev.detail.aborted
38+
? `aborted:${ev.detail.aborted.status}`
39+
: `completed:${ev.detail.status}`;
3840
}}
3941
>
4042
<input type="hidden" name="boom" value="1" />

e2e/qwik-e2e/tests/qwikrouter/actions.e2e.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,9 @@ test.describe('action abort semantics (spa)', () => {
190190
await expect(page.locator('#abort-state')).toHaveText('false:undefined:undefined:false');
191191
});
192192

193-
test('submitcompleted does not fire when the submission aborts', async ({ page }) => {
193+
test('submitcompleted fires with detail.aborted when the submission aborts', async ({ page }) => {
194194
await page.locator('#abort-form-submit').click();
195+
await expect(page.locator('#abort-form-detail')).toHaveText('aborted:418');
195196
await expect(page.locator('#abort-state')).toHaveText('false:undefined:undefined:false');
196-
await expect(page.locator('#abort-form-detail')).toHaveText('none');
197197
});
198198
});

packages/docs/src/routes/api/qwik-router/api.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@
334334
}
335335
],
336336
"kind": "Interface",
337-
"content": "```typescript\nexport interface FormSubmitCompletedDetail<T, ERROR = unknown> \n```\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nerror\n\n\n</td><td>\n\n\n</td><td>\n\n[ServerError](#servererror-variable)<!-- -->&lt;ERROR&gt; \\| undefined\n\n\n</td><td>\n\nThe `ServerError` from a returned `fail()` or a failed validator.\n\n\n</td></tr>\n<tr><td>\n\nstatus\n\n\n</td><td>\n\n\n</td><td>\n\nnumber\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nvalue\n\n\n</td><td>\n\n\n</td><td>\n\nT \\| undefined\n\n\n</td><td>\n\nThe action's successful return value. `undefined` when the action failed or aborted.\n\n\n</td></tr>\n</tbody></table>",
337+
"content": "```typescript\nexport interface FormSubmitCompletedDetail<T, ERROR = unknown> \n```\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\naborted?\n\n\n</td><td>\n\n\n</td><td>\n\n[ServerError](#servererror-variable)\n\n\n</td><td>\n\n_(Optional)_ Set when the submission aborted (a thrown `error()` or an unexpected server error).\n\n\n</td></tr>\n<tr><td>\n\nerror\n\n\n</td><td>\n\n\n</td><td>\n\n[ServerError](#servererror-variable)<!-- -->&lt;ERROR&gt; \\| undefined\n\n\n</td><td>\n\nThe `ServerError` from a returned `fail()` or a failed validator.\n\n\n</td></tr>\n<tr><td>\n\nstatus\n\n\n</td><td>\n\n\n</td><td>\n\nnumber\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nvalue\n\n\n</td><td>\n\n\n</td><td>\n\nT \\| undefined\n\n\n</td><td>\n\nThe action's successful return value. `undefined` when the action failed or aborted.\n\n\n</td></tr>\n</tbody></table>",
338338
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/form-component.tsx",
339339
"mdFile": "router.formsubmitsuccessdetail.md"
340340
},

packages/docs/src/routes/api/qwik-router/index.mdx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,21 @@ Description
10101010
</th></tr></thead>
10111011
<tbody><tr><td>
10121012
1013+
aborted?
1014+
1015+
</td><td>
1016+
1017+
</td><td>
1018+
1019+
[ServerError](#servererror-variable)
1020+
1021+
</td><td>
1022+
1023+
_(Optional)_ Set when the submission aborted (a thrown `error()` or an unexpected server error).
1024+
1025+
</td></tr>
1026+
<tr><td>
1027+
10131028
error
10141029
10151030
</td><td>

packages/docs/src/routes/docs/(qwikrouter)/action/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ export const useDeletePost = routeAction$(
439439
);
440440
```
441441

442-
A thrown `error()` never lands on `action.error`: the action records no state, a programmatic `action.submit()` rejects with the error, and a `<Form>` submission resets without firing `submitcompleted` — use `fail()` for anything the UI should display. Plain (non-`error()`) throws behave like unexpected errors and result in a sanitized 500.
442+
A thrown `error()` never lands on `action.error`: the action records no state and `action.submit()` rejects with the error. `<Form>` handles the rejection internally and fires `submitcompleted` with `detail.aborted` set — use `fail()` for anything the UI should display. Plain (non-`error()`) throws behave like unexpected errors and result in a sanitized 500.
443443

444444
## Previous form state
445445

packages/qwik-router/src/runtime/src/form-component.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export interface FormSubmitCompletedDetail<T, ERROR = unknown> {
1010
value: T | undefined;
1111
/** The `ServerError` from a returned `fail()` or a failed validator. */
1212
error: ServerError<ERROR> | undefined;
13+
/** Set when the submission aborted (a thrown `error()` or an unexpected server error). */
14+
aborted?: ServerError;
1315
}
1416

1517
/** @public */
@@ -63,7 +65,8 @@ export const Form = <O, I>(
6365
!reloadDocument
6466
? $((evt: SubmitEvent) => {
6567
if (!action.submitted) {
66-
return action.submit(evt);
68+
// Swallow our own rejection — submitcompleted is the form's abort channel.
69+
return action.submit(evt).catch(() => {});
6770
}
6871
})
6972
: undefined,
@@ -84,7 +87,7 @@ export const Form = <O, I>(
8487
// Since v2, this fires before the action is executed so it can be prevented
8588
onSubmit$,
8689
// action.submit "submitcompleted" event for onSubmitCompleted$ events
87-
!reloadDocument ? action.submit : undefined,
90+
!reloadDocument ? $((evt: SubmitEvent) => action.submit(evt).catch(() => {})) : undefined,
8891
],
8992
method: 'post',
9093
['data-spa-reset']: spaReset ? 'true' : undefined,

packages/qwik-router/src/runtime/src/qwik-router.runtime.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ export interface FormProps<O, I> extends Omit<QwikJSX.IntrinsicElements['form'],
223223

224224
// @public (undocumented)
225225
export interface FormSubmitSuccessDetail<T, ERROR = unknown> {
226+
aborted?: ServerError;
226227
error: ServerError<ERROR> | undefined;
227228
// (undocumented)
228229
status: number;

packages/qwik-router/src/runtime/src/server-functions.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,20 @@ Action.run() can only be called on the browser, for example when a user clicks a
120120
state.isRunning = false;
121121
if (aborted) {
122122
if (form) {
123-
// The submission aborted — submitcompleted does not fire.
124-
return { status, value: undefined, error: undefined };
123+
const detail = {
124+
status,
125+
value: undefined,
126+
error: undefined,
127+
aborted,
128+
} satisfies FormSubmitCompletedDetail<unknown>;
129+
form.dispatchEvent(
130+
new CustomEvent('submitcompleted', {
131+
bubbles: false,
132+
cancelable: false,
133+
composed: false,
134+
detail: detail,
135+
})
136+
);
125137
}
126138
throw aborted;
127139
}

0 commit comments

Comments
 (0)