react-form-action 2.10.0 → 2.12.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
@@ -102,7 +102,6 @@ export function SubscribeForm() {
102
102
 
103
103
  ```tsx
104
104
  // app/subscribe/page.tsx
105
-
106
105
  import { Action } from "react-form-action/client";
107
106
 
108
107
  import { subscribeAction } from "./action";
@@ -232,112 +231,6 @@ export default function Page({
232
231
  }
233
232
  ```
234
233
 
235
- ### Action Creator
236
-
237
- Low-level action creator, which provides the `success`, `failure` and `invalid` envelope constructors. With the `createFormAction` you must handle the native `FormData` by yourself.
238
-
239
- ```ts
240
- "use server";
241
-
242
- import { createFormAction } from "react-form-action";
243
- import { z } from "zod";
244
-
245
- // Define custom serializable error & success data types
246
- type ErrorData = {
247
- message: string;
248
- };
249
-
250
- type SuccessData = {
251
- message: string;
252
- };
253
-
254
- type ValiationError = {
255
- name?: string;
256
- };
257
-
258
- const updateUserSchema = z.object({ name: z.string() });
259
-
260
- export const updateUser = createFormAction<
261
- SuccessData,
262
- ErrorData,
263
- ValiationError
264
- >(({ success, failure, invalid }) =>
265
- // success and failure helper functions create wrappers for success & error data respectively
266
- async (prevState, formData) => {
267
- if (prevState.type === "initial") {
268
- // use the initialData passed to <Form /> here
269
- // prevState.data === "foobar"
270
- }
271
-
272
- try {
273
- const { name } = updateUserSchema.parse({
274
- name: formData.get("name"),
275
- });
276
-
277
- const user = await updateCurrentUser(name);
278
-
279
- if (user) {
280
- // {type: "success", data: "Your profile has been updated.", error: null, validationError: null}
281
- return success({
282
- message: "Your profile has been updated.",
283
- });
284
- } else {
285
- // {type: "error", data: null, error: { message: "No current user." }, validationError: null}
286
- return failure({ message: "No current user." });
287
- }
288
- } catch (error) {
289
- if (error instanceof ZodError) {
290
- // {type: "invalid", data: null, error: null, validationError: {name: "Invalid input"}}
291
- return invalid({
292
- name: error.issues[0]?.message ?? "Validation error",
293
- });
294
- }
295
-
296
- return failure({ message: "Failed to update user." });
297
- }
298
- },
299
- );
300
- ```
301
-
302
- The action creator supports arguments binding:
303
-
304
- ```ts
305
- export const updateUser = createFormAction(
306
- (
307
- { success, failure, invalid },
308
- userId: string /* Here you can specify multiple arguments */,
309
- ) =>
310
- async (prevState, formData) => {
311
- try {
312
- const { name } = updateUserSchema.parse({
313
- name: formData.get("name"),
314
- });
315
-
316
- const user = await db.users.findById(userId);
317
-
318
- if (!user) {
319
- return failure({ message: "No such user." });
320
- }
321
-
322
- const updated = await user.update({ name });
323
-
324
- if (updated) {
325
- return success({
326
- message: "User has been updated.",
327
- });
328
- } else {
329
- return failure({ message: "Failed to update." });
330
- }
331
- } catch (error) {
332
- // handle error
333
- }
334
- },
335
- );
336
-
337
- // call bind as usuall, the "123" becomes the "userId"
338
- updateUser.bind(null, "123");
339
- ```
340
-
341
234
  ### Action Context
342
235
 
343
236
  The `<Action>` components enables you to access your `action`'s state with the `useActionContext()` hook:
@@ -385,6 +278,63 @@ export function SignupForm() {
385
278
  }
386
279
  ```
387
280
 
281
+ #### Client-side validation
282
+
283
+ The `<Action validate={} />` prop enables you to tap into the `formData` before they are passed to the server action.
284
+
285
+ ```tsx
286
+ "use client"; // NOTE: The <Action> renders a context provider (a client component)
287
+
288
+ import type { PropsWithChildren } from "react";
289
+ import { Action } from "react-form-action/client";
290
+ import { z } from "zod";
291
+ import { zfd } from "zod-form-data";
292
+ import { signUp } from "./action";
293
+ import { signUpSchema } from "./schema";
294
+
295
+ export function SignUpAction({ children }: PropsWithChildren) {
296
+ return (
297
+ <Action
298
+ action={signUp}
299
+ initialData={undefined}
300
+ validate={(formData) => {
301
+ const result = zfd.formData(signUpSchema).safeParse(formData);
302
+
303
+ // NOTE: the return must be of the same type as your action's ValidationError type
304
+ if (result.error) {
305
+ return z.treeifyError(result.error);
306
+ }
307
+
308
+ return null;
309
+ }}
310
+ >
311
+ {children}
312
+ </Action>
313
+ );
314
+ }
315
+ ```
316
+
317
+ Use the `validateSchema` to avoid the zod-form-data boilerplate:
318
+
319
+ ```tsx
320
+ import { validateSchema } from "react-form-action";
321
+
322
+ import { signUpSchema } from "./schema";
323
+
324
+ export function SignUpAction({ children }: PropsWithChildren) {
325
+ return (
326
+ <Action
327
+ action={signUp}
328
+ initialData={undefined}
329
+ validate={validateSchema(signUpSchema)}
330
+ >
331
+ {children}
332
+ </Action>
333
+ );
334
+ }
335
+ ```
336
+
337
+
388
338
  ### `<Form>` Component
389
339
 
390
340
  The `<form>` submits the action in `onSubmit` handler to [prevent automatic form reset](https://github.com/facebook/react/issues/29034).
@@ -545,3 +495,111 @@ function SubmitButton() {
545
495
  );
546
496
  };
547
497
  ```
498
+
499
+
500
+
501
+ ### Action Creator
502
+
503
+ Low-level action creator, which provides the `success`, `failure` and `invalid` envelope constructors. With the `createFormAction` you must handle the native `FormData` by yourself.
504
+
505
+ ```ts
506
+ "use server";
507
+
508
+ import { createFormAction } from "react-form-action";
509
+ import { z } from "zod";
510
+
511
+ // Define custom serializable error & success data types
512
+ type ErrorData = {
513
+ message: string;
514
+ };
515
+
516
+ type SuccessData = {
517
+ message: string;
518
+ };
519
+
520
+ type ValiationError = {
521
+ name?: string;
522
+ };
523
+
524
+ const updateUserSchema = z.object({ name: z.string() });
525
+
526
+ export const updateUser = createFormAction<
527
+ SuccessData,
528
+ ErrorData,
529
+ ValiationError
530
+ >(({ success, failure, invalid }) =>
531
+ // success and failure helper functions create wrappers for success & error data respectively
532
+ async (prevState, formData) => {
533
+ if (prevState.type === "initial") {
534
+ // use the initialData passed to <Form /> here
535
+ // prevState.data === "foobar"
536
+ }
537
+
538
+ try {
539
+ const { name } = updateUserSchema.parse({
540
+ name: formData.get("name"),
541
+ });
542
+
543
+ const user = await updateCurrentUser(name);
544
+
545
+ if (user) {
546
+ // {type: "success", data: "Your profile has been updated.", error: null, validationError: null}
547
+ return success({
548
+ message: "Your profile has been updated.",
549
+ });
550
+ } else {
551
+ // {type: "error", data: null, error: { message: "No current user." }, validationError: null}
552
+ return failure({ message: "No current user." });
553
+ }
554
+ } catch (error) {
555
+ if (error instanceof ZodError) {
556
+ // {type: "invalid", data: null, error: null, validationError: {name: "Invalid input"}}
557
+ return invalid({
558
+ name: error.issues[0]?.message ?? "Validation error",
559
+ });
560
+ }
561
+
562
+ return failure({ message: "Failed to update user." });
563
+ }
564
+ },
565
+ );
566
+ ```
567
+
568
+ The action creator supports arguments binding:
569
+
570
+ ```ts
571
+ export const updateUser = createFormAction(
572
+ (
573
+ { success, failure, invalid },
574
+ userId: string /* Here you can specify multiple arguments */,
575
+ ) =>
576
+ async (prevState, formData) => {
577
+ try {
578
+ const { name } = updateUserSchema.parse({
579
+ name: formData.get("name"),
580
+ });
581
+
582
+ const user = await db.users.findById(userId);
583
+
584
+ if (!user) {
585
+ return failure({ message: "No such user." });
586
+ }
587
+
588
+ const updated = await user.update({ name });
589
+
590
+ if (updated) {
591
+ return success({
592
+ message: "User has been updated.",
593
+ });
594
+ } else {
595
+ return failure({ message: "Failed to update." });
596
+ }
597
+ } catch (error) {
598
+ // handle error
599
+ }
600
+ },
601
+ );
602
+
603
+ // call bind as usuall, the "123" becomes the "userId"
604
+ updateUser.bind(null, "123");
605
+ ```
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as InvalidState, i as InitialState, n as FailureState, o as SuccessState, r as FormAction, t as ActionState } from "./createFormAction-4p5Fvn2A.mjs";
1
+ import { a as FormAction, c as SuccessState, i as FailureState, o as InitialState, r as ActionState, s as InvalidState, t as Flatten } from "./formAction-0eiUHrXx.mjs";
2
2
  import React$1, { JSX, PropsWithChildren } from "react";
3
3
  import { $ZodErrorTree } from "zod/v4/core";
4
4
  //#region src/Action.d.ts
@@ -21,13 +21,13 @@ type ActionStatusFlags<T extends ActionState<unknown, unknown, unknown>["type"]
21
21
  isFailure: T extends "failure" ? true : false;
22
22
  isSuccess: T extends "success" ? true : false;
23
23
  };
24
- type ActionContextState<T extends ActionState<unknown, unknown, unknown>> = T & ActionStatusFlags<T["type"]> & {
24
+ type ActionContextState<T extends ActionState<unknown, unknown, unknown>> = Flatten<T & ActionStatusFlags<T["type"]> & {
25
25
  /**
26
26
  * The dispatch function returned from React.useActionState()
27
27
  */
28
28
  action: (payload: FormData) => void;
29
29
  isPending: boolean;
30
- };
30
+ }>;
31
31
  /**
32
32
  * NOTE: ActionContextState<ActionState<...>> would not allow discriminate the union inside of the ActionState.
33
33
  */
@@ -35,20 +35,8 @@ type SpreadActionContext<Data = unknown, Error = unknown, ValidationError = unkn
35
35
  /**
36
36
  * A hook to consume the form action state from the context.
37
37
  */
38
- declare function useActionContext<Data, Error, ValidationError, Args extends unknown[] = []>(action?: FormAction<Data, Error, ValidationError, FormData, Args>): SpreadActionContext<Data, Error, ValidationError>;
39
- declare function Action<Data, Error, ValidationError, Args extends unknown[] = []>({ children, action: formAction, initialData, args, permalink, validate }: ActionProps<Data, Error, ValidationError, Args>): React$1.JSX.Element;
40
- //#endregion
41
- //#region src/Form.d.ts
42
- type FormProps = Omit<JSX.IntrinsicElements["form"], "action"> & {
43
- /**
44
- * Opt-in into automatic form reset by using the form "action" prop.
45
- * By default, the onSubmit with a custom transition which opts-out of the implicit form reset.
46
- * See for more. https://github.com/facebook/react/issues/29034
47
- * @default false
48
- */
49
- autoReset?: boolean;
50
- };
51
- declare function Form({ autoReset, ...props }: FormProps): JSX.Element;
38
+ declare function useActionContext<Data, Error, ValidationError, Args extends unknown[] = []>(_action?: FormAction<Data, Error, ValidationError, FormData, Args>): SpreadActionContext<Data, Error, ValidationError>;
39
+ declare function Action<Data, Error, ValidationError, Args extends unknown[] = []>({ children, action: formAction, initialData, args, permalink, validate }: ActionProps<Data, Error, ValidationError, Args>): import("react").JSX.Element;
52
40
  //#endregion
53
41
  //#region src/ZodFieldError.d.ts
54
42
  declare const SEPARATOR: ".";
@@ -70,13 +58,17 @@ declare const noError: Readonly<{
70
58
  declare function ZodFieldError<Errors extends $ZodErrorTree<object>, Name extends "" | InferZodErrorPaths<Errors> = "">({ errors, name, children }: ZodFieldErrorProps<Errors, Name>): React$1.ReactNode;
71
59
  //#endregion
72
60
  //#region src/createComponents.d.ts
61
+ type Reducers<State = unknown> = Record<string, (state: State) => unknown>;
62
+ type InferFieldProps<T extends Reducers<any>> = { [K in keyof T]: ReturnType<T[K]>; };
73
63
  /**
74
64
  * Creates a typed components for actions created with the formAction builder.
75
65
  */
76
- declare function createComponents<Data, Error, ValidationError extends $ZodErrorTree<any>, Args extends unknown[] = []>(action: FormAction<Data, Error, ValidationError, FormData, Args>): {
66
+ declare function createComponents<Data, Error, ValidationError extends $ZodErrorTree<any>, Args extends unknown[] = [], FieldReducers extends Reducers<SpreadActionContext<Data, Error, ValidationError>> = Reducers<SpreadActionContext<Data, Error, ValidationError>>>(action: FormAction<Data, Error, ValidationError, FormData, Args>, options?: {
67
+ fieldProps?: FieldReducers;
68
+ }): {
77
69
  FieldError: <Name extends "" | InferZodErrorPaths<ValidationError>>({ name, children }: {
78
70
  name: Name;
79
- children?: (props: ZodFieldErrorChildrenProps<Name>) => React$1.ReactNode;
71
+ children?: (props: Flatten<ZodFieldErrorChildrenProps<Name> & InferFieldProps<FieldReducers>>) => React$1.ReactNode;
80
72
  }) => React$1.JSX.Element;
81
73
  Success: ({ children }: PropsWithChildren | {
82
74
  children?: (props: {
@@ -98,6 +90,18 @@ declare function createComponents<Data, Error, ValidationError extends $ZodError
98
90
  }) => React$1.ReactNode;
99
91
  };
100
92
  //#endregion
93
+ //#region src/Form.d.ts
94
+ type FormProps = Omit<JSX.IntrinsicElements["form"], "action"> & {
95
+ /**
96
+ * Opt-in into automatic form reset by using the form "action" prop.
97
+ * By default, the onSubmit with a custom transition which opts-out of the implicit form reset.
98
+ * See for more. https://github.com/facebook/react/issues/29034
99
+ * @default false
100
+ */
101
+ autoReset?: boolean;
102
+ };
103
+ declare function Form({ autoReset, ...props }: FormProps): JSX.Element;
104
+ //#endregion
101
105
  //#region src/Pending.d.ts
102
106
  /**
103
107
  * Conditionally renders the children, when the form action is in the "pending" state.
@@ -108,4 +112,4 @@ declare function Pending({ children }: PropsWithChildren | {
108
112
  }) => React.ReactNode;
109
113
  }): import("react").ReactNode;
110
114
  //#endregion
111
- export { Action, ActionContextState, ActionProps, Form, FormProps, InferZodErrorPaths, Paths, Pending, ZodFieldError, ZodFieldErrorChildrenProps, ZodFieldErrorProps, createComponents, noError, useActionContext };
115
+ export { Action, ActionContextState, ActionProps, Form, FormProps, InferZodErrorPaths, Paths, Pending, SpreadActionContext, ZodFieldError, ZodFieldErrorChildrenProps, ZodFieldErrorProps, createComponents, noError, useActionContext };
package/dist/client.mjs CHANGED
@@ -1 +1 @@
1
- "use client";import{n as e,t}from"./createFormAction-BwMYAzm2.mjs";import n,{createContext as r,startTransition as i,use as a,useActionState as o}from"react";const s={isInitial:!1,isInvalid:!1,isFailure:!1,isSuccess:!1},c=r(null);function l(e){let t=a(c);if(!t)throw Error(`ActionContext must be initialized before use. Is your useActionContext hook wrapped with an <Action> Component?`);return t}function u({children:r,action:i,initialData:a,args:l,permalink:u,validate:d}){let f=l?i.bind(null,...l):i,[p,m,h]=o(async(e,n)=>{if(d){let r=await t(({invalid:e,success:t})=>async(n,r)=>{let i=d(r);return i?e(i):t(!0)})(e,n);if(r.type===`invalid`)return r}return f(e,n)},e(a),u),g=p.type===`initial`?{...p,...s,action:m,isPending:h,isInitial:!0}:p.type===`invalid`?{...p,...s,action:m,isPending:h,isInvalid:!0}:p.type===`failure`?{...p,...s,action:m,isPending:h,isFailure:!0}:{...p,...s,action:m,isPending:h,isSuccess:!0};return n.createElement(c,{value:g},r)}function d({autoReset:e=!1,...t}){let{action:r}=l(),a=e?{action:r}:{onSubmit:e=>{e.preventDefault();let t=e.currentTarget;i(()=>{r(new FormData(t))})}};return n.createElement(`form`,{...a,...t})}const f=Object.freeze({errors:Object.freeze([])});function p({errors:e,name:t,children:r=({error:e})=>n.createElement(n.Fragment,null,e)}){if(t.length===0)return r({name:t,error:e.errors[0],errors:e.errors});let i=t.split(`.`),a=e;for(;i.length;){let e=i.shift();if(!a.properties){a=f;break}a=a.properties[e]??f}return r({name:t,error:a.errors[0],errors:a.errors})}function m(e){function t({name:t,children:r}){let{isInvalid:i,validationError:a}=l(e);return n.createElement(p,{errors:a??f,name:t},r??(({error:e})=>i&&n.createElement(n.Fragment,null,e)))}function r({children:t}){let{isSuccess:n,data:r}=l(e);return typeof t==`function`?t({isSuccess:n,data:r}):n&&t}function i({children:t}){let{isInvalid:n,validationError:r}=l(e);return typeof t==`function`?t({isInvalid:n,validationError:r}):n&&t}return{FieldError:t,Success:r,Invalid:i}}function h({children:e}){let{isPending:t}=l();return typeof e==`function`?e({isPending:t}):t&&e}export{u as Action,d as Form,h as Pending,p as ZodFieldError,m as createComponents,f as noError,l as useActionContext};
1
+ "use client";import{n as e,t}from"./createFormAction-BwMYAzm2.mjs";import{createContext as n,startTransition as r,use as i,useActionState as a}from"react";import{Fragment as o,jsx as s}from"react/jsx-runtime";const c={isInitial:!1,isInvalid:!1,isFailure:!1,isSuccess:!1},l=n(null);function u(e){let t=i(l);if(!t)throw Error(`ActionContext must be initialized before use. Is your useActionContext hook wrapped with an <Action> Component?`);return t}function d({children:n,action:r,initialData:i,args:o,permalink:u,validate:d}){let f=o?r.bind(null,...o):r,[p,m,h]=a(async(e,n)=>{if(d){let r=await t(({invalid:e,success:t})=>async(n,r)=>{let i=d(r);return i?e(i):t(!0)})(e,n);if(r.type===`invalid`)return r}return f(e,n)},e(i),u),g=p.type===`initial`?{...p,...c,action:m,isPending:h,isInitial:!0}:p.type===`invalid`?{...p,...c,action:m,isPending:h,isInvalid:!0}:p.type===`failure`?{...p,...c,action:m,isPending:h,isFailure:!0}:{...p,...c,action:m,isPending:h,isSuccess:!0};return s(l,{value:g,children:n})}const f=Object.freeze({errors:Object.freeze([])});function p({errors:e,name:t,children:n=({error:e})=>s(o,{children:e})}){if(t.length===0)return n({name:t,error:e.errors[0],errors:e.errors});let r=t.split(`.`),i=e;for(let e of r){if(!i.properties){i=f;break}i=i.properties[e]??f}return n({name:t,error:i.errors[0],errors:i.errors})}function m(e,t={}){function n({name:n,children:r}){let i=u(e),{isInvalid:a,validationError:c}=i,l=({error:e})=>a&&s(o,{children:e}),d=Object.fromEntries(Object.entries(t.fieldProps??{}).map(([e,t])=>[e,t(i)])),m=r??l;return s(p,{errors:c??f,name:n,children:e=>m({...e,...d})})}function r({children:t}){let{isSuccess:n,data:r}=u(e);return typeof t==`function`?t({isSuccess:n,data:r}):n&&t}function i({children:t}){let{isInvalid:n,validationError:r}=u(e);return typeof t==`function`?t({isInvalid:n,validationError:r}):n&&t}return{FieldError:n,Success:r,Invalid:i}}function h({autoReset:e=!1,...t}){let{action:n}=u();return s(`form`,{...e?{action:n}:{onSubmit:e=>{e.preventDefault();let t=e.currentTarget;r(()=>{n(new FormData(t))})}},...t})}function g({children:e}){let{isPending:t}=u();return typeof e==`function`?e({isPending:t}):t&&e}export{d as Action,h as Form,g as Pending,p as ZodFieldError,m as createComponents,f as noError,u as useActionContext};
@@ -0,0 +1,90 @@
1
+ import { ZodObject, ZodTuple, ZodType, z } from "zod/v4";
2
+ import { $ZodErrorTree, $ZodShape, $ZodType } from "zod/v4/core";
3
+ //#region src/createFormAction.d.ts
4
+ type InitialState<T> = {
5
+ type: "initial";
6
+ data: T;
7
+ error: null;
8
+ validationError: null;
9
+ };
10
+ type InvalidState<T> = {
11
+ type: "invalid";
12
+ data: null;
13
+ error: null;
14
+ validationError: T;
15
+ };
16
+ type FailureState<T> = {
17
+ type: "failure";
18
+ data: null;
19
+ error: T;
20
+ validationError: null;
21
+ };
22
+ type SuccessState<T> = {
23
+ type: "success";
24
+ data: T;
25
+ error: null;
26
+ validationError: null;
27
+ };
28
+ type ActionState<Data, Error, ValidationError = Record<string, never>> = InitialState<Data> | InvalidState<ValidationError> | FailureState<Error> | SuccessState<Data>;
29
+ type FormAction<Data, Error = Data, ValidationError = Record<string, never>, Payload = FormData, Arguments extends unknown[] = []> = (...args: [...Arguments, state: ActionState<Data, Error, ValidationError>, payload: Payload]) => Promise<ActionState<Data, Error, ValidationError>>;
30
+ declare function initial<Data>(data: Data): InitialState<Data>;
31
+ declare function createFormAction<Data, Error = Data, ValidationError = Record<string, never>, Payload = FormData, Arguments extends [...unknown[]] = []>(formAction: (params: {
32
+ success: (data: Data) => SuccessState<Data>;
33
+ failure: (error: Error) => FailureState<Error>;
34
+ invalid: (validationError: ValidationError) => InvalidState<ValidationError>;
35
+ }, ...args: Arguments) => FormAction<Data, Error, ValidationError, Payload>): (...args: [...Arguments, state: ActionState<Data, Error, ValidationError>, payload: Payload]) => Promise<ActionState<Data, Error, ValidationError>>;
36
+ //#endregion
37
+ //#region src/formAction.d.ts
38
+ declare const emptyInput: unique symbol;
39
+ type EmptyInput = typeof emptyInput;
40
+ type Flatten<T> = Identity<{ [K in keyof T]: T[K]; }>;
41
+ type Identity<T> = T;
42
+ type ErrorHandler<Context, Err, Args> = (params: {
43
+ error: unknown;
44
+ ctx: Context;
45
+ args: Args;
46
+ }) => Promise<Err>;
47
+ type InitialContext = Record<"formData", FormData>;
48
+ /**
49
+ * Action without schema has no input.
50
+ */
51
+ type Action<Data, Context, Args extends unknown[]> = (params: {
52
+ args: Args;
53
+ ctx: Context;
54
+ }) => Promise<Data>;
55
+ /**
56
+ * Action with schema will get parsed formData as the input.
57
+ */
58
+ type SchemaAction<Data, Context, Args extends unknown[], Schema extends ZodType> = (params: {
59
+ args: Args;
60
+ ctx: Context;
61
+ input: z.output<Schema>;
62
+ }) => Promise<Data>;
63
+ type FormActionBuilder<Schema extends ZodType | EmptyInput, Args extends $ZodType[] = [], TErr = unknown, Context = InitialContext, PlainArgs extends unknown[] = z.output<ZodTuple<Args, null>>> = {
64
+ args: <T extends [$ZodType, ...$ZodType[]]>(args: T) => FormActionBuilder<Schema, [...Args, ...T], TErr, Context>;
65
+ input: <T extends ZodObject>(newInput: T extends ZodObject ? T : "The schema output must be an object.") => FormActionBuilder<Schema extends ZodObject<infer O1 extends $ZodShape> ? T extends ZodObject<infer O2 extends $ZodShape> ? ZodObject<O1 & O2> : never : T, Args, TErr, Context>;
66
+ /**
67
+ * A finishing call on the builder completing your action.
68
+ * @param action async function to execute. It will receive parsed formData as input, when the builder was initialized with a schema.
69
+ * @returns runnable server action exportable from a module.
70
+ */
71
+ run: Schema extends ZodType ? <Data>(action: SchemaAction<Data, Flatten<Context>, PlainArgs, Schema>) => (...args: [...PlainArgs, state: ActionState<Data, TErr, $ZodErrorTree<z.output<Schema>> & $ZodErrorTree<PlainArgs>>, payload: FormData]) => Promise<Flatten<InvalidState<$ZodErrorTree<z.output<Schema>> & $ZodErrorTree<PlainArgs>> | FailureState<TErr> | SuccessState<Data>>> : <Data>(action: Action<Data, Flatten<Context>, PlainArgs>) => (...args: [...PlainArgs, state: ActionState<Data, TErr, $ZodErrorTree<PlainArgs>>, payload: FormData]) => Promise<Flatten<InvalidState<$ZodErrorTree<PlainArgs>> | FailureState<TErr> | SuccessState<Data>>>;
72
+ /**
73
+ * A chainable context enhancing helper.
74
+ * @param middleware A function which receives the current context, and returns an object which will be added to the context.
75
+ * @returns FormActionBuilder
76
+ */
77
+ use: <NewContext extends Record<string, unknown>>(middleware: ({ ctx }: {
78
+ ctx: Context;
79
+ }) => Promise<NewContext>) => FormActionBuilder<Schema, Args, TErr, Context & NewContext>;
80
+ /**
81
+ * A chainable error handler to handle errors thrown while running the action passed to .run().
82
+ * It does not receive errors thrown from the context/middleware.
83
+ * @param processError A function which receives action's errors.
84
+ * @returns FormActionBuilder
85
+ */
86
+ error: <TErr>(processError: ErrorHandler<Context, TErr, PlainArgs>) => FormActionBuilder<Schema, Args, TErr, Context>;
87
+ };
88
+ declare const formAction: FormActionBuilder<typeof emptyInput, [], unknown, InitialContext, []>;
89
+ //#endregion
90
+ export { FormAction as a, SuccessState as c, FailureState as i, createFormAction as l, formAction as n, InitialState as o, ActionState as r, InvalidState as s, Flatten as t, initial as u };
package/dist/index.d.mts CHANGED
@@ -1,57 +1,11 @@
1
- import { a as InvalidState, c as initial, i as InitialState, n as FailureState, o as SuccessState, r as FormAction, s as createFormAction, t as ActionState } from "./createFormAction-4p5Fvn2A.mjs";
2
- import { ZodObject, ZodTuple, ZodType, z } from "zod/v4";
3
- import { $ZodErrorTree, $ZodShape, $ZodType } from "zod/v4/core";
4
- //#region src/formAction.d.ts
5
- declare const emptyInput: unique symbol;
6
- type EmptyInput = typeof emptyInput;
7
- type Flatten<T> = Identity<{ [K in keyof T]: T[K]; }>;
8
- type Identity<T> = T;
9
- type ErrorHandler<Context, Err, Args> = (params: {
10
- error: unknown;
11
- ctx: Context;
12
- args: Args;
13
- }) => Promise<Err>;
14
- type InitialContext = Record<"formData", FormData>;
1
+ import { a as FormAction, c as SuccessState, i as FailureState, l as createFormAction, n as formAction, o as InitialState, r as ActionState, s as InvalidState, t as Flatten, u as initial } from "./formAction-0eiUHrXx.mjs";
2
+ import { ZodObject, z } from "zod";
3
+ //#region src/validateSchema.d.ts
15
4
  /**
16
- * Action without schema has no input.
5
+ * A helper to make a validate function for the client validation with <Action validate={} /> prop.
6
+ * @param schema z.object(...) schema
7
+ * @returns validate function
17
8
  */
18
- type Action<Data, Context, Args extends unknown[]> = (params: {
19
- args: Args;
20
- ctx: Context;
21
- }) => Promise<Data>;
22
- /**
23
- * Action with schema will get parsed formData as the input.
24
- */
25
- type SchemaAction<Data, Context, Args extends unknown[], Schema extends ZodType> = (params: {
26
- args: Args;
27
- ctx: Context;
28
- input: z.output<Schema>;
29
- }) => Promise<Data>;
30
- type FormActionBuilder<Schema extends ZodType | EmptyInput, Args extends $ZodType[] = [], TErr = unknown, Context = InitialContext, PlainArgs extends unknown[] = z.output<ZodTuple<Args, null>>> = {
31
- args: <T extends [$ZodType, ...$ZodType[]]>(args: T) => FormActionBuilder<Schema, [...Args, ...T], TErr, Context>;
32
- input: <T extends ZodObject>(newInput: T extends ZodObject ? T : "The schema output must be an object.") => FormActionBuilder<Schema extends ZodObject<infer O1 extends $ZodShape> ? T extends ZodObject<infer O2 extends $ZodShape> ? ZodObject<O1 & O2> : never : T, Args, TErr, Context>;
33
- /**
34
- * A finishing call on the builder completing your action.
35
- * @param action async function to execute. It will receive parsed formData as input, when the builder was initialized with a schema.
36
- * @returns runnable server action exportable from a module.
37
- */
38
- run: Schema extends ZodType ? <Data>(action: SchemaAction<Data, Flatten<Context>, PlainArgs, Schema>) => (...args: [...PlainArgs, state: ActionState<Data, TErr, $ZodErrorTree<z.output<Schema>> & $ZodErrorTree<PlainArgs>>, payload: FormData]) => Promise<Flatten<InvalidState<$ZodErrorTree<z.output<Schema>> & $ZodErrorTree<PlainArgs>> | FailureState<TErr> | SuccessState<Data>>> : <Data>(action: Action<Data, Flatten<Context>, PlainArgs>) => (...args: [...PlainArgs, state: ActionState<Data, TErr, $ZodErrorTree<PlainArgs>>, payload: FormData]) => Promise<Flatten<InvalidState<$ZodErrorTree<PlainArgs>> | FailureState<TErr> | SuccessState<Data>>>;
39
- /**
40
- * A chainable context enhancing helper.
41
- * @param middleware A function which receives the current context, and returns an object which will be added to the context.
42
- * @returns FormActionBuilder
43
- */
44
- use: <NewContext extends Record<string, unknown>>(middleware: ({ ctx }: {
45
- ctx: Context;
46
- }) => Promise<NewContext>) => FormActionBuilder<Schema, Args, TErr, Context & NewContext>;
47
- /**
48
- * A chainable error handler to handle errors thrown while running the action passed to .run().
49
- * It does not receive errors thrown from the context/middleware.
50
- * @param processError A function which receives action's errors.
51
- * @returns FormActionBuilder
52
- */
53
- error: <TErr>(processError: ErrorHandler<Context, TErr, PlainArgs>) => FormActionBuilder<Schema, Args, TErr, Context>;
54
- };
55
- declare const formAction: FormActionBuilder<typeof emptyInput, [], unknown, InitialContext, []>;
9
+ declare function validateSchema<T extends ZodObject>(schema: T): (formData: FormData) => z.core.$ZodErrorTree<z.TypeOf<T>, string> | null;
56
10
  //#endregion
57
- export { ActionState, FailureState, FormAction, InitialState, InvalidState, SuccessState, createFormAction, formAction, initial };
11
+ export { ActionState, FailureState, Flatten, FormAction, InitialState, InvalidState, SuccessState, createFormAction, formAction, initial, validateSchema };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{n as e,t}from"./createFormAction-BwMYAzm2.mjs";import{z as n}from"zod/v4";import{zfd as r}from"zod-form-data";const i=Symbol();function a(e,o=[],s,c){async function l(e){let t={formData:e};for(let e of o){let n=await e({ctx:t});t={...t,...n}}return t}return{args(t){return a(e,o,s,c?[...c,...t]:t)},input(t){if(e===i)return a(t,o,s,c);if(e._zod.def.checks)throw Error(`Previous input is not augmentable because it contains an effect.`);if(t._zod.def.checks)throw Error(`Your input contains effect which prevents merging it with the previous inputs.`);if(e._zod.def.type===`object`)return a(n.object({...e.shape,...t.shape}),o,s,c);throw Error(`Merging inputs works only for object schemas without effects.`)},use(t){return a(e,[...o,t],s,c)},error(t){return a(e,o,t,c)},run:e===i?e=>t(({success:t,failure:r,invalid:i},...a)=>async(o,u)=>{let d=await l(u);if(c){let e=n.tuple(c).safeParse(a);if(!e.success)return i(n.treeifyError(e.error))}try{return t(await e({args:a,ctx:d}))}catch(e){if(s)return r(await s({args:a,error:e,ctx:d}));throw e}}):e===i?void 0:i=>t(({success:t,failure:a,invalid:o},...u)=>{let d=r.formData(e);return async(e,r)=>{let f=await l(r);if(c){let e=n.tuple(c).safeParse(u);if(!e.success)return o(n.treeifyError(e.error))}let p=d.safeParse(r);if(!p.success)return o(n.treeifyError(p.error));let m=p.data;try{return t(await i({args:u,input:m,ctx:f}))}catch(e){if(s)return a(await s({args:u,error:e,ctx:f}));throw e}}})}}const o=a(i);export{t as createFormAction,o as formAction,e as initial};
1
+ import{n as e,t}from"./createFormAction-BwMYAzm2.mjs";import{z as n}from"zod/v4";import{zfd as r}from"zod-form-data";import{z as i}from"zod";const a=Symbol();function o(e,i=[],s,c){async function l(e){let t={formData:e};for(let e of i){let n=await e({ctx:t});t={...t,...n}}return t}return{args(t){return o(e,i,s,c?[...c,...t]:t)},input(t){if(e===a)return o(t,i,s,c);if(e._zod.def.checks)throw Error(`Previous input is not augmentable because it contains an effect.`);if(t._zod.def.checks)throw Error(`Your input contains effect which prevents merging it with the previous inputs.`);if(e._zod.def.type===`object`)return o(n.object({...e.shape,...t.shape}),i,s,c);throw Error(`Merging inputs works only for object schemas without effects.`)},use(t){return o(e,[...i,t],s,c)},error(t){return o(e,i,t,c)},run:e===a?e=>t(({success:t,failure:r,invalid:i},...a)=>async(o,u)=>{let d=await l(u);if(c){let e=n.tuple(c).safeParse(a);if(!e.success)return i(n.treeifyError(e.error))}try{return t(await e({args:a,ctx:d}))}catch(e){if(s)return r(await s({args:a,error:e,ctx:d}));throw e}}):e===a?void 0:i=>t(({success:t,failure:a,invalid:o},...u)=>{let d=r.formData(e);return async(e,r)=>{let f=await l(r);if(c){let e=n.tuple(c).safeParse(u);if(!e.success)return o(n.treeifyError(e.error))}let p=d.safeParse(r);if(!p.success)return o(n.treeifyError(p.error));let m=p.data;try{return t(await i({args:u,input:m,ctx:f}))}catch(e){if(s)return a(await s({args:u,error:e,ctx:f}));throw e}}})}}const s=o(a);function c(e){return t=>{let n=r.formData(e).safeParse(t);return n.error?i.treeifyError(n.error):null}}export{t as createFormAction,s as formAction,e as initial,c as validateSchema};
package/package.json CHANGED
@@ -1,76 +1,78 @@
1
- {
2
- "name": "react-form-action",
3
- "version": "2.10.0",
4
- "description": "State management helpers for the react form actions.",
5
- "repository": {
6
- "type": "git",
7
- "url": "https://github.com/MiroslavPetrik/react-form-action.git"
8
- },
9
- "main": "dist/index.js",
10
- "module": "dist/index.mjs",
11
- "files": [
12
- "/dist"
13
- ],
14
- "scripts": {
15
- "build": "tsdown src/index.ts src/client.ts --minify",
16
- "test": "vitest",
17
- "typecheck": "vitest --typecheck",
18
- "format": "prettier . --write",
19
- "format:check": "prettier . --check",
20
- "ncu": "ncu --interactive --format group",
21
- "pub:next": "npm publish --tag next"
22
- },
23
- "exports": {
24
- ".": {
25
- "import": "./dist/index.mjs",
26
- "module": "./dist/index.mjs",
27
- "types": "./dist/index.d.mts",
28
- "default": "./dist/index.mjs"
29
- },
30
- "./client": {
31
- "import": "./dist/client.mjs",
32
- "module": "./dist/client.mjs",
33
- "types": "./dist/client.d.mts",
34
- "default": "./dist/client.mjs"
35
- }
36
- },
37
- "publishConfig": {
38
- "access": "public"
39
- },
40
- "keywords": [
41
- "react",
42
- "form-action",
43
- "react-form-action",
44
- "next",
45
- "next-form-action",
46
- "nextjs",
47
- "server-action"
48
- ],
49
- "author": "Miroslav Petrik",
50
- "license": "MIT",
51
- "peerDependencies": {
52
- "react": ">=19",
53
- "react-dom": ">=19",
54
- "zod": "^3.25.0 || ^4.0.0",
55
- "zod-form-data": "^3.0.0"
56
- },
57
- "devDependencies": {
58
- "@testing-library/dom": "^10.4.1",
59
- "@testing-library/jest-dom": "^7.0.0",
60
- "@testing-library/react": "^16.3.2",
61
- "@testing-library/user-event": "^14.6.1",
62
- "@types/react": "^19.2.17",
63
- "@types/react-dom": "^19.2.3",
64
- "@vitejs/plugin-react": "^6.0.3",
65
- "happy-dom": "^20.11.0",
66
- "npm-check-updates": "^22.2.9",
67
- "prettier": "^3.9.6",
68
- "react": "^19.2.7",
69
- "react-dom": "^19.2.7",
70
- "tsdown": "^0.22.12",
71
- "typescript": "^6.0.3",
72
- "vitest": "^4.1.10",
73
- "zod": "^4.4.3",
74
- "zod-form-data": "^3.0.3"
75
- }
76
- }
1
+ {
2
+ "name": "react-form-action",
3
+ "version": "2.12.0",
4
+ "description": "State management helpers for the react form actions.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/MiroslavPetrik/react-form-action.git"
8
+ },
9
+ "main": "dist/index.js",
10
+ "module": "dist/index.mjs",
11
+ "files": [
12
+ "/dist"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsdown src/index.ts src/client.ts --minify",
16
+ "test": "vitest",
17
+ "typecheck": "vitest --typecheck",
18
+ "lint": "biome lint ./src",
19
+ "lint:write": "biome lint ./src --write",
20
+ "format": "biome format --write .",
21
+ "format:check": "biome check .",
22
+ "ncu": "ncu --interactive --format group",
23
+ "pub:next": "npm publish --tag next"
24
+ },
25
+ "exports": {
26
+ ".": {
27
+ "import": "./dist/index.mjs",
28
+ "module": "./dist/index.mjs",
29
+ "types": "./dist/index.d.mts",
30
+ "default": "./dist/index.mjs"
31
+ },
32
+ "./client": {
33
+ "import": "./dist/client.mjs",
34
+ "module": "./dist/client.mjs",
35
+ "types": "./dist/client.d.mts",
36
+ "default": "./dist/client.mjs"
37
+ }
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "keywords": [
43
+ "react",
44
+ "form-action",
45
+ "react-form-action",
46
+ "next",
47
+ "next-form-action",
48
+ "nextjs",
49
+ "server-action"
50
+ ],
51
+ "author": "Miroslav Petrik",
52
+ "license": "MIT",
53
+ "peerDependencies": {
54
+ "react": ">=19",
55
+ "react-dom": ">=19",
56
+ "zod": "^3.25.0 || ^4.0.0",
57
+ "zod-form-data": "^3.0.0"
58
+ },
59
+ "devDependencies": {
60
+ "@biomejs/biome": "^2.5.8",
61
+ "@testing-library/dom": "^10.4.1",
62
+ "@testing-library/jest-dom": "^7.0.1",
63
+ "@testing-library/react": "^16.3.2",
64
+ "@testing-library/user-event": "^14.6.4",
65
+ "@types/react": "^19.2.18",
66
+ "@types/react-dom": "^19.2.4",
67
+ "@vitejs/plugin-react": "^6.0.5",
68
+ "happy-dom": "^20.11.2",
69
+ "npm-check-updates": "^23.0.2",
70
+ "react": "^19.2.8",
71
+ "react-dom": "^19.2.8",
72
+ "tsdown": "^0.22.14",
73
+ "typescript": "^7.0.2",
74
+ "vitest": "^4.1.10",
75
+ "zod": "^4.4.3",
76
+ "zod-form-data": "^3.0.3"
77
+ }
78
+ }
@@ -1,35 +0,0 @@
1
- //#region src/createFormAction.d.ts
2
- type InitialState<T> = {
3
- type: "initial";
4
- data: T;
5
- error: null;
6
- validationError: null;
7
- };
8
- type InvalidState<T> = {
9
- type: "invalid";
10
- data: null;
11
- error: null;
12
- validationError: T;
13
- };
14
- type FailureState<T> = {
15
- type: "failure";
16
- data: null;
17
- error: T;
18
- validationError: null;
19
- };
20
- type SuccessState<T> = {
21
- type: "success";
22
- data: T;
23
- error: null;
24
- validationError: null;
25
- };
26
- type ActionState<Data, Error, ValidationError = Record<string, never>> = InitialState<Data> | InvalidState<ValidationError> | FailureState<Error> | SuccessState<Data>;
27
- type FormAction<Data, Error = Data, ValidationError = Record<string, never>, Payload = FormData, Arguments extends unknown[] = []> = (...args: [...Arguments, state: ActionState<Data, Error, ValidationError>, payload: Payload]) => Promise<ActionState<Data, Error, ValidationError>>;
28
- declare function initial<Data>(data: Data): InitialState<Data>;
29
- declare function createFormAction<Data, Error = Data, ValidationError = Record<string, never>, Payload = FormData, Arguments extends [...unknown[]] = []>(formAction: (params: {
30
- success: (data: Data) => SuccessState<Data>;
31
- failure: (error: Error) => FailureState<Error>;
32
- invalid: (validationError: ValidationError) => InvalidState<ValidationError>;
33
- }, ...args: Arguments) => FormAction<Data, Error, ValidationError, Payload>): (...args: [...Arguments, state: ActionState<Data, Error, ValidationError>, payload: Payload]) => Promise<ActionState<Data, Error, ValidationError>>;
34
- //#endregion
35
- export { InvalidState as a, initial as c, InitialState as i, FailureState as n, SuccessState as o, FormAction as r, createFormAction as s, ActionState as t };