@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.
- package/README.md +127 -71
- package/dist/{chunk-HX57TC2S.js → chunk-F4324Q33.js} +2 -2
- package/dist/chunk-F4324Q33.js.map +1 -0
- package/dist/chunk-SBJFTOWW.js +3 -0
- package/dist/{chunk-2RLEUOSR.js.map → chunk-SBJFTOWW.js.map} +1 -1
- package/dist/{chunk-5MOCOBGV.js → chunk-UF5QHE5K.js} +2 -2
- package/dist/chunk-UF5QHE5K.js.map +1 -0
- package/dist/chunk-XMGRKSHM.js +183 -0
- package/dist/chunk-XMGRKSHM.js.map +1 -0
- package/dist/formAction.d.ts +41 -15
- package/dist/formAction.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -4
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.js +1 -1
- package/dist/useDynamicFetcher.d.ts +18 -8
- package/dist/useDynamicFetcher.js +1 -1
- package/dist/useDynamicSubmitter.d.ts +300 -133
- package/dist/useDynamicSubmitter.js +1 -1
- package/package.json +4 -4
- package/src/formAction.ts +41 -15
- package/src/types/index.ts +0 -1
- package/src/useDynamicFetcher.ts +18 -8
- package/src/useDynamicSubmitter.tsx +323 -101
- package/dist/chunk-2RLEUOSR.js +0 -3
- package/dist/chunk-5MOCOBGV.js.map +0 -1
- package/dist/chunk-HX57TC2S.js.map +0 -1
- package/dist/chunk-JJN6GBJL.js +0 -55
- package/dist/chunk-JJN6GBJL.js.map +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as react_router from 'react-router';
|
|
2
2
|
import { useFetcher, SubmitTarget, SubmitOptions, FetcherFormProps } from 'react-router';
|
|
3
|
+
import React from 'react';
|
|
3
4
|
import { z } from 'zod';
|
|
4
5
|
import { HrefArgs } from './types/HrefArgs.js';
|
|
5
6
|
import { RouteWithActionModule } from './types/RouteWithActionModule.js';
|
|
@@ -7,119 +8,277 @@ import './types/RegisterPages.js';
|
|
|
7
8
|
import './types/Func.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* export const formSchema = z.object({
|
|
30
|
-
* title: z.string().min(1, "Title is required"),
|
|
31
|
-
* content: z.string().min(10, "Content must be at least 10 characters"),
|
|
32
|
-
* published: z.boolean().optional().default(false),
|
|
33
|
-
* });
|
|
34
|
-
*
|
|
35
|
-
* // Create the action using formAction
|
|
36
|
-
* export const action = formAction({
|
|
37
|
-
* schema: formSchema,
|
|
38
|
-
* handler: async ({ request, params }, formData) => {
|
|
39
|
-
* const postId = params.id;
|
|
40
|
-
* const updated = await db.posts.update({
|
|
41
|
-
* where: { id: postId },
|
|
42
|
-
* data: formData,
|
|
43
|
-
* });
|
|
44
|
-
* return success(updated);
|
|
45
|
-
* },
|
|
46
|
-
* });
|
|
47
|
-
* ```
|
|
48
|
-
*
|
|
49
|
-
* @example
|
|
50
|
-
* ### Using the hook in a component
|
|
51
|
-
*
|
|
52
|
-
* ```tsx
|
|
53
|
-
* import { useDynamicSubmitter } from "@firtoz/router-toolkit";
|
|
54
|
-
*
|
|
55
|
-
* function EditPostForm({ postId }: { postId: string }) {
|
|
56
|
-
* // Type-safe submitter with full inference
|
|
57
|
-
* const submitter = useDynamicSubmitter<typeof import("./admin.posts.$id")>(
|
|
58
|
-
* "/admin/posts/:id",
|
|
59
|
-
* { id: postId }
|
|
60
|
-
* );
|
|
61
|
-
*
|
|
62
|
-
* // submitter.data is the typed response from the action
|
|
63
|
-
* // submitter.state is "idle" | "loading" | "submitting"
|
|
64
|
-
*
|
|
65
|
-
* // Option 1: Submit as JSON (recommended for programmatic submissions)
|
|
66
|
-
* // Defaults to POST if no options provided
|
|
67
|
-
* const handleSubmitJson = async () => {
|
|
68
|
-
* await submitter.submitJson({
|
|
69
|
-
* title: "My Post",
|
|
70
|
-
* content: "Post content here",
|
|
71
|
-
* published: true,
|
|
72
|
-
* });
|
|
73
|
-
* };
|
|
74
|
-
*
|
|
75
|
-
* // Option 2: Submit with FormData or SubmitTarget
|
|
76
|
-
* const handleSubmit = async (formData: FormData) => {
|
|
77
|
-
* await submitter.submit(formData, { method: "POST" });
|
|
78
|
-
* };
|
|
79
|
-
*
|
|
80
|
-
* // Option 3: Use the Form component (defaults to POST)
|
|
81
|
-
* return (
|
|
82
|
-
* <submitter.Form>
|
|
83
|
-
* <input name="title" />
|
|
84
|
-
* <textarea name="content" />
|
|
85
|
-
* <button type="submit">Save</button>
|
|
86
|
-
* </submitter.Form>
|
|
87
|
-
* );
|
|
88
|
-
* }
|
|
89
|
-
* ```
|
|
11
|
+
* An augmentable interface users can modify in their app-code to opt into
|
|
12
|
+
* future-flag-specific types
|
|
13
|
+
*/
|
|
14
|
+
interface Future {
|
|
15
|
+
}
|
|
16
|
+
type MiddlewareEnabled = Future extends {
|
|
17
|
+
v8_middleware: infer T extends boolean;
|
|
18
|
+
} ? T : false;
|
|
19
|
+
/**
|
|
20
|
+
* A context instance used as the key for the `get`/`set` methods of a
|
|
21
|
+
* {@link RouterContextProvider}. Accepts an optional default
|
|
22
|
+
* value to be returned if no value has been set.
|
|
23
|
+
*/
|
|
24
|
+
interface RouterContext<T = unknown> {
|
|
25
|
+
defaultValue?: T;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Provides methods for writing/reading values in application context in a
|
|
29
|
+
* type-safe way. Primarily for usage with [middleware](../../how-to/middleware).
|
|
90
30
|
*
|
|
91
31
|
* @example
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
32
|
+
* import {
|
|
33
|
+
* createContext,
|
|
34
|
+
* RouterContextProvider
|
|
35
|
+
* } from "react-router";
|
|
36
|
+
*
|
|
37
|
+
* const userContext = createContext<User | null>(null);
|
|
38
|
+
* const contextProvider = new RouterContextProvider();
|
|
39
|
+
* contextProvider.set(userContext, getUser());
|
|
40
|
+
* // ^ Type-safe
|
|
41
|
+
* const user = contextProvider.get(userContext);
|
|
42
|
+
* // ^ User
|
|
43
|
+
*
|
|
44
|
+
* @public
|
|
45
|
+
* @category Utils
|
|
46
|
+
* @mode framework
|
|
47
|
+
* @mode data
|
|
48
|
+
*/
|
|
49
|
+
declare class RouterContextProvider {
|
|
50
|
+
#private;
|
|
51
|
+
/**
|
|
52
|
+
* Create a new `RouterContextProvider` instance
|
|
53
|
+
* @param init An optional initial context map to populate the provider with
|
|
54
|
+
*/
|
|
55
|
+
constructor(init?: Map<RouterContext, unknown>);
|
|
56
|
+
/**
|
|
57
|
+
* Access a value from the context. If no value has been set for the context,
|
|
58
|
+
* it will return the context's `defaultValue` if provided, or throw an error
|
|
59
|
+
* if no `defaultValue` was set.
|
|
60
|
+
* @param context The context to get the value for
|
|
61
|
+
* @returns The value for the context, or the context's `defaultValue` if no
|
|
62
|
+
* value was set
|
|
63
|
+
*/
|
|
64
|
+
get<T>(context: RouterContext<T>): T;
|
|
65
|
+
/**
|
|
66
|
+
* Set a value for the context. If the context already has a value set, this
|
|
67
|
+
* will overwrite it.
|
|
68
|
+
*
|
|
69
|
+
* @param context The context to set the value for
|
|
70
|
+
* @param value The value to set for the context
|
|
71
|
+
* @returns {void}
|
|
72
|
+
*/
|
|
73
|
+
set<C extends RouterContext>(context: C, value: C extends RouterContext<infer T> ? T : never): void;
|
|
74
|
+
}
|
|
75
|
+
type DefaultContext = MiddlewareEnabled extends true ? Readonly<RouterContextProvider> : any;
|
|
76
|
+
/**
|
|
77
|
+
* @private
|
|
78
|
+
* Arguments passed to route loader/action functions. Same for now but we keep
|
|
79
|
+
* this as a private implementation detail in case they diverge in the future.
|
|
80
|
+
*/
|
|
81
|
+
interface DataFunctionArgs<Context> {
|
|
82
|
+
/** A {@link https://developer.mozilla.org/en-US/docs/Web/API/Request Fetch Request instance} which you can use to read headers (like cookies, and {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams URLSearchParams} from the request. */
|
|
83
|
+
request: Request;
|
|
84
|
+
/**
|
|
85
|
+
* A URL instance representing the application location being navigated to or fetched.
|
|
86
|
+
* Without `future.unstable_passThroughRequests` enabled, this matches `request.url`.
|
|
87
|
+
* With `future.unstable_passThroughRequests` enabled, this is a normalized
|
|
88
|
+
* URL with React-Router-specific implementation details removed (`.data`
|
|
89
|
+
* suffixes, `index`/`_routes` search params).
|
|
90
|
+
* The URL includes the origin from the request for convenience.
|
|
91
|
+
*/
|
|
92
|
+
unstable_url: URL;
|
|
93
|
+
/**
|
|
94
|
+
* Matched un-interpolated route pattern for the current path (i.e., /blog/:slug).
|
|
95
|
+
* Mostly useful as a identifier to aggregate on for logging/tracing/etc.
|
|
96
|
+
*/
|
|
97
|
+
unstable_pattern: string;
|
|
98
|
+
/**
|
|
99
|
+
* {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route.
|
|
100
|
+
* @example
|
|
101
|
+
* // app/routes.ts
|
|
102
|
+
* route("teams/:teamId", "./team.tsx"),
|
|
103
|
+
*
|
|
104
|
+
* // app/team.tsx
|
|
105
|
+
* export function loader({
|
|
106
|
+
* params,
|
|
107
|
+
* }: Route.LoaderArgs) {
|
|
108
|
+
* params.teamId;
|
|
109
|
+
* // ^ string
|
|
110
|
+
* }
|
|
111
|
+
*/
|
|
112
|
+
params: Params;
|
|
113
|
+
/**
|
|
114
|
+
* This is the context passed in to your server adapter's getLoadContext() function.
|
|
115
|
+
* It's a way to bridge the gap between the adapter's request/response API with your React Router app.
|
|
116
|
+
* It is only applicable if you are using a custom server adapter.
|
|
117
|
+
*/
|
|
118
|
+
context: Context;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Arguments passed to loader functions
|
|
121
122
|
*/
|
|
123
|
+
interface LoaderFunctionArgs<Context = DefaultContext> extends DataFunctionArgs<Context> {
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Arguments passed to action functions
|
|
127
|
+
*/
|
|
128
|
+
interface ActionFunctionArgs<Context = DefaultContext> extends DataFunctionArgs<Context> {
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The parameters that were parsed from the URL path.
|
|
132
|
+
*/
|
|
133
|
+
type Params<Key extends string = string> = {
|
|
134
|
+
readonly [key in Key]: string | undefined;
|
|
135
|
+
};
|
|
136
|
+
declare class DataWithResponseInit<D> {
|
|
137
|
+
type: string;
|
|
138
|
+
data: D;
|
|
139
|
+
init: ResponseInit | null;
|
|
140
|
+
constructor(data: D, init?: ResponseInit);
|
|
141
|
+
}
|
|
122
142
|
|
|
143
|
+
type Serializable = undefined | null | boolean | string | symbol | number | Array<Serializable> | {
|
|
144
|
+
[key: PropertyKey]: Serializable;
|
|
145
|
+
} | bigint | Date | URL | RegExp | Error | Map<Serializable, Serializable> | Set<Serializable> | Promise<Serializable>;
|
|
146
|
+
|
|
147
|
+
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
|
|
148
|
+
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
149
|
+
type Func = (...args: any[]) => unknown;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* A brand that can be applied to a type to indicate that it will serialize
|
|
153
|
+
* to a specific type when transported to the client from a loader.
|
|
154
|
+
* Only use this if you have additional serialization/deserialization logic
|
|
155
|
+
* in your application.
|
|
156
|
+
*/
|
|
157
|
+
type unstable_SerializesTo<T> = {
|
|
158
|
+
unstable__ReactRouter_SerializesTo: [T];
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
type Serialize<T> = T extends unstable_SerializesTo<infer To> ? To : T extends Serializable ? T : T extends (...args: any[]) => unknown ? undefined : T extends Promise<infer U> ? Promise<Serialize<U>> : T extends Map<infer K, infer V> ? Map<Serialize<K>, Serialize<V>> : T extends ReadonlyMap<infer K, infer V> ? ReadonlyMap<Serialize<K>, Serialize<V>> : T extends Set<infer U> ? Set<Serialize<U>> : T extends ReadonlySet<infer U> ? ReadonlySet<Serialize<U>> : T extends [] ? [] : T extends readonly [infer F, ...infer R] ? [Serialize<F>, ...Serialize<R>] : T extends Array<infer U> ? Array<Serialize<U>> : T extends readonly unknown[] ? readonly Serialize<T[number]>[] : T extends Record<any, any> ? {
|
|
162
|
+
[K in keyof T]: Serialize<T[K]>;
|
|
163
|
+
} : undefined;
|
|
164
|
+
type VoidToUndefined<T> = Equal<T, void> extends true ? undefined : T;
|
|
165
|
+
type DataFrom<T> = IsAny<T> extends true ? undefined : T extends Func ? VoidToUndefined<Awaited<ReturnType<T>>> : undefined;
|
|
166
|
+
type ClientData<T> = T extends Response ? never : T extends DataWithResponseInit<infer U> ? U : T;
|
|
167
|
+
type ServerData<T> = T extends Response ? never : T extends DataWithResponseInit<infer U> ? Serialize<U> : Serialize<T>;
|
|
168
|
+
type ServerDataFrom<T> = ServerData<DataFrom<T>>;
|
|
169
|
+
type ClientDataFrom<T> = ClientData<DataFrom<T>>;
|
|
170
|
+
type ClientDataFunctionArgs<Params> = {
|
|
171
|
+
/**
|
|
172
|
+
* A {@link https://developer.mozilla.org/en-US/docs/Web/API/Request Fetch Request instance} which you can use to read the URL, the method, the "content-type" header, and the request body from the request.
|
|
173
|
+
*
|
|
174
|
+
* @note Because client data functions are called before a network request is made, the Request object does not include the headers which the browser automatically adds. React Router infers the "content-type" header from the enc-type of the form that performed the submission.
|
|
175
|
+
**/
|
|
176
|
+
request: Request;
|
|
177
|
+
/**
|
|
178
|
+
* A URL instance representing the application location being navigated to or fetched.
|
|
179
|
+
* Without `future.unstable_passThroughRequests` enabled, this matches `request.url`.
|
|
180
|
+
* With `future.unstable_passThroughRequests` enabled, this is a normalized
|
|
181
|
+
* URL with React-Router-specific implementation details removed (`.data`
|
|
182
|
+
* pathnames, `index`/`_routes` search params).
|
|
183
|
+
* The URL includes the origin from the request for convenience.
|
|
184
|
+
*/
|
|
185
|
+
unstable_url: URL;
|
|
186
|
+
/**
|
|
187
|
+
* {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route.
|
|
188
|
+
* @example
|
|
189
|
+
* // app/routes.ts
|
|
190
|
+
* route("teams/:teamId", "./team.tsx"),
|
|
191
|
+
*
|
|
192
|
+
* // app/team.tsx
|
|
193
|
+
* export function clientLoader({
|
|
194
|
+
* params,
|
|
195
|
+
* }: Route.ClientLoaderArgs) {
|
|
196
|
+
* params.teamId;
|
|
197
|
+
* // ^ string
|
|
198
|
+
* }
|
|
199
|
+
**/
|
|
200
|
+
params: Params;
|
|
201
|
+
/**
|
|
202
|
+
* Matched un-interpolated route pattern for the current path (i.e., /blog/:slug).
|
|
203
|
+
* Mostly useful as a identifier to aggregate on for logging/tracing/etc.
|
|
204
|
+
*/
|
|
205
|
+
unstable_pattern: string;
|
|
206
|
+
/**
|
|
207
|
+
* When `future.v8_middleware` is not enabled, this is undefined.
|
|
208
|
+
*
|
|
209
|
+
* When `future.v8_middleware` is enabled, this is an instance of
|
|
210
|
+
* `RouterContextProvider` and can be used to access context values
|
|
211
|
+
* from your route middlewares. You may pass in initial context values in your
|
|
212
|
+
* `<HydratedRouter getContext>` prop
|
|
213
|
+
*/
|
|
214
|
+
context: Readonly<RouterContextProvider>;
|
|
215
|
+
};
|
|
216
|
+
type SerializeFrom<T> = T extends (...args: infer Args) => unknown ? Args extends [
|
|
217
|
+
ClientLoaderFunctionArgs | ClientActionFunctionArgs | ClientDataFunctionArgs<unknown>
|
|
218
|
+
] ? ClientDataFrom<T> : ServerDataFrom<T> : T;
|
|
219
|
+
/**
|
|
220
|
+
* Arguments passed to a route `clientAction` function
|
|
221
|
+
*/
|
|
222
|
+
type ClientActionFunctionArgs = ActionFunctionArgs & {
|
|
223
|
+
serverAction: <T = unknown>() => Promise<SerializeFrom<T>>;
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* Arguments passed to a route `clientLoader` function
|
|
227
|
+
*/
|
|
228
|
+
type ClientLoaderFunctionArgs = LoaderFunctionArgs & {
|
|
229
|
+
serverLoader: <T = unknown>() => Promise<SerializeFrom<T>>;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Thrown when a new `submit` or `submitJson` runs before a prior returned promise has settled.
|
|
234
|
+
* The new submission proceeds; catch this error if overlapping calls are expected.
|
|
235
|
+
*/
|
|
236
|
+
declare class SubmitterSupersededError extends Error {
|
|
237
|
+
readonly name = "SubmitterSupersededError";
|
|
238
|
+
constructor(message?: string);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Thrown when the component that owns the submitter unmounts before a `submit` /
|
|
242
|
+
* `submitJson` promise has settled.
|
|
243
|
+
*/
|
|
244
|
+
declare class SubmitterUnmountedError extends Error {
|
|
245
|
+
readonly name = "SubmitterUnmountedError";
|
|
246
|
+
constructor(message?: string);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Action payload type on the fetcher (same shape React Router puts on `fetcher.data` after the action runs).
|
|
250
|
+
* Includes `undefined` while idle or in flight—use {@link SubmitterSettledData} for the value after
|
|
251
|
+
* `await submitter.submit` / `await submitter.submitJson`.
|
|
252
|
+
*/
|
|
253
|
+
type DynamicSubmitterData<TInfo extends RouteWithActionModule> = ReturnType<typeof useFetcher<TInfo["action"]>>["data"];
|
|
254
|
+
/**
|
|
255
|
+
* Payload type after a successful `await submitter.submit` / `await submitter.submitJson`.
|
|
256
|
+
* Omits `undefined` from {@link DynamicSubmitterData}: the promise only resolves when `fetcher.data`
|
|
257
|
+
* is defined (otherwise it rejects). Inner success values may still be void / optional `result` for
|
|
258
|
+
* `MaybeError<undefined>` from `formAction` + `success()`.
|
|
259
|
+
*/
|
|
260
|
+
type SubmitterSettledData<TInfo extends RouteWithActionModule> = NonNullable<DynamicSubmitterData<TInfo>>;
|
|
261
|
+
/**
|
|
262
|
+
* Options for {@link useDynamicSubmitter}.
|
|
263
|
+
*/
|
|
264
|
+
type UseDynamicSubmitterOptions = {
|
|
265
|
+
/**
|
|
266
|
+
* Appended to the default fetcher key so multiple submitters can target the same resolved URL
|
|
267
|
+
* without sharing React Router fetcher state. Omit to use the default key for that URL.
|
|
268
|
+
*/
|
|
269
|
+
keySuffix?: string;
|
|
270
|
+
};
|
|
271
|
+
/**
|
|
272
|
+
* React Router `useFetcher` key used by {@link useDynamicSubmitter} for a resolved href.
|
|
273
|
+
* Pass the same string as {@link UseDynamicSubmitterResult.fetcherKey} (or call with the same
|
|
274
|
+
* `resolvedHref` and `keySuffix` as the submitter) so a parallel `useFetcher({ key })` observes
|
|
275
|
+
* the same submission lifecycle.
|
|
276
|
+
*
|
|
277
|
+
* When `keySuffix` is set, it is encoded and joined with a fixed delimiter so arbitrary strings
|
|
278
|
+
* are safe in the key.
|
|
279
|
+
*/
|
|
280
|
+
declare function dynamicSubmitterFetcherKey(resolvedHref: string, keySuffix?: string): string;
|
|
281
|
+
type UseDynamicSubmitterRest<R extends RouteWithActionModule["route"]> = HrefArgs<R> extends readonly [] ? [options?: UseDynamicSubmitterOptions] : [...hrefArgs: HrefArgs<R>, options?: UseDynamicSubmitterOptions];
|
|
123
282
|
/**
|
|
124
283
|
* Function type for submitting form data with a SubmitTarget.
|
|
125
284
|
*
|
|
@@ -137,7 +296,7 @@ import './types/Func.js';
|
|
|
137
296
|
*/
|
|
138
297
|
type SubmitFunc<TModule extends RouteWithActionModule> = (target: z.infer<TModule["formSchema"]> & SubmitTarget, options: Omit<SubmitOptions, "action" | "method" | "encType"> & {
|
|
139
298
|
method: Exclude<SubmitOptions["method"], "GET">;
|
|
140
|
-
}) => Promise<
|
|
299
|
+
}) => Promise<SubmitterSettledData<TModule>>;
|
|
141
300
|
/**
|
|
142
301
|
* Options for submitJson function.
|
|
143
302
|
* Method defaults to "POST" if not specified.
|
|
@@ -167,7 +326,7 @@ type SubmitJsonOptions = Omit<SubmitOptions, "action" | "method" | "encType"> &
|
|
|
167
326
|
* await submitter.submitJson(data, { method: "PUT" });
|
|
168
327
|
* ```
|
|
169
328
|
*/
|
|
170
|
-
type SubmitJsonFunc<TModule extends RouteWithActionModule> = (data: z.infer<TModule["formSchema"]>, options?: SubmitJsonOptions) => Promise<
|
|
329
|
+
type SubmitJsonFunc<TModule extends RouteWithActionModule> = (data: z.infer<TModule["formSchema"]>, options?: SubmitJsonOptions) => Promise<SubmitterSettledData<TModule>>;
|
|
171
330
|
/**
|
|
172
331
|
* Form component type with pre-bound action URL.
|
|
173
332
|
*
|
|
@@ -191,6 +350,18 @@ type SubmitJsonFunc<TModule extends RouteWithActionModule> = (data: z.infer<TMod
|
|
|
191
350
|
type SubmitForm = (props: Omit<FetcherFormProps & React.RefAttributes<HTMLFormElement>, "action" | "method"> & {
|
|
192
351
|
method?: Exclude<SubmitOptions["method"], "GET">;
|
|
193
352
|
}) => React.ReactElement;
|
|
353
|
+
/**
|
|
354
|
+
* Stable object returned by {@link useDynamicSubmitter}: `submit`, `submitJson`, `Form`, and
|
|
355
|
+
* `fetcherKey`. The reference is memoized and does not change when the internal fetcher’s
|
|
356
|
+
* `state` / `data` update.
|
|
357
|
+
*/
|
|
358
|
+
type UseDynamicSubmitterResult<TInfo extends RouteWithActionModule> = {
|
|
359
|
+
submit: SubmitFunc<TInfo>;
|
|
360
|
+
submitJson: SubmitJsonFunc<TInfo>;
|
|
361
|
+
Form: SubmitForm;
|
|
362
|
+
/** Pass to {@link useDynamicSubmitterFetcher} or `useFetcher({ key })` for reactive `state` / `data`. */
|
|
363
|
+
fetcherKey: string;
|
|
364
|
+
};
|
|
194
365
|
/**
|
|
195
366
|
* Creates a type-safe fetcher for submitting forms to dynamic routes.
|
|
196
367
|
*
|
|
@@ -202,15 +373,13 @@ type SubmitForm = (props: Omit<FetcherFormProps & React.RefAttributes<HTMLFormEl
|
|
|
202
373
|
* @template TInfo - The route module type (use `typeof import("./route-file")`)
|
|
203
374
|
*
|
|
204
375
|
* @param path - The route path (must match the route's `route` export)
|
|
205
|
-
* @param
|
|
376
|
+
* @param rest - Route parameters (if any), then optional {@link UseDynamicSubmitterOptions}. For
|
|
377
|
+
* static routes, you may pass only options as the second argument (e.g. `{ keySuffix: "a" }`).
|
|
378
|
+
* Options are recognized only when the object contains exclusively the `keySuffix` key (do not use
|
|
379
|
+
* a route param object whose only field is named `keySuffix` unless it is meant as options).
|
|
206
380
|
*
|
|
207
|
-
* @returns
|
|
208
|
-
*
|
|
209
|
-
* - `submitJson` - Submit a plain object as JSON (schema type only)
|
|
210
|
-
* - `Form` - Pre-bound form component
|
|
211
|
-
* - `data` - Response data from the action (typed)
|
|
212
|
-
* - `state` - Fetcher state ("idle" | "loading" | "submitting")
|
|
213
|
-
* - All other useFetcher properties
|
|
381
|
+
* @returns Stable `{ submit, submitJson, Form, fetcherKey }`. Await the promises for action results;
|
|
382
|
+
* use {@link useDynamicSubmitterFetcher} or local state for reactive loading/data.
|
|
214
383
|
*
|
|
215
384
|
* @example
|
|
216
385
|
* ### Basic usage with route parameters
|
|
@@ -231,26 +400,24 @@ type SubmitForm = (props: Omit<FetcherFormProps & React.RefAttributes<HTMLFormEl
|
|
|
231
400
|
* { userId: "123" }
|
|
232
401
|
* );
|
|
233
402
|
*
|
|
234
|
-
*
|
|
235
|
-
* await submitter.submitJson({
|
|
403
|
+
* const data = await submitter.submitJson({
|
|
236
404
|
* displayName: "John Doe",
|
|
237
405
|
* email: "john@example.com",
|
|
238
406
|
* notifications: true,
|
|
239
407
|
* });
|
|
240
408
|
*
|
|
241
|
-
*
|
|
242
|
-
* if (submitter.data?.success) {
|
|
409
|
+
* if (data.success) {
|
|
243
410
|
* console.log("Settings updated!");
|
|
244
411
|
* }
|
|
245
412
|
* ```
|
|
246
413
|
*/
|
|
247
|
-
declare
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
414
|
+
declare function useDynamicSubmitter<TInfo extends RouteWithActionModule>(path: TInfo["route"], ...rest: UseDynamicSubmitterRest<TInfo["route"]>): UseDynamicSubmitterResult<TInfo>;
|
|
415
|
+
/**
|
|
416
|
+
* React Router `useFetcher` bound to the same key as {@link useDynamicSubmitter}, so `state` /
|
|
417
|
+
* `data` reflect the same submissions as `submitter.submit` / `submitter.Form`.
|
|
418
|
+
*
|
|
419
|
+
* Call at component top level next to `useDynamicSubmitter`.
|
|
420
|
+
*/
|
|
421
|
+
declare function useDynamicSubmitterFetcher<TInfo extends RouteWithActionModule>(submitter: UseDynamicSubmitterResult<TInfo>): react_router.FetcherWithComponents<SerializeFrom<TInfo["action"]>>;
|
|
255
422
|
|
|
256
|
-
export { useDynamicSubmitter };
|
|
423
|
+
export { type DynamicSubmitterData, type SubmitterSettledData, SubmitterSupersededError, SubmitterUnmountedError, type UseDynamicSubmitterOptions, type UseDynamicSubmitterResult, dynamicSubmitterFetcherKey, useDynamicSubmitter, useDynamicSubmitterFetcher };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { useDynamicSubmitter } from './chunk-
|
|
1
|
+
export { SubmitterSupersededError, SubmitterUnmountedError, dynamicSubmitterFetcherKey, useDynamicSubmitter, useDynamicSubmitterFetcher } from './chunk-XMGRKSHM.js';
|
|
2
2
|
//# sourceMappingURL=useDynamicSubmitter.js.map
|
|
3
3
|
//# sourceMappingURL=useDynamicSubmitter.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firtoz/router-toolkit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.1",
|
|
4
4
|
"description": "Type-safe React Router 7 framework mode helpers with enhanced fetching, form submission, and state management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -64,9 +64,8 @@
|
|
|
64
64
|
"url": "https://github.com/firtoz/fullstack-toolkit/issues"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
|
-
"@firtoz/maybe-error": "^1.6.0",
|
|
68
67
|
"react": "^19.2.5",
|
|
69
|
-
"react-router": "^7.14.
|
|
68
|
+
"react-router": "^7.14.2",
|
|
70
69
|
"zod": "^4.3.6"
|
|
71
70
|
},
|
|
72
71
|
"engines": {
|
|
@@ -76,13 +75,14 @@
|
|
|
76
75
|
"access": "public"
|
|
77
76
|
},
|
|
78
77
|
"dependencies": {
|
|
78
|
+
"@firtoz/maybe-error": "^1.6.1",
|
|
79
79
|
"zod-form-data": "^3.0.1"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
82
|
"@testing-library/react": "^16.3.1",
|
|
83
83
|
"@types/jsdom": "^28.0.1",
|
|
84
84
|
"@types/react": "^19.2.14",
|
|
85
|
-
"bun-types": "^1.3.
|
|
85
|
+
"bun-types": "^1.3.13",
|
|
86
86
|
"jsdom": "^29.0.2"
|
|
87
87
|
}
|
|
88
88
|
}
|
package/src/formAction.ts
CHANGED
|
@@ -52,13 +52,26 @@
|
|
|
52
52
|
* @example
|
|
53
53
|
* ### Using with useDynamicSubmitter
|
|
54
54
|
*
|
|
55
|
-
* The route above can be used with `useDynamicSubmitter` for type-safe form submissions
|
|
55
|
+
* The route above can be used with `useDynamicSubmitter` for type-safe form submissions.
|
|
56
|
+
* The hook exposes {@link UseDynamicSubmitterResult.fetcherKey} (built with
|
|
57
|
+
* {@link dynamicSubmitterFetcherKey}) so a parallel `useFetcher` stays aligned; prefer
|
|
58
|
+
* {@link useDynamicSubmitterFetcher} instead of hand-rolling the key.
|
|
59
|
+
*
|
|
60
|
+
* The **optional** {@link useDynamicSubmitterFetcher} below is only for declarative UI that reads
|
|
61
|
+
* `fetcher.state` / `fetcher.data` in render (same submission as `submitter`). For promise-first
|
|
62
|
+
* flows, omit it and use `await submitter.submitJson(...)` plus local `useState` for pending.
|
|
63
|
+
* Use {@link UseDynamicSubmitterOptions.keySuffix} when two submitters target the same URL and
|
|
64
|
+
* must not share fetcher state.
|
|
56
65
|
*
|
|
57
66
|
* ```tsx
|
|
58
|
-
* import {
|
|
67
|
+
* import {
|
|
68
|
+
* useDynamicSubmitter,
|
|
69
|
+
* useDynamicSubmitterFetcher,
|
|
70
|
+
* } from "@firtoz/router-toolkit";
|
|
59
71
|
*
|
|
60
72
|
* function LoginForm() {
|
|
61
73
|
* const submitter = useDynamicSubmitter<typeof import("./auth.login")>("/auth/login");
|
|
74
|
+
* const fetcher = useDynamicSubmitterFetcher(submitter);
|
|
62
75
|
*
|
|
63
76
|
* // Option 1: Submit as JSON (defaults to POST)
|
|
64
77
|
* const handleLoginJson = async () => {
|
|
@@ -69,7 +82,7 @@
|
|
|
69
82
|
* });
|
|
70
83
|
* };
|
|
71
84
|
*
|
|
72
|
-
* // Option 2:
|
|
85
|
+
* // Option 2: Form + useDynamicSubmitterFetcher for reactive state/data
|
|
73
86
|
* return (
|
|
74
87
|
* <submitter.Form>
|
|
75
88
|
* <input name="email" type="email" placeholder="Email" />
|
|
@@ -77,16 +90,16 @@
|
|
|
77
90
|
* <label>
|
|
78
91
|
* <input name="rememberMe" type="checkbox" /> Remember me
|
|
79
92
|
* </label>
|
|
80
|
-
* <button disabled={
|
|
81
|
-
* {
|
|
93
|
+
* <button type="submit" disabled={fetcher.state === "submitting"}>
|
|
94
|
+
* {fetcher.state === "submitting" ? "Logging in..." : "Login"}
|
|
82
95
|
* </button>
|
|
83
96
|
*
|
|
84
|
-
* {
|
|
97
|
+
* {fetcher.data && !fetcher.data.success && (
|
|
85
98
|
* <div className="error">
|
|
86
|
-
* {
|
|
99
|
+
* {fetcher.data.error.type === "validation"
|
|
87
100
|
* ? "Please check your inputs"
|
|
88
|
-
* :
|
|
89
|
-
* ?
|
|
101
|
+
* : fetcher.data.error.type === "handler"
|
|
102
|
+
* ? fetcher.data.error.error
|
|
90
103
|
* : "An unexpected error occurred"}
|
|
91
104
|
* </div>
|
|
92
105
|
* )}
|
|
@@ -139,21 +152,21 @@
|
|
|
139
152
|
*
|
|
140
153
|
* ```tsx
|
|
141
154
|
* import { useDynamicFetcher, useDynamicSubmitter } from "@firtoz/router-toolkit";
|
|
142
|
-
* import { useEffect } from "react";
|
|
155
|
+
* import { useEffect, useState } from "react";
|
|
143
156
|
*
|
|
144
157
|
* function PostEditor({ postId }: { postId: string }) {
|
|
145
|
-
* // Fetch post data
|
|
146
158
|
* const fetcher = useDynamicFetcher<typeof import("./admin.posts.$id")>(
|
|
147
159
|
* "/admin/posts/:id",
|
|
148
160
|
* { id: postId }
|
|
149
161
|
* );
|
|
150
162
|
*
|
|
151
|
-
* // Submit updates
|
|
152
163
|
* const submitter = useDynamicSubmitter<typeof import("./admin.posts.$id")>(
|
|
153
164
|
* "/admin/posts/:id",
|
|
154
165
|
* { id: postId }
|
|
155
166
|
* );
|
|
156
167
|
*
|
|
168
|
+
* const [saving, setSaving] = useState(false);
|
|
169
|
+
*
|
|
157
170
|
* useEffect(() => {
|
|
158
171
|
* fetcher.load();
|
|
159
172
|
* }, [fetcher.load]);
|
|
@@ -165,15 +178,28 @@
|
|
|
165
178
|
* const post = fetcher.data?.post;
|
|
166
179
|
*
|
|
167
180
|
* return (
|
|
168
|
-
* <submitter.Form
|
|
181
|
+
* <submitter.Form
|
|
182
|
+
* method="PUT"
|
|
183
|
+
* onSubmit={async (e) => {
|
|
184
|
+
* e.preventDefault();
|
|
185
|
+
* setSaving(true);
|
|
186
|
+
* try {
|
|
187
|
+
* const fd = new FormData(e.currentTarget);
|
|
188
|
+
* await submitter.submit(fd, { method: "PUT" });
|
|
189
|
+
* fetcher.load();
|
|
190
|
+
* } finally {
|
|
191
|
+
* setSaving(false);
|
|
192
|
+
* }
|
|
193
|
+
* }}
|
|
194
|
+
* >
|
|
169
195
|
* <input name="title" defaultValue={post?.title} />
|
|
170
196
|
* <textarea name="content" defaultValue={post?.content} />
|
|
171
197
|
* <label>
|
|
172
198
|
* <input name="published" type="checkbox" defaultChecked={post?.published} />
|
|
173
199
|
* Published
|
|
174
200
|
* </label>
|
|
175
|
-
* <button disabled={
|
|
176
|
-
* {
|
|
201
|
+
* <button type="submit" disabled={saving}>
|
|
202
|
+
* {saving ? "Saving..." : "Save"}
|
|
177
203
|
* </button>
|
|
178
204
|
* </submitter.Form>
|
|
179
205
|
* );
|
package/src/types/index.ts
CHANGED