@void/react 0.21.9 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,7 +39,7 @@ Main runtime APIs live at `@void/react`:
39
39
  - `useParams()`
40
40
  - `useNavigation()`
41
41
  - `useShared()`
42
- - `useForm()`
42
+ - `Form` and `useForm()`
43
43
  - `useIslandForm()`
44
44
  - `action()`
45
45
 
@@ -110,7 +110,7 @@ export interface Props {
110
110
  inside the form for button pending state.
111
111
 
112
112
  ```tsx
113
- import { useForm } from '@void/react';
113
+ import { Form, useForm } from '@void/react';
114
114
  import { useFormStatus } from 'react-dom';
115
115
 
116
116
  function SubmitButton() {
@@ -122,16 +122,15 @@ export default function CreateUser() {
122
122
  const form = useForm('/users', { name: '', email: '' });
123
123
 
124
124
  return (
125
- <form action={form.post}>
125
+ <Form form={form}>
126
126
  <input
127
127
  name="name"
128
128
  value={form.data.name}
129
129
  onChange={(e) => form.setData('name', e.target.value)}
130
130
  />
131
131
  {form.errors.name && <span>{form.errors.name}</span>}
132
- {form.error && <p>{form.error.message}</p>}
133
132
  <SubmitButton />
134
- </form>
133
+ </Form>
135
134
  );
136
135
  }
137
136
  ```
@@ -217,6 +216,12 @@ import { defineHandler } from 'void';
217
216
  export const loader = defineHandler(() => ({ title: 'Home' }));
218
217
  ```
219
218
 
219
+ ## Form
220
+
221
+ Use `Form` with `useForm()` to display action and validation errors automatically. It preserves controlled input after a failed submission and announces errors accessibly. Structured usage-limit responses display the application's message and an optional retry time. Submissions are blocked while pending and before that deadline; inputs remain editable and requests are never retried automatically.
222
+
223
+ `method` selects `post` (the default), `put`, `patch`, or `delete`. Use `renderError` to customize action-error presentation (a snippet in Svelte). Native forms remain available when you manage error presentation yourself.
224
+
220
225
  ## Tests
221
226
 
222
227
  - `test/unit/link.test.ts`
@@ -0,0 +1,7 @@
1
+ import { createActionClient } from "void/pages-client";
2
+ //#region src/runtime/action.ts
3
+ const client = createActionClient();
4
+ const action = client.action;
5
+ const setActionRouter = client.setActionRouter;
6
+ //#endregion
7
+ export { setActionRouter as n, action as t };
@@ -1,7 +1,15 @@
1
- import { AnchorHTMLAttributes, ButtonHTMLAttributes, Context, JSX } from "react";
2
- import { ActionResult, NavigationState, VoidActionError, VoidRouter } from "void/pages-client";
1
+ import { AnchorHTMLAttributes, ButtonHTMLAttributes, Context, FormHTMLAttributes, JSX, ReactElement, ReactNode } from "react";
2
+ import { ActionClient, FormMethod, FormModel, NavigationState, VoidActionError, VoidRouter } from "void/pages-client";
3
3
  import { CloudContextVariables, InferProps } from "void";
4
- import { ActionFormOptions, ActionUrl, ResolveActionBody, ResolveActionParams } from "void/routes";
4
+ import { ActionFormOptions, ActionUrl, ResolveActionBody } from "void/routes";
5
+ //#region src/runtime/form.d.ts
6
+ type FormProps = Omit<FormHTMLAttributes<HTMLFormElement>, "action" | "onSubmit" | "method" | "onError"> & {
7
+ form: FormModel<[FormData]>;
8
+ method?: FormMethod;
9
+ renderError?: (error: VoidActionError) => ReactNode;
10
+ };
11
+ declare function Form({ form, method, renderError, children, ...props }: FormProps): ReactElement;
12
+ //#endregion
5
13
  //#region src/runtime/link.d.ts
6
14
  type PrefetchStrategy = "hover" | "click" | "mount" | "visible";
7
15
  type LinkMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -79,20 +87,8 @@ type IslandFormReturn<T extends Record<string, unknown>> = Readonly<{
79
87
  declare function useIslandForm<T extends Record<string, unknown>>(defaults: T): IslandFormReturn<T>;
80
88
  //#endregion
81
89
  //#region src/runtime/action.d.ts
82
- type ActionMethod = "POST" | "PUT" | "PATCH" | "DELETE";
83
- type HasParams<U extends string> = [ResolveActionParams<U>] extends [never] ? false : true;
84
- type ActionOptions<U extends string> = {
85
- data?: ResolveActionBody<U>;
86
- method?: ActionMethod;
87
- } & (HasParams<U> extends true ? {
88
- params: ResolveActionParams<U>;
89
- } : {
90
- params?: never;
91
- });
92
- /** Called during app initialization to store the router instance */
93
- declare function setActionRouter(router: VoidRouter): void;
94
- /** Programmatic one-shot page action call with Inertia page update */
95
- declare function action<U extends ActionUrl & string>(url: U, ...args: HasParams<U> extends true ? [options: ActionOptions<U>] : [options?: ActionOptions<U>]): Promise<ActionResult>;
90
+ declare const action: ActionClient["action"];
91
+ declare const setActionRouter: ActionClient["setActionRouter"];
96
92
  //#endregion
97
93
  //#region src/runtime/context.d.ts
98
94
  declare const SharedContext: Context<Record<string, unknown> | null>;
@@ -104,4 +100,4 @@ declare const NavigationContext: Context<NavigationState>;
104
100
  //#region src/runtime/index.d.ts
105
101
  type Deferred<T> = Promise<T>;
106
102
  //#endregion
107
- export { ParamsContext as a, action as c, useNavigation as d, useForm as f, Link as g, useRouter as h, NavigationContext as i, setActionRouter as l, useParams as m, InferProps as n, RouterContext as o, useShared as p, ErrorsContext as r, SharedContext as s, Deferred as t, useIslandForm as u };
103
+ export { Form as _, ParamsContext as a, action as c, useNavigation as d, useForm as f, Link as g, useRouter as h, NavigationContext as i, setActionRouter as l, useParams as m, InferProps as n, RouterContext as o, useShared as p, ErrorsContext as r, SharedContext as s, Deferred as t, useIslandForm as u };
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as ParamsContext, c as action, d as useNavigation, f as useForm, g as Link, h as useRouter, i as NavigationContext, l as setActionRouter, m as useParams, n as InferProps, o as RouterContext, p as useShared, r as ErrorsContext, s as SharedContext, t as Deferred, u as useIslandForm } from "./index-Cx2A1XEu.mjs";
2
- export { Deferred, ErrorsContext, type InferProps, Link, NavigationContext, ParamsContext, RouterContext, SharedContext, action, setActionRouter, useForm, useIslandForm, useNavigation, useParams, useRouter, useShared };
1
+ import { _ as Form, a as ParamsContext, c as action, d as useNavigation, f as useForm, g as Link, h as useRouter, i as NavigationContext, l as setActionRouter, m as useParams, n as InferProps, o as RouterContext, p as useShared, r as ErrorsContext, s as SharedContext, t as Deferred, u as useIslandForm } from "./index-D5M3zrVB.mjs";
2
+ export { Deferred, ErrorsContext, Form, type InferProps, Link, NavigationContext, ParamsContext, RouterContext, SharedContext, action, setActionRouter, useForm, useIslandForm, useNavigation, useParams, useRouter, useShared };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
+ import { a as useParams, c as Form, i as useShared, n as useNavigation, o as useRouter, r as useForm, s as Link, t as useIslandForm } from "./runtime-D_Wv52MZ.mjs";
1
2
  import { a as SharedContext, i as RouterContext, n as NavigationContext, r as ParamsContext, t as ErrorsContext } from "./context-B0GL7p5m.mjs";
2
- import { a as useParams, i as useShared, n as useNavigation, o as useRouter, r as useForm, s as Link, t as useIslandForm } from "./runtime-W1oiBA4P.mjs";
3
- import { n as setActionRouter, t as action } from "./action-Bq1iSG6m.mjs";
4
- export { ErrorsContext, Link, NavigationContext, ParamsContext, RouterContext, SharedContext, action, setActionRouter, useForm, useIslandForm, useNavigation, useParams, useRouter, useShared };
3
+ import { n as setActionRouter, t as action } from "./action-4EvQppia.mjs";
4
+ export { ErrorsContext, Form, Link, NavigationContext, ParamsContext, RouterContext, SharedContext, action, setActionRouter, useForm, useIslandForm, useNavigation, useParams, useRouter, useShared };
@@ -1,2 +1,2 @@
1
- import { a as ParamsContext, c as action, d as useNavigation, f as useForm, g as Link, h as useRouter, i as NavigationContext, l as setActionRouter, m as useParams, n as InferProps, o as RouterContext, p as useShared, r as ErrorsContext, s as SharedContext, t as Deferred, u as useIslandForm } from "../index-Cx2A1XEu.mjs";
2
- export { Deferred, ErrorsContext, type InferProps, Link, NavigationContext, ParamsContext, RouterContext, SharedContext, action, setActionRouter, useForm, useIslandForm, useNavigation, useParams, useRouter, useShared };
1
+ import { _ as Form, a as ParamsContext, c as action, d as useNavigation, f as useForm, g as Link, h as useRouter, i as NavigationContext, l as setActionRouter, m as useParams, n as InferProps, o as RouterContext, p as useShared, r as ErrorsContext, s as SharedContext, t as Deferred, u as useIslandForm } from "../index-D5M3zrVB.mjs";
2
+ export { Deferred, ErrorsContext, Form, type InferProps, Link, NavigationContext, ParamsContext, RouterContext, SharedContext, action, setActionRouter, useForm, useIslandForm, useNavigation, useParams, useRouter, useShared };
@@ -1,4 +1,4 @@
1
+ import { a as useParams, c as Form, i as useShared, n as useNavigation, o as useRouter, r as useForm, s as Link, t as useIslandForm } from "../runtime-D_Wv52MZ.mjs";
1
2
  import { a as SharedContext, i as RouterContext, n as NavigationContext, r as ParamsContext, t as ErrorsContext } from "../context-B0GL7p5m.mjs";
2
- import { a as useParams, i as useShared, n as useNavigation, o as useRouter, r as useForm, s as Link, t as useIslandForm } from "../runtime-W1oiBA4P.mjs";
3
- import { n as setActionRouter, t as action } from "../action-Bq1iSG6m.mjs";
4
- export { ErrorsContext, Link, NavigationContext, ParamsContext, RouterContext, SharedContext, action, setActionRouter, useForm, useIslandForm, useNavigation, useParams, useRouter, useShared };
3
+ import { n as setActionRouter, t as action } from "../action-4EvQppia.mjs";
4
+ export { ErrorsContext, Form, Link, NavigationContext, ParamsContext, RouterContext, SharedContext, action, setActionRouter, useForm, useIslandForm, useNavigation, useParams, useRouter, useShared };
@@ -1,5 +1,5 @@
1
1
  import { a as SharedContext, i as RouterContext, n as NavigationContext, r as ParamsContext, t as ErrorsContext } from "../context-B0GL7p5m.mjs";
2
- import { n as setActionRouter } from "../action-Bq1iSG6m.mjs";
2
+ import { n as setActionRouter } from "../action-4EvQppia.mjs";
3
3
  import { n as DEFERRED_SHELL_END_ID } from "../deferred-shell-DOuP2YAo.mjs";
4
4
  import React, { Suspense, startTransition, use, useCallback, useEffect, useInsertionEffect, useLayoutEffect, useMemo, useReducer, useTransition } from "react";
5
5
  import { createPageNavigationEngine, createRouterFacade, idleNavigationState, isAbortError, isCallSiteActionError } from "void/pages-client";
@@ -1,8 +1,30 @@
1
1
  import { a as SharedContext, i as RouterContext, n as NavigationContext, r as ParamsContext } from "./context-B0GL7p5m.mjs";
2
- import "./action-Bq1iSG6m.mjs";
2
+ import "./action-4EvQppia.mjs";
3
3
  import { useActionState, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
4
- import { VoidActionError, categorizeActionError, isAbortError, isEqualFormValue, ssrProxy, submitAction } from "void/pages-client";
5
- import { jsx } from "react/jsx-runtime";
4
+ import { VoidActionError, categorizeActionError, formErrorMessage, formRetryMessage, formSubmissionBlocked, isAbortError, isEqualFormValue, scheduleFormRetry, ssrProxy, submitAction } from "void/pages-client";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/runtime/form.tsx
7
+ function Form({ form, method = "post", renderError, children, ...props }) {
8
+ const [now, setNow] = useState(Date.now);
9
+ useEffect(() => scheduleFormRetry(form.error, () => setNow(Date.now())), [form.error]);
10
+ const message = formErrorMessage(form);
11
+ const retry = formRetryMessage(form.error);
12
+ const blocked = formSubmissionBlocked(form, now);
13
+ return /* @__PURE__ */ jsxs("form", {
14
+ ...props,
15
+ action: form[method],
16
+ "aria-busy": form.pending,
17
+ onSubmit: (event) => {
18
+ if (blocked) event.preventDefault();
19
+ },
20
+ children: [message && /* @__PURE__ */ jsxs("div", {
21
+ role: "alert",
22
+ "data-void-form-error": true,
23
+ children: [form.error && renderError ? renderError(form.error) : message, retry && /* @__PURE__ */ jsx("p", { children: retry })]
24
+ }), children]
25
+ });
26
+ }
27
+ //#endregion
6
28
  //#region src/runtime/link.tsx
7
29
  function appendQueryValue(params, key, value) {
8
30
  if (value === null || value === void 0) return false;
@@ -497,4 +519,4 @@ function useIslandForm(defaults) {
497
519
  };
498
520
  }
499
521
  //#endregion
500
- export { useParams as a, useShared as i, useNavigation as n, useRouter as o, useForm as r, Link as s, useIslandForm as t };
522
+ export { useParams as a, Form as c, useShared as i, useNavigation as n, useRouter as o, useForm as r, Link as s, useIslandForm as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@void/react",
3
- "version": "0.21.9",
3
+ "version": "0.23.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/voidzero-dev/void.git",
@@ -35,21 +35,21 @@
35
35
  "@vitejs/plugin-react": "^6.1.1"
36
36
  },
37
37
  "devDependencies": {
38
- "@types/node": "^26.5.1",
38
+ "@types/node": "^26.6.3",
39
39
  "@types/react": "^19.3.0",
40
40
  "@types/react-dom": "^19.3.0",
41
41
  "pathslash": "^0.1.0",
42
42
  "react": "^19.3.0",
43
43
  "react-dom": "^19.3.0",
44
- "vite": "npm:@voidzero-dev/vite-plus-core@0.3.1",
45
- "vite-plus": "0.3.1",
46
- "void": "0.21.9"
44
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.0.0",
45
+ "vite-plus": "1.0.0",
46
+ "void": "0.23.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": "^19.0.0",
50
50
  "react-dom": "^19.0.0",
51
51
  "vite": "^8.0.0",
52
- "void": "0.21.9"
52
+ "void": "0.23.0"
53
53
  },
54
54
  "scripts": {
55
55
  "build": "vp pack",
@@ -1,29 +0,0 @@
1
- import { submitAction } from "void/pages-client";
2
- //#region src/runtime/action.ts
3
- let _router = null;
4
- /** Called during app initialization to store the router instance */
5
- function setActionRouter(router) {
6
- _router = router;
7
- }
8
- /** Programmatic one-shot page action call with Inertia page update */
9
- async function action(url, ...args) {
10
- if (!_router) throw new Error("action(): called before router initialization.");
11
- const options = args[0];
12
- const data = options?.data;
13
- const method = options?.method || "POST";
14
- let resolvedUrl = url;
15
- let actionQuery = "";
16
- const qIdx = resolvedUrl.indexOf("?");
17
- if (qIdx !== -1) {
18
- actionQuery = resolvedUrl.slice(qIdx);
19
- resolvedUrl = resolvedUrl.slice(0, qIdx);
20
- }
21
- if (options?.params) for (const [key, value] of Object.entries(options.params)) resolvedUrl = resolvedUrl.replace(`:${key}`, encodeURIComponent(value));
22
- resolvedUrl = resolvedUrl + actionQuery;
23
- return submitAction(_router, resolvedUrl, {
24
- method,
25
- data
26
- });
27
- }
28
- //#endregion
29
- export { setActionRouter as n, action as t };