Skip to content

Commit

Permalink
feat(Form): multi onSubmit$ handlers (#6241)
Browse files Browse the repository at this point in the history
* feat(Form): multi onSubmit$

* style(Form): fmt

* chore: api.update

* fix: multiple onSubmit

* feat(Form): "submitcompleted" event for action forms

* style(Form): comments

* revert: submitcompleted fires in action.submit

* Revert "fix: multiple onSubmit"

This reverts commit f60741d.

* refactor(Form): array onSubmit new API

* style: lint

* chore(Form): api.update

* refactor(Form): isArray once

* feat(action): submitted

* fix: test

* chore: update tests

* chore: correct test

* test: fix spa only tests

* chore: fix types

* fix: submitted

* Update index.tsx
  • Loading branch information
PatrickJS authored May 10, 2024
1 parent de50926 commit ac6ea5c
Show file tree
Hide file tree
Showing 10 changed files with 141 additions and 33 deletions.
4 changes: 2 additions & 2 deletions packages/docs/src/routes/api/qwik-city/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
}
],
"kind": "TypeAlias",
"content": "```typescript\nexport type ActionStore<RETURN, INPUT, OPTIONAL extends boolean = true> = {\n readonly actionPath: string;\n readonly isRunning: boolean;\n readonly status?: number;\n readonly formData: FormData | undefined;\n readonly value: RETURN | undefined;\n readonly submit: QRL<OPTIONAL extends true ? (form?: INPUT | FormData | SubmitEvent) => Promise<ActionReturn<RETURN>> : (form: INPUT | FormData | SubmitEvent) => Promise<ActionReturn<RETURN>>>;\n};\n```\n**References:** [ActionReturn](#actionreturn)",
"content": "```typescript\nexport type ActionStore<RETURN, INPUT, OPTIONAL extends boolean = true> = {\n readonly actionPath: string;\n readonly isRunning: boolean;\n readonly status?: number;\n readonly formData: FormData | undefined;\n readonly value: RETURN | undefined;\n readonly submit: QRL<OPTIONAL extends true ? (form?: INPUT | FormData | SubmitEvent) => Promise<ActionReturn<RETURN>> : (form: INPUT | FormData | SubmitEvent) => Promise<ActionReturn<RETURN>>>;\n readonly submitted: boolean;\n};\n```\n**References:** [ActionReturn](#actionreturn)",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/runtime/src/types.ts",
"mdFile": "qwik-city.actionstore.md"
},
Expand Down Expand Up @@ -250,7 +250,7 @@
}
],
"kind": "Interface",
"content": "```typescript\nexport interface FormProps<O, I> extends Omit<QwikJSX.IntrinsicElements['form'], 'action' | 'method'> \n```\n**Extends:** Omit&lt;QwikJSX.IntrinsicElements\\['form'\\], 'action' \\| 'method'&gt;\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\n[action?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[ActionStore](#actionstore)<!-- -->&lt;O, I, true \\| false&gt;\n\n\n</td><td>\n\n_(Optional)_ Reference to the action returned by `action()`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\n[key?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring \\| number \\| null\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[onSubmit$?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n(event: Event, form: HTMLFormElement) =&gt; ValueOrPromise&lt;void&gt;\n\n\n</td><td>\n\n_(Optional)_ Event handler executed right when the form is submitted.\n\n\n</td></tr>\n<tr><td>\n\n[onSubmitCompleted$?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n(event: CustomEvent&lt;[FormSubmitCompletedDetail](#formsubmitsuccessdetail)<!-- -->&lt;O&gt;&gt;, form: HTMLFormElement) =&gt; ValueOrPromise&lt;void&gt;\n\n\n</td><td>\n\n_(Optional)_ Event handler executed right after the action is executed successfully and returns some data.\n\n\n</td></tr>\n<tr><td>\n\n[reloadDocument?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_ When `true` the form submission will cause a full page reload, even if SPA mode is enabled and JS is available.\n\n\n</td></tr>\n<tr><td>\n\n[spaReset?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_ When `true` all the form inputs will be reset in SPA mode, just like happens in a full page form submission.\n\nDefaults to `false`\n\n\n</td></tr>\n</tbody></table>",
"content": "```typescript\nexport interface FormProps<O, I> extends Omit<QwikJSX.IntrinsicElements['form'], 'action' | 'method'> \n```\n**Extends:** Omit&lt;QwikJSX.IntrinsicElements\\['form'\\], 'action' \\| 'method'&gt;\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\n[action?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[ActionStore](#actionstore)<!-- -->&lt;O, I, true \\| false&gt;\n\n\n</td><td>\n\n_(Optional)_ Reference to the action returned by `action()`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\n[key?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring \\| number \\| null\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[onSubmit$?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nQRLEventHandlerMulti&lt;SubmitEvent, HTMLFormElement&gt; \\| undefined\n\n\n</td><td>\n\n_(Optional)_ Event handler executed right when the form is submitted.\n\n\n</td></tr>\n<tr><td>\n\n[onSubmitCompleted$?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nQRLEventHandlerMulti&lt;CustomEvent&lt;[FormSubmitCompletedDetail](#formsubmitsuccessdetail)<!-- -->&lt;O&gt;&gt;, HTMLFormElement&gt; \\| undefined\n\n\n</td><td>\n\n_(Optional)_ Event handler executed right after the action is executed successfully and returns some data.\n\n\n</td></tr>\n<tr><td>\n\n[reloadDocument?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_ When `true` the form submission will cause a full page reload, even if SPA mode is enabled and JS is available.\n\n\n</td></tr>\n<tr><td>\n\n[spaReset?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_ When `true` all the form inputs will be reset in SPA mode, just like happens in a full page form submission.\n\nDefaults to `false`\n\n\n</td></tr>\n</tbody></table>",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/runtime/src/form-component.tsx",
"mdFile": "qwik-city.formprops.md"
},
Expand Down
5 changes: 3 additions & 2 deletions packages/docs/src/routes/api/qwik-city/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export type ActionStore<RETURN, INPUT, OPTIONAL extends boolean = true> = {
? (form?: INPUT | FormData | SubmitEvent) => Promise<ActionReturn<RETURN>>
: (form: INPUT | FormData | SubmitEvent) => Promise<ActionReturn<RETURN>>
>;
readonly submitted: boolean;
};
```

Expand Down Expand Up @@ -1264,7 +1265,7 @@ _(Optional)_
</td><td>
(event: Event, form: HTMLFormElement) =&gt; ValueOrPromise&lt;void&gt;
QRLEventHandlerMulti&lt;SubmitEvent, HTMLFormElement&gt; \| undefined
</td><td>
Expand All @@ -1279,7 +1280,7 @@ _(Optional)_ Event handler executed right when the form is submitted.
</td><td>
(event: CustomEvent&lt;[FormSubmitCompletedDetail](#formsubmitsuccessdetail)&lt;O&gt;&gt;, form: HTMLFormElement) =&gt; ValueOrPromise&lt;void&gt;
QRLEventHandlerMulti&lt;CustomEvent&lt;[FormSubmitCompletedDetail](#formsubmitsuccessdetail)&lt;O&gt;&gt;, HTMLFormElement&gt; \| undefined
</td><td>
Expand Down
4 changes: 2 additions & 2 deletions packages/docs/src/routes/api/qwik/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -1704,7 +1704,7 @@
}
],
"kind": "Function",
"content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\nLoad the prefetch graph for the container.\n\nEach Qwik container needs to include its own prefetch graph.\n\n\n```typescript\nPrefetchGraph: (opts?: {\n base?: string;\n manifestHash?: string;\n manifestURL?: string;\n}) => import(\"@builder.io/qwik/jsx-runtime\").JSXNode<\"script\">\n```\n\n\n<table><thead><tr><th>\n\nParameter\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\nopts\n\n\n</td><td>\n\n{ base?: string; manifestHash?: string; manifestURL?: string; }\n\n\n</td><td>\n\n_(Optional)_ Options for the loading prefetch graph.\n\n- `base` - Base of the graph. For a default installation this will default to `/build/`<!-- -->. But if more than one MFE is installed on the page, then each MFE needs to have its own base. - `manifestHash` - Hash of the manifest file to load. If not provided the hash will be extracted from the container attribute `q:manifest-hash` and assume the default build file `${base}/q-bundle-graph-${manifestHash}.json`<!-- -->. - `manifestURL` - URL of the manifest file to load if non-standard bundle graph location name.\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nimport(\"@builder.io/qwik/jsx-runtime\").JSXNode&lt;\"script\"&gt;",
"content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\nLoad the prefetch graph for the container.\n\nEach Qwik container needs to include its own prefetch graph.\n\n\n```typescript\nPrefetchGraph: (opts?: {\n base?: string;\n manifestHash?: string;\n manifestURL?: string;\n}) => import(\"@builder.io/qwik/jsx-runtime\").JSXNode<\"script\">\n```\n\n\n<table><thead><tr><th>\n\nParameter\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\nopts\n\n\n</td><td>\n\n{ base?: string; manifestHash?: string; manifestURL?: string; }\n\n\n</td><td>\n\n_(Optional)_ Options for the loading prefetch graph.\n\n- `base` - Base of the graph. For a default installation this will default to `/build/`<!-- -->. But if more than one MFE is installed on the page, then each MFE needs to have its own base. - `manifestHash` - Hash of the manifest file to load. If not provided the hash will be extracted from the container attribute `q:manifest-hash` and assume the default build file `${base}/q-bundle-graph-${manifestHash}.json`<!-- -->. - `manifestURL` - URL of the manifest file to load if non-standard bundle graph location name.\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nimport(\"@builder.io/qwik/jsx-runtime\").[JSXNode](#jsxnode)<!-- -->&lt;\"script\"&gt;",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts",
"mdFile": "qwik.prefetchgraph.md"
},
Expand All @@ -1718,7 +1718,7 @@
}
],
"kind": "Function",
"content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\nInstall a service worker which will prefetch the bundles.\n\nThere can only be one service worker per page. Because there can be many separate Qwik Containers on the page each container needs to load its prefetch graph using `PrefetchGraph` component.\n\n\n```typescript\nPrefetchServiceWorker: (opts: {\n base?: string;\n path?: string;\n verbose?: boolean;\n fetchBundleGraph?: boolean;\n}) => import(\"@builder.io/qwik/jsx-runtime\").JSXNode<\"script\">\n```\n\n\n<table><thead><tr><th>\n\nParameter\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\nopts\n\n\n</td><td>\n\n{ base?: string; path?: string; verbose?: boolean; fetchBundleGraph?: boolean; }\n\n\n</td><td>\n\nOptions for the prefetch service worker.\n\n- `base` - Base URL for the service worker. - `path` - Path to the service worker.\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nimport(\"@builder.io/qwik/jsx-runtime\").JSXNode&lt;\"script\"&gt;",
"content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\nInstall a service worker which will prefetch the bundles.\n\nThere can only be one service worker per page. Because there can be many separate Qwik Containers on the page each container needs to load its prefetch graph using `PrefetchGraph` component.\n\n\n```typescript\nPrefetchServiceWorker: (opts: {\n base?: string;\n path?: string;\n verbose?: boolean;\n fetchBundleGraph?: boolean;\n}) => import(\"@builder.io/qwik/jsx-runtime\").JSXNode<\"script\">\n```\n\n\n<table><thead><tr><th>\n\nParameter\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\nopts\n\n\n</td><td>\n\n{ base?: string; path?: string; verbose?: boolean; fetchBundleGraph?: boolean; }\n\n\n</td><td>\n\nOptions for the prefetch service worker.\n\n- `base` - Base URL for the service worker. - `path` - Path to the service worker.\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nimport(\"@builder.io/qwik/jsx-runtime\").[JSXNode](#jsxnode)<!-- -->&lt;\"script\"&gt;",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts",
"mdFile": "qwik.prefetchserviceworker.md"
},
Expand Down
4 changes: 2 additions & 2 deletions packages/docs/src/routes/api/qwik/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3520,7 +3520,7 @@ _(Optional)_ Options for the loading prefetch graph.
</tbody></table>
**Returns:**
import("@builder.io/qwik/jsx-runtime").JSXNode&lt;"script"&gt;
import("@builder.io/qwik/jsx-runtime").[JSXNode](#jsxnode)&lt;"script"&gt;
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts)
Expand Down Expand Up @@ -3572,7 +3572,7 @@ Options for the prefetch service worker.
</tbody></table>
**Returns:**
import("@builder.io/qwik/jsx-runtime").JSXNode&lt;"script"&gt;
import("@builder.io/qwik/jsx-runtime").[JSXNode](#jsxnode)&lt;"script"&gt;
[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/components/prefetch.ts)
Expand Down
8 changes: 5 additions & 3 deletions packages/qwik-city/runtime/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { EnvGetter } from '@builder.io/qwik-city/middleware/request-handler
import { JSXNode } from '@builder.io/qwik';
import { JSXOutput } from '@builder.io/qwik';
import { QRL } from '@builder.io/qwik';
import { QRLEventHandlerMulti } from '@builder.io/qwik';
import { QwikIntrinsicElements } from '@builder.io/qwik';
import { QwikJSX } from '@builder.io/qwik';
import type { ReadonlySignal } from '@builder.io/qwik';
Expand All @@ -23,7 +24,7 @@ import { RequestEventCommon } from '@builder.io/qwik-city/middleware/request-han
import { RequestEventLoader } from '@builder.io/qwik-city/middleware/request-handler';
import { RequestHandler } from '@builder.io/qwik-city/middleware/request-handler';
import type { ResolveSyncValue } from '@builder.io/qwik-city/middleware/request-handler';
import { ValueOrPromise } from '@builder.io/qwik';
import type { ValueOrPromise } from '@builder.io/qwik';
import { z } from 'zod';
import type * as zod from 'zod';

Expand Down Expand Up @@ -68,6 +69,7 @@ export type ActionStore<RETURN, INPUT, OPTIONAL extends boolean = true> = {
readonly formData: FormData | undefined;
readonly value: RETURN | undefined;
readonly submit: QRL<OPTIONAL extends true ? (form?: INPUT | FormData | SubmitEvent) => Promise<ActionReturn<RETURN>> : (form: INPUT | FormData | SubmitEvent) => Promise<ActionReturn<RETURN>>>;
readonly submitted: boolean;
};

// @public (undocumented)
Expand Down Expand Up @@ -219,8 +221,8 @@ export interface FormProps<O, I> extends Omit<QwikJSX.IntrinsicElements['form'],
action?: ActionStore<O, I, true | false>;
// (undocumented)
key?: string | number | null;
onSubmit$?: (event: Event, form: HTMLFormElement) => ValueOrPromise<void>;
onSubmitCompleted$?: (event: CustomEvent<FormSubmitSuccessDetail<O>>, form: HTMLFormElement) => ValueOrPromise<void>;
onSubmit$?: QRLEventHandlerMulti<SubmitEvent, HTMLFormElement> | undefined;
onSubmitCompleted$?: QRLEventHandlerMulti<CustomEvent<FormSubmitSuccessDetail<O>>, HTMLFormElement> | undefined;
reloadDocument?: boolean;
spaReset?: boolean;
}
Expand Down
75 changes: 56 additions & 19 deletions packages/qwik-city/runtime/src/form-component.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {
jsx,
_wrapSignal,
type QwikJSX,
type ValueOrPromise,
component$,
Slot,
$,
type QwikJSX,
type QRLEventHandlerMulti,
} from '@builder.io/qwik';
import type { ActionStore } from './types';
import { useNavigate } from './use-functions';
Expand Down Expand Up @@ -36,13 +37,12 @@ export interface FormProps<O, I>
spaReset?: boolean;

/** Event handler executed right when the form is submitted. */
onSubmit$?: (event: Event, form: HTMLFormElement) => ValueOrPromise<void>;
onSubmit$?: QRLEventHandlerMulti<SubmitEvent, HTMLFormElement> | undefined;

/** Event handler executed right after the action is executed successfully and returns some data. */
onSubmitCompleted$?: (
event: CustomEvent<FormSubmitCompletedDetail<O>>,
form: HTMLFormElement
) => ValueOrPromise<void>;
onSubmitCompleted$?:
| QRLEventHandlerMulti<CustomEvent<FormSubmitCompletedDetail<O>>, HTMLFormElement>
| undefined;

key?: string | number | null;
}
Expand All @@ -53,13 +53,44 @@ export const Form = <O, I>(
key: string | null
) => {
if (action) {
const isArrayApi = Array.isArray(onSubmit$);
// if you pass an array you can choose where you want action.submit in it
if (isArrayApi) {
return jsx(
'form',
{
...rest,
action: action.actionPath,
'preventdefault:submit': !reloadDocument,
onSubmit$: [
...onSubmit$,
// action.submit "submitcompleted" event for onSubmitCompleted$ events
!reloadDocument
? $((evt: SubmitEvent) => {
if (!action.submitted) {
return action.submit(evt);
}
})
: undefined,
],
method: 'post',
['data-spa-reset']: spaReset ? 'true' : undefined,
},
key
);
}
return jsx(
'form',
{
...rest,
action: action.actionPath,
'preventdefault:submit': !reloadDocument,
onSubmit$: [!reloadDocument ? action.submit : undefined, onSubmit$],
onSubmit$: [
// action.submit "submitcompleted" event for onSubmitCompleted$ events
!reloadDocument ? action.submit : undefined,
// TODO: v2 breaking change this should fire before the action.submit
onSubmit$,
],
method: 'post',
['data-spa-reset']: spaReset ? 'true' : undefined,
},
Expand Down Expand Up @@ -87,15 +118,19 @@ export const GetForm = component$<FormProps<undefined, undefined>>(
preventdefault:submit={!reloadDocument}
data-spa-reset={spaReset ? 'true' : undefined}
{...rest}
onSubmit$={async (_, form) => {
const formData = new FormData(form);
const params = new URLSearchParams();
formData.forEach((value, key) => {
if (typeof value === 'string') {
params.append(key, value);
}
});
nav('?' + params.toString(), { type: 'form', forceReload: true }).then(() => {
onSubmit$={[
...(Array.isArray(onSubmit$) ? onSubmit$ : [onSubmit$]),
$(async (_evt, form) => {
const formData = new FormData(form);
const params = new URLSearchParams();
formData.forEach((value, key) => {
if (typeof value === 'string') {
params.append(key, value);
}
});
await nav('?' + params.toString(), { type: 'form', forceReload: true });
}),
$((_evt, form) => {
if (form.getAttribute('data-spa-reset') === 'true') {
form.reset();
}
Expand All @@ -109,8 +144,10 @@ export const GetForm = component$<FormProps<undefined, undefined>>(
},
})
);
});
}}
//
}),
// end of array
]}
>
<Slot />
</form>
Expand Down
2 changes: 2 additions & 0 deletions packages/qwik-city/runtime/src/server-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const routeActionQrl = ((
const currentAction = useAction();
const initialState: Editable<Partial<ActionStore<unknown, unknown>>> = {
actionPath: `?${QACTION_KEY}=${id}`,
submitted: false,
isRunning: false,
status: undefined,
value: undefined,
Expand Down Expand Up @@ -104,6 +105,7 @@ Action.run() can only be called on the browser, for example when a user clicks a
if (data instanceof FormData) {
state.formData = data;
}
state.submitted = true;
state.isRunning = true;
loc.isNavigating = true;
currentAction.value = {
Expand Down
2 changes: 2 additions & 0 deletions packages/qwik-city/runtime/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,8 @@ export type ActionStore<RETURN, INPUT, OPTIONAL extends boolean = true> = {
? (form?: INPUT | FormData | SubmitEvent) => Promise<ActionReturn<RETURN>>
: (form: INPUT | FormData | SubmitEvent) => Promise<ActionReturn<RETURN>>
>;
/** Is action.submit was submitted */
readonly submitted: boolean;
};

type Failed = {
Expand Down
Loading

0 comments on commit ac6ea5c

Please sign in to comment.