@firtoz/router-toolkit 8.0.1 → 9.0.1

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.
@@ -1,55 +0,0 @@
1
- import { useMemo, useCallback } from 'react';
2
- import { href, useFetcher } from 'react-router';
3
- import { jsx } from 'react/jsx-runtime';
4
-
5
- // src/useDynamicSubmitter.tsx
6
- var useDynamicSubmitter = (path, ...args) => {
7
- const url = useMemo(() => {
8
- return href(path, ...args);
9
- }, [path, args]);
10
- const fetcher = useFetcher({
11
- key: `submitter-${url}`
12
- });
13
- const submit = useCallback(
14
- (target, options) => {
15
- return fetcher.submit(target, {
16
- ...options,
17
- method: options?.method ?? "POST",
18
- action: url,
19
- encType: "multipart/form-data"
20
- });
21
- },
22
- [fetcher.submit, url]
23
- );
24
- const submitJson = useCallback(
25
- (data, options = {}) => {
26
- return fetcher.submit(
27
- data,
28
- {
29
- ...options,
30
- method: options.method ?? "POST",
31
- action: url,
32
- encType: "application/json"
33
- }
34
- );
35
- },
36
- [fetcher.submit, url]
37
- );
38
- const OriginalForm = fetcher.Form;
39
- const Form = useCallback(
40
- ({ method = "POST", ...props }) => {
41
- return /* @__PURE__ */ jsx(OriginalForm, { action: url, method, ...props });
42
- },
43
- [url, OriginalForm]
44
- );
45
- return {
46
- ...fetcher,
47
- submit,
48
- submitJson,
49
- Form
50
- };
51
- };
52
-
53
- export { useDynamicSubmitter };
54
- //# sourceMappingURL=chunk-JJN6GBJL.js.map
55
- //# sourceMappingURL=chunk-JJN6GBJL.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/useDynamicSubmitter.tsx"],"names":[],"mappings":";;;;;AA6QO,IAAM,mBAAA,GAAsB,CAClC,IAAA,EAAA,GACG,IAAA,KAaC;AACJ,EAAA,MAAM,GAAA,GAAM,QAAQ,MAAM;AAEzB,IAAA,OAAO,IAAA,CAAK,IAAA,EAAM,GAAI,IAAY,CAAA;AAAA,EACnC,CAAA,EAAG,CAAC,IAAA,EAAM,IAAI,CAAC,CAAA;AAEf,EAAA,MAAM,UAAU,UAAA,CAA4B;AAAA,IAC3C,GAAA,EAAK,aAAa,GAAG,CAAA;AAAA,GACrB,CAAA;AAED,EAAA,MAAM,MAAA,GAA4B,WAAA;AAAA,IACjC,CAAC,QAAQ,OAAA,KAAY;AACpB,MAAA,OAAO,OAAA,CAAQ,OAAO,MAAA,EAAQ;AAAA,QAC7B,GAAG,OAAA;AAAA,QACH,MAAA,EAAS,SAAS,MAAA,IACjB,MAAA;AAAA,QACD,MAAA,EAAQ,GAAA;AAAA,QACR,OAAA,EAAS;AAAA,OAC+B,CAAA;AAAA,IAC1C,CAAA;AAAA,IACA,CAAC,OAAA,CAAQ,MAAA,EAAQ,GAAG;AAAA,GACrB;AAEA,EAAA,MAAM,UAAA,GAAoC,WAAA;AAAA,IACzC,CAAC,IAAA,EAAM,OAAA,GAAU,EAAC,KAAM;AACvB,MAAA,OAAO,OAAA,CAAQ,MAAA;AAAA,QACd,IAAA;AAAA,QACA;AAAA,UACC,GAAG,OAAA;AAAA,UACH,MAAA,EAAS,QAAQ,MAAA,IAChB,MAAA;AAAA,UACD,MAAA,EAAQ,GAAA;AAAA,UACR,OAAA,EAAS;AAAA;AACV,OACD;AAAA,IACD,CAAA;AAAA,IACA,CAAC,OAAA,CAAQ,MAAA,EAAQ,GAAG;AAAA,GACrB;AAEA,EAAA,MAAM,eAAe,OAAA,CAAQ,IAAA;AAE7B,EAAA,MAAM,IAAA,GAAmB,WAAA;AAAA,IACxB,CAAC,EAAE,MAAA,GAAS,MAAA,EAAQ,GAAG,OAAM,KAAM;AAClC,MAAA,2BAAQ,YAAA,EAAA,EAAa,MAAA,EAAQ,GAAA,EAAK,MAAA,EAAiB,GAAG,KAAA,EAAO,CAAA;AAAA,IAC9D,CAAA;AAAA,IACA,CAAC,KAAK,YAAY;AAAA,GACnB;AAEA,EAAA,OAAO;AAAA,IACN,GAAG,OAAA;AAAA,IACH,MAAA;AAAA,IACA,UAAA;AAAA,IACA;AAAA,GACD;AACD","file":"chunk-JJN6GBJL.js","sourcesContent":["/**\n * @fileoverview Type-safe dynamic form submission hook for React Router 7\n *\n * This module provides a hook that creates a type-safe fetcher for submitting forms\n * to dynamic routes with full TypeScript inference for the form schema and route params.\n *\n * @example\n * ### Route Setup (`app/routes/admin.posts.$id.tsx`)\n *\n * First, set up your route with the required exports:\n *\n * ```typescript\n * import { z } from \"zod\";\n * import { formAction, type RoutePath } from \"@firtoz/router-toolkit\";\n * import { success, fail } from \"@firtoz/maybe-error\";\n *\n * // Export the route path for type inference\n * export const route: RoutePath<\"/admin/posts/:id\"> = \"/admin/posts/:id\";\n *\n * // Define the form schema\n * export const formSchema = z.object({\n * title: z.string().min(1, \"Title is required\"),\n * content: z.string().min(10, \"Content must be at least 10 characters\"),\n * published: z.boolean().optional().default(false),\n * });\n *\n * // Create the action using formAction\n * export const action = formAction({\n * schema: formSchema,\n * handler: async ({ request, params }, formData) => {\n * const postId = params.id;\n * const updated = await db.posts.update({\n * where: { id: postId },\n * data: formData,\n * });\n * return success(updated);\n * },\n * });\n * ```\n *\n * @example\n * ### Using the hook in a component\n *\n * ```tsx\n * import { useDynamicSubmitter } from \"@firtoz/router-toolkit\";\n *\n * function EditPostForm({ postId }: { postId: string }) {\n * // Type-safe submitter with full inference\n * const submitter = useDynamicSubmitter<typeof import(\"./admin.posts.$id\")>(\n * \"/admin/posts/:id\",\n * { id: postId }\n * );\n *\n * // submitter.data is the typed response from the action\n * // submitter.state is \"idle\" | \"loading\" | \"submitting\"\n *\n * // Option 1: Submit as JSON (recommended for programmatic submissions)\n * // Defaults to POST if no options provided\n * const handleSubmitJson = async () => {\n * await submitter.submitJson({\n * title: \"My Post\",\n * content: \"Post content here\",\n * published: true,\n * });\n * };\n *\n * // Option 2: Submit with FormData or SubmitTarget\n * const handleSubmit = async (formData: FormData) => {\n * await submitter.submit(formData, { method: \"POST\" });\n * };\n *\n * // Option 3: Use the Form component (defaults to POST)\n * return (\n * <submitter.Form>\n * <input name=\"title\" />\n * <textarea name=\"content\" />\n * <button type=\"submit\">Save</button>\n * </submitter.Form>\n * );\n * }\n * ```\n *\n * @example\n * ### Handling responses\n *\n * ```tsx\n * function LoginForm() {\n * const submitter = useDynamicSubmitter<typeof import(\"./auth.login\")>(\"/auth/login\");\n *\n * useEffect(() => {\n * if (submitter.data?.success) {\n * // Handle success\n * console.log(\"Logged in as:\", submitter.data.value.user.email);\n * } else if (submitter.data && !submitter.data.success) {\n * // Handle error\n * if (submitter.data.error.type === \"validation\") {\n * console.log(\"Validation errors:\", submitter.data.error.error);\n * }\n * }\n * }, [submitter.data]);\n *\n * return (\n * <submitter.Form>\n * <input name=\"email\" type=\"email\" />\n * <input name=\"password\" type=\"password\" />\n * <button disabled={submitter.state !== \"idle\"}>\n * {submitter.state === \"submitting\" ? \"Logging in...\" : \"Login\"}\n * </button>\n * </submitter.Form>\n * );\n * }\n * ```\n */\n\n// biome-ignore lint/style/useImportType: We need to import React here.\nimport React, { useCallback, useMemo } from \"react\";\nimport {\n\ttype FetcherFormProps,\n\thref,\n\ttype SubmitOptions,\n\ttype SubmitTarget,\n\tuseFetcher,\n} from \"react-router\";\nimport type { z } from \"zod\";\nimport type { HrefArgs } from \"./types/HrefArgs\";\nimport type { RouteWithActionModule } from \"./types/RouteWithActionModule\";\n\n/**\n * Function type for submitting form data with a SubmitTarget.\n *\n * Accepts the form schema data combined with SubmitTarget (FormData, HTMLFormElement, etc.)\n * Use this when you have a FormData object or form element reference.\n *\n * @example\n * ```typescript\n * // With FormData\n * submitter.submit(formData, { method: \"POST\" });\n *\n * // With form element reference\n * submitter.submit(formRef.current, { method: \"POST\" });\n * ```\n */\ntype SubmitFunc<TModule extends RouteWithActionModule> = (\n\ttarget: z.infer<TModule[\"formSchema\"]> & SubmitTarget,\n\toptions: Omit<SubmitOptions, \"action\" | \"method\" | \"encType\"> & {\n\t\tmethod: Exclude<SubmitOptions[\"method\"], \"GET\">;\n\t},\n) => Promise<void>;\n\n/**\n * Options for submitJson function.\n * Method defaults to \"POST\" if not specified.\n */\ntype SubmitJsonOptions = Omit<\n\tSubmitOptions,\n\t\"action\" | \"method\" | \"encType\"\n> & {\n\tmethod?: Exclude<SubmitOptions[\"method\"], \"GET\">;\n};\n\n/**\n * Function type for submitting form data as JSON.\n *\n * Accepts only the inferred form schema type (plain object).\n * Automatically serializes the data as JSON. This is the recommended\n * approach for programmatic form submissions.\n *\n * Options are optional and default to `{ method: \"POST\" }`.\n *\n * @example\n * ```typescript\n * // Submit a plain object - fully type-safe (defaults to POST)\n * await submitter.submitJson({\n * email: \"user@example.com\",\n * password: \"secret123\",\n * rememberMe: true,\n * });\n *\n * // Or specify a different method\n * await submitter.submitJson(data, { method: \"PUT\" });\n * ```\n */\ntype SubmitJsonFunc<TModule extends RouteWithActionModule> = (\n\tdata: z.infer<TModule[\"formSchema\"]>,\n\toptions?: SubmitJsonOptions,\n) => Promise<void>;\n\n/**\n * Form component type with pre-bound action URL.\n *\n * Renders a form element that automatically submits to the correct route.\n * Method defaults to \"POST\" if not specified.\n *\n * @example\n * ```typescript\n * // Defaults to POST\n * <submitter.Form>\n * <input name=\"title\" />\n * <button type=\"submit\">Submit</button>\n * </submitter.Form>\n *\n * // Or specify a different method\n * <submitter.Form method=\"PUT\">\n * ...\n * </submitter.Form>\n * ```\n */\ntype SubmitForm = (\n\tprops: Omit<\n\t\tFetcherFormProps & React.RefAttributes<HTMLFormElement>,\n\t\t\"action\" | \"method\"\n\t> & {\n\t\tmethod?: Exclude<SubmitOptions[\"method\"], \"GET\">;\n\t},\n) => React.ReactElement;\n\n/**\n * Creates a type-safe fetcher for submitting forms to dynamic routes.\n *\n * This hook provides full TypeScript inference for:\n * - Route parameters (from the route path)\n * - Form data schema (from the route's formSchema export)\n * - Action response type (from the route's action export)\n *\n * @template TInfo - The route module type (use `typeof import(\"./route-file\")`)\n *\n * @param path - The route path (must match the route's `route` export)\n * @param args - Route parameters (if the route has dynamic segments like `:id`)\n *\n * @returns An extended fetcher object with:\n * - `submit` - Submit with FormData/SubmitTarget (includes schema type)\n * - `submitJson` - Submit a plain object as JSON (schema type only)\n * - `Form` - Pre-bound form component\n * - `data` - Response data from the action (typed)\n * - `state` - Fetcher state (\"idle\" | \"loading\" | \"submitting\")\n * - All other useFetcher properties\n *\n * @example\n * ### Basic usage with route parameters\n *\n * ```typescript\n * // In your route file (app/routes/users.$userId.settings.tsx):\n * export const route: RoutePath<\"/users/:userId/settings\"> = \"/users/:userId/settings\";\n * export const formSchema = z.object({\n * displayName: z.string().min(2),\n * email: z.string().email(),\n * notifications: z.boolean().default(true),\n * });\n * export const action = formAction({ schema: formSchema, handler: ... });\n *\n * // In your component:\n * const submitter = useDynamicSubmitter<typeof import(\"./users.$userId.settings\")>(\n * \"/users/:userId/settings\",\n * { userId: \"123\" }\n * );\n *\n * // Submit using submitJson (type-safe, no FormData needed, defaults to POST)\n * await submitter.submitJson({\n * displayName: \"John Doe\",\n * email: \"john@example.com\",\n * notifications: true,\n * });\n *\n * // Check the response\n * if (submitter.data?.success) {\n * console.log(\"Settings updated!\");\n * }\n * ```\n */\nexport const useDynamicSubmitter = <TInfo extends RouteWithActionModule>(\n\tpath: TInfo[\"route\"],\n\t...args: TInfo[\"route\"] extends \"undefined\"\n\t\t? HrefArgs<\"/\">\n\t\t: HrefArgs<TInfo[\"route\"]>\n): Omit<\n\tReturnType<typeof useFetcher<TInfo[\"action\"]>>,\n\t\"load\" | \"submit\" | \"Form\"\n> & {\n\t/** Submit with FormData or SubmitTarget (schema type & SubmitTarget) */\n\tsubmit: SubmitFunc<TInfo>;\n\t/** Submit a plain object as JSON (schema type only, defaults to POST) */\n\tsubmitJson: SubmitJsonFunc<TInfo>;\n\t/** Pre-bound Form component with action URL already set (defaults to POST) */\n\tForm: SubmitForm;\n} => {\n\tconst url = useMemo(() => {\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Intentional\n\t\treturn href(path, ...(args as any));\n\t}, [path, args]);\n\n\tconst fetcher = useFetcher<TInfo[\"action\"]>({\n\t\tkey: `submitter-${url}`,\n\t});\n\n\tconst submit: SubmitFunc<TInfo> = useCallback(\n\t\t(target, options) => {\n\t\t\treturn fetcher.submit(target, {\n\t\t\t\t...options,\n\t\t\t\tmethod: (options?.method ??\n\t\t\t\t\t\"POST\") as import(\"react-router\").HTMLFormMethod,\n\t\t\t\taction: url,\n\t\t\t\tencType: \"multipart/form-data\",\n\t\t\t} as Parameters<typeof fetcher.submit>[1]);\n\t\t},\n\t\t[fetcher.submit, url],\n\t);\n\n\tconst submitJson: SubmitJsonFunc<TInfo> = useCallback(\n\t\t(data, options = {}) => {\n\t\t\treturn fetcher.submit(\n\t\t\t\tdata as SubmitTarget,\n\t\t\t\t{\n\t\t\t\t\t...options,\n\t\t\t\t\tmethod: (options.method ??\n\t\t\t\t\t\t\"POST\") as import(\"react-router\").HTMLFormMethod,\n\t\t\t\t\taction: url,\n\t\t\t\t\tencType: \"application/json\",\n\t\t\t\t} as Parameters<typeof fetcher.submit>[1],\n\t\t\t);\n\t\t},\n\t\t[fetcher.submit, url],\n\t);\n\n\tconst OriginalForm = fetcher.Form;\n\n\tconst Form: SubmitForm = useCallback(\n\t\t({ method = \"POST\", ...props }) => {\n\t\t\treturn <OriginalForm action={url} method={method} {...props} />;\n\t\t},\n\t\t[url, OriginalForm],\n\t);\n\n\treturn {\n\t\t...fetcher,\n\t\tsubmit,\n\t\tsubmitJson,\n\t\tForm,\n\t};\n};\n"]}