react-form-action 0.2.0 → 0.4.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
@@ -2,10 +2,27 @@
2
2
 
3
3
  End-to-end typesafe success, error & validation state control for Next.js 14 form actions.
4
4
 
5
+ ## Features
6
+
7
+ **Action Creator**
8
+
9
+ - ✅ Provides `"invalid" | "success" | "failure"` response objects.
10
+ - ✅ Define generic payload for each of the response type.
11
+
12
+ **Form Action builder**
13
+
14
+ - ✅ tRPC-like builder API for `.input(zodSchema)` & context `.use(middleware)`
15
+ - ✅ Parses `formData` with [`zod-form-data`](https://www.npmjs.com/package/zod-form-data)
16
+
17
+ **Stateful `<Form />`**
18
+
19
+ - ✅ `<Form />` component reads the action's response.
20
+ - ✅ Computes progress meta-state like `isInvalid`, `isSuccess` and more.
21
+
5
22
  ## Install
6
23
 
7
24
  ```
8
- npm i react-form-action
25
+ npm i react-form-action zod-form-data
9
26
  ```
10
27
 
11
28
  ## Usage
@@ -20,20 +37,18 @@ import { formAction } from "react-form-action";
20
37
  import { z } from "zod";
21
38
  import { cookies } from "next/headers";
22
39
 
23
- const emailSchema = z.object({ email: z.string().email() });
24
-
25
40
  const i18nMiddleware = async () => {
26
41
  const { t } = await useTranslation("auth", cookies().get("i18n")?.value);
27
42
  // will be added to context
28
43
  return { t };
29
44
  };
30
45
 
31
- const authAction = formAction(emailSchema)
46
+ const authAction = formAction
32
47
  .use(i18nMiddleware)
33
48
  .use(async ({ ctx: { t } }) =>
34
49
  console.log("🎉 context enhanced by previous middlewares 🎉", t)
35
50
  )
36
- .error((error) => {
51
+ .error(({ error }) => {
37
52
  if (error instanceof DbError) {
38
53
  return error.custom.code;
39
54
  } else {
@@ -43,25 +58,38 @@ const authAction = formAction(emailSchema)
43
58
  }
44
59
  });
45
60
 
46
- export const resendVerifyEmailAction = authAction.run(
47
- async ({ ctx: { t }, input: { email } }) => {
48
- // do custom work
49
- await db.resendVerificationEmail({ email });
61
+ export const signIn = authAction
62
+ .input(z.object({ email: z.string().email() }))
63
+ // 🎉 extend the previous input (only without refinements and transforms)
64
+ .input(z.object({ password: z.string() }))
65
+ .run(async ({ ctx: { t }, input: { email, password } }) => {
66
+ // Type inferred: {email: string, password: string}
50
67
 
51
- // return translated success message
52
- return t("verificationEmail.success");
53
- }
54
- );
68
+ await db.signIn({ email, password });
55
69
 
56
- export const sendResetPasswordEmail = authAction.run(
57
- async ({ ctx: { t }, input: { email } }) => {
58
- // do custom work
59
- await db.emailPasswordSendPasswordResetEmail({ email });
70
+ return t("verificationEmail.success");
71
+ });
60
72
 
61
- // return translated success message
62
- return t("resetPasswordEmail.success");
63
- }
64
- );
73
+ export const signUp = authAction
74
+ .input(
75
+ z
76
+ .object({
77
+ email: z.string().email(),
78
+ password: z.string(),
79
+ confirm: z.string(),
80
+ })
81
+ .refine((data) => data.password === data.confirm, {
82
+ message: "Passwords don't match",
83
+ path: ["confirm"],
84
+ })
85
+ ) // if using refinement, only one input call is permited, as schema with ZodEffects is not extendable.
86
+ .run(async ({ ctx: { t }, input: { email, password } }) => {
87
+ // 🎉 passwords match!
88
+
89
+ await db.signUp({ email, password });
90
+
91
+ return t("signUp.success");
92
+ });
65
93
  ```
66
94
 
67
95
  ### Server Action (usable in Next.js)
package/dist/client.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import React__default, { FormHTMLAttributes } from 'react';
3
3
  import { RenderProp } from 'react-render-prop-type';
4
- import { I as InitialState, a as InvalidState, b as FailureState, S as SuccessState, F as FormState } from './createFormAction-0c8f36ce.js';
4
+ import { b as InitialState, I as InvalidState, a as FailureState, S as SuccessState, F as FormState } from './createFormAction-3a51310f.js';
5
5
  import { useFormStatus } from 'react-dom';
6
6
 
7
7
  type FormStateProps<Data, ValidationError, Error, Payload> = {
@@ -29,4 +29,4 @@ declare function createFormAction<Data, Error = Data, ValidationError = Record<s
29
29
  invalid: (validationError: ValidationError) => InvalidState<ValidationError>;
30
30
  }) => (state: FormState<Data, Error, ValidationError>, payload: Payload) => Promise<FormState<Data, Error, ValidationError>>): (state: FormState<Data, Error, ValidationError>, payload: Payload) => Promise<FormState<Data, Error, ValidationError>>;
31
31
 
32
- export { FormState as F, InitialState as I, SuccessState as S, InvalidState as a, FailureState as b, createFormAction as c };
32
+ export { FormState as F, InvalidState as I, SuccessState as S, FailureState as a, InitialState as b, createFormAction as c };
package/dist/index.d.mts CHANGED
@@ -1,10 +1,13 @@
1
- import { F as FormState } from './createFormAction-0c8f36ce.js';
2
- export { b as FailureState, I as InitialState, a as InvalidState, S as SuccessState, c as createFormAction } from './createFormAction-0c8f36ce.js';
3
- import { ZodSchema, ZodUndefined, z } from 'zod';
1
+ import { F as FormState, I as InvalidState, a as FailureState, S as SuccessState } from './createFormAction-3a51310f.js';
2
+ export { b as InitialState, c as createFormAction } from './createFormAction-3a51310f.js';
3
+ import { ZodTypeAny, ZodType, ZodObject, ZodRawShape, objectUtil, z, ZodSchema, ZodEffects } from 'zod';
4
4
 
5
- type MiddlewareFn<Context, NewContext extends Record<string, unknown> = Record<string, unknown>> = ({ ctx }: {
6
- ctx: Context;
7
- }) => Promise<NewContext>;
5
+ declare const emptyInput: unique symbol;
6
+ type EmptyInput = typeof emptyInput;
7
+ type Flatten<T> = Identity<{
8
+ [K in keyof T]: T[K];
9
+ }>;
10
+ type Identity<T> = T;
8
11
  type InitialContext = Record<"formData", FormData>;
9
12
  /**
10
13
  * Action without schema has no input.
@@ -19,13 +22,15 @@ type SchemaAction<Data, Context, Schema extends ZodSchema> = (params: {
19
22
  ctx: Context;
20
23
  input: z.infer<Schema>;
21
24
  }) => Promise<Data>;
22
- type FormActionBuilder<Schema extends ZodSchema = ZodUndefined, Err = Error, Context = InitialContext> = {
25
+ type AnyZodEffects = ZodEffects<any, any, any>;
26
+ type FormActionBuilder<Schema extends ZodTypeAny | EmptyInput, Err = Error, Context = InitialContext> = {
27
+ input: <T extends ZodTypeAny>(newInput: T extends ZodType<infer Out extends Record<string, unknown>> ? Schema extends AnyZodEffects ? "Extending schema with effect is not possible." : Schema extends EmptyInput ? T : Schema extends ZodTypeAny ? T extends AnyZodEffects ? "Your input contains effect which prevents merging it with the previous inputs." : T : T : "The schema output must be an object.") => FormActionBuilder<Schema extends ZodObject<infer O1 extends ZodRawShape> ? T extends ZodObject<infer O2 extends ZodRawShape> ? ZodObject<objectUtil.extendShape<O1, O2>> : never : T, Err, Context>;
23
28
  /**
24
29
  * A finishing call on the builder completing your action.
25
30
  * @param action async function to execute. It will receive parsed formData as input, when the builder was initialized with a schema.
26
31
  * @returns runnable server action exportable from a module.
27
32
  */
28
- run: Schema extends undefined ? <Data>(action: Action<Data, Context>) => (state: FormState<Data, Err>, payload: FormData) => Promise<Data> : <Data>(action: SchemaAction<Data, Context, Schema>) => (state: FormState<Data, Err, ZodErrorMessages<Schema>>, payload: FormData) => Promise<Data>;
33
+ run: Schema extends ZodTypeAny ? <Data>(action: SchemaAction<Data, Context, Schema>) => (state: FormState<Data, Err, z.inferFlattenedErrors<Schema>>, payload: FormData) => Promise<Flatten<InvalidState<z.inferFlattenedErrors<Schema>> | FailureState<Err> | SuccessState<Data>>> : <Data>(action: Action<Data, Context>) => (state: FormState<Data, Err>, payload: FormData) => Promise<Flatten<FailureState<Err> | SuccessState<Data>>>;
29
34
  /**
30
35
  * A chainable context enhancing helper.
31
36
  * @param middleware A function which receives the current context, and returns an object which will be added to the context.
@@ -40,19 +45,11 @@ type FormActionBuilder<Schema extends ZodSchema = ZodUndefined, Err = Error, Con
40
45
  * @param processError A function which receives action's errors.
41
46
  * @returns FormActionBuilder
42
47
  */
43
- error: <Err>(processError: (error: unknown) => Err) => FormActionBuilder<Schema, Err, Context>;
44
- };
45
- declare function formAction<Schema extends ZodSchema = ZodUndefined, Err = Error, Context = InitialContext, NewContext extends Record<string, unknown> = Record<string, unknown>>(schema?: Schema,
46
- /**
47
- * @private
48
- */
49
- middleware?: MiddlewareFn<Context, NewContext>[],
50
- /**
51
- * @private
52
- */
53
- processError?: (error: unknown) => Err): FormActionBuilder<Schema, Err, Context>;
54
- type ZodErrorMessages<Schema extends ZodSchema> = {
55
- [k in keyof z.infer<Schema>]?: string;
48
+ error: <Err>(processError: (params: {
49
+ error: unknown;
50
+ ctx: Context;
51
+ }) => Err) => FormActionBuilder<Schema, Err, Context>;
56
52
  };
53
+ declare const formAction: FormActionBuilder<typeof emptyInput, Error, InitialContext>;
57
54
 
58
- export { FormState, formAction };
55
+ export { FailureState, FormState, InvalidState, SuccessState, formAction };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- function x(r){function n(a){return{type:"success",data:a,error:null,validationError:null}}function e(a){return{type:"failure",data:null,error:a,validationError:null}}function i(a){return{type:"invalid",data:null,error:null,validationError:a}}return(a,m)=>r({success:n,failure:e,invalid:i})(a,m)}function C(r,n=[],e){async function i(t){let o={formData:t};for(let c of n){let d=await c({ctx:o});o={...o,...d}}return o}return{run:r?t=>x(({success:o,failure:c,invalid:d})=>async(S,s)=>{let l=await i(s),u=r.safeParse(s);if(!u.success)return d(D(u.error));let p=u.data;try{return o(await t({input:p,ctx:l}))}catch(E){if(e)return c(e(E));throw E}}):t=>x(({success:o,failure:c})=>async(d,S)=>{let s=await i(S);try{return o(await t({ctx:s}))}catch(l){if(e)return c(e(l));throw l}}),use(t){return C(r,[...n,t])},error(t){return C(r,n,t)}}}var D=r=>r.errors.reduce((n,{message:e,path:i})=>({...n,[i[0]]:e}),{});export{x as createFormAction,C as formAction};
1
+ function y(e){function a(n){return{type:"success",data:n,error:null,validationError:null}}function o(n){return{type:"failure",data:null,error:n,validationError:null}}function d(n){return{type:"invalid",data:null,error:null,validationError:n}}return(n,S)=>e({success:a,failure:o,invalid:d})(n,S)}import{ZodFirstPartyTypeKind as C}from"zod";import{zfd as w}from"zod-form-data";var l=Symbol();function s(e,a=[],o){async function d(t){let r={formData:t};for(let i of a){let u=await i({ctx:r});r={...r,...u}}return r}return{input(t){if(e===l)return s(t,a);if(e._def.effect)throw new Error("Previous input is not augmentable because it contains an effect.");if(t._def.effect)throw new Error("Your input contains effect which prevents merging it with the previous inputs.");if(e._def.typeName===C.ZodObject){let r=e.merge(t);return s(r,a)}else throw Error("Merging inputs works only for object schemas without effects.")},use(t){return s(e,[...a,t])},error(t){return s(e,a,t)},run:e===l?t=>y(({success:r,failure:i})=>async(u,p)=>{let x=await d(p);try{return r(await t({ctx:x}))}catch(c){if(o)return i(o({error:c,ctx:x}));throw c}}):e===l?void 0:t=>y(({success:r,failure:i,invalid:u})=>{let p=w.formData(e);return async(x,c)=>{let f=await d(c),m=p.safeParse(c);if(!m.success)return u(m.error.flatten());let h=m.data;try{return r(await t({input:h,ctx:f}))}catch(E){if(o)return i(o({error:E,ctx:f}));throw E}}})}}var b=s(l);export{y as createFormAction,b as formAction};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-form-action",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "State management helpers for the react form actions.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,7 +13,8 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "build": "tsup",
16
- "test": "vitest"
16
+ "test": "vitest",
17
+ "typecheck": "vitest --typecheck"
17
18
  },
18
19
  "exports": {
19
20
  ".": {
@@ -43,7 +44,8 @@
43
44
  "peerDependencies": {
44
45
  "react": ">=18.2",
45
46
  "react-dom": ">=18.2",
46
- "zod": "^3"
47
+ "zod": "^3",
48
+ "zod-form-data": "^2"
47
49
  },
48
50
  "devDependencies": {
49
51
  "@types/react": "^18.2.37",