@kasperrt/smolter 0.4.2 → 0.4.4
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/.generated/client/hash-history.ts +27 -0
- package/.generated/client/hydration.ts +98 -0
- package/.generated/client/root-data.ts +52 -0
- package/.generated/core/environment.ts +1 -0
- package/.generated/core/navigation.ts +1 -0
- package/.generated/core/prepare-routes.ts +50 -0
- package/.generated/core/presentation.ts +1 -1
- package/.generated/core/responses.ts +12 -0
- package/.generated/core/utils.ts +5 -2
- package/.generated/preact/await.tsx +1 -0
- package/.generated/preact/client.tsx +8 -27
- package/.generated/preact/deferred-hydration.ts +1 -98
- package/.generated/preact/document.tsx +10 -0
- package/.generated/preact/index.ts +11 -2
- package/.generated/preact/lazy-route.ts +7 -0
- package/.generated/preact/navigation.tsx +19 -5
- package/.generated/preact/root-data.ts +1 -45
- package/.generated/preact/route-hooks.tsx +12 -2
- package/.generated/preact/route-types.ts +14 -0
- package/.generated/preact/router.tsx +5 -48
- package/.generated/react/client.tsx +130 -0
- package/.generated/react/document.tsx +235 -0
- package/.generated/react/fetcher.ts +29 -7
- package/.generated/react/hooks.ts +20 -18
- package/.generated/react/index.ts +28 -2
- package/.generated/react/lazy-route.ts +116 -0
- package/.generated/react/link.ts +1 -38
- package/.generated/react/navigation-state.ts +13 -0
- package/.generated/react/navigation.ts +145 -0
- package/.generated/react/outlet.ts +48 -0
- package/.generated/react/root-context.ts +22 -0
- package/.generated/react/route-hooks.ts +159 -0
- package/.generated/react/route-types.ts +91 -0
- package/.generated/react/router.ts +50 -0
- package/.generated/react/runtime.ts +1 -0
- package/.generated/server/application.tsx +18 -250
- package/.generated/server/assets.ts +7 -0
- package/.generated/server/dispatch.ts +87 -0
- package/.generated/server/handler.ts +3 -0
- package/.generated/server/http.ts +2 -0
- package/.generated/server/index.ts +2 -0
- package/.generated/server/node.ts +5 -2
- package/.generated/server/preact.ts +5 -2
- package/.generated/server/react.ts +34 -0
- package/.generated/server/render-application.ts +203 -0
- package/.generated/vite/application/lazy-files.ts +2 -1
- package/.generated/vite/application/vite.ts +12 -4
- package/.generated/vite/react.ts +11 -0
- package/CHANGELOG.md +20 -0
- package/README.md +48 -48
- package/dist/client/hash-history.d.ts +2 -0
- package/dist/client/hash-history.js +26 -0
- package/dist/client/hydration.d.ts +10 -0
- package/dist/{preact/deferred-hydration.js → client/hydration.js} +1 -1
- package/dist/client/root-data.d.ts +14 -0
- package/dist/{preact → client}/root-data.js +1 -1
- package/dist/core/environment.d.ts +1 -0
- package/dist/core/navigation.d.ts +1 -0
- package/dist/core/prepare-routes.d.ts +7 -0
- package/dist/core/prepare-routes.js +28 -0
- package/dist/core/presentation.js +1 -1
- package/dist/core/responses.d.ts +12 -0
- package/dist/core/responses.js +5 -0
- package/dist/core/utils.d.ts +5 -2
- package/dist/core/utils.js +1 -2
- package/dist/preact/await.d.ts +1 -0
- package/dist/preact/await.js +1 -0
- package/dist/preact/client.d.ts +7 -1
- package/dist/preact/client.js +11 -27
- package/dist/preact/deferred-hydration.d.ts +1 -10
- package/dist/preact/document.d.ts +10 -0
- package/dist/preact/document.js +11 -2
- package/dist/preact/index.d.ts +6 -2
- package/dist/preact/index.js +1 -1
- package/dist/preact/lazy-route.d.ts +2 -0
- package/dist/preact/lazy-route.js +6 -0
- package/dist/preact/navigation-state.js +1 -1
- package/dist/preact/navigation.d.ts +6 -0
- package/dist/preact/navigation.js +9 -5
- package/dist/preact/root-data.d.ts +1 -8
- package/dist/preact/route-hooks.d.ts +10 -0
- package/dist/preact/route-hooks.js +13 -3
- package/dist/preact/route-types.d.ts +14 -0
- package/dist/preact/router.d.ts +5 -2
- package/dist/preact/router.js +4 -26
- package/dist/react/client.d.ts +20 -0
- package/dist/react/client.js +86 -0
- package/dist/react/document.d.ts +25 -0
- package/dist/react/document.js +169 -0
- package/dist/react/fetcher.d.ts +8 -4
- package/dist/react/fetcher.js +12 -1
- package/dist/react/form.js +1 -1
- package/dist/react/hooks.d.ts +7 -6
- package/dist/react/index.d.ts +13 -2
- package/dist/react/index.js +7 -2
- package/dist/react/lazy-route.d.ts +22 -0
- package/dist/react/lazy-route.js +57 -0
- package/dist/react/link.d.ts +1 -12
- package/dist/react/navigation-state.d.ts +2 -0
- package/dist/react/navigation-state.js +9 -0
- package/dist/react/navigation.d.ts +33 -0
- package/dist/react/navigation.js +64 -0
- package/dist/react/outlet.d.ts +16 -0
- package/dist/react/outlet.js +26 -0
- package/dist/react/root-context.d.ts +14 -0
- package/dist/react/root-context.js +10 -0
- package/dist/react/route-hooks.d.ts +34 -0
- package/dist/react/route-hooks.js +89 -0
- package/dist/react/route-types.d.ts +69 -0
- package/dist/react/router.d.ts +10 -0
- package/dist/react/router.js +22 -0
- package/dist/react/runtime.d.ts +1 -1
- package/dist/react/runtime.js +1 -1
- package/dist/server/application.d.ts +4 -5
- package/dist/server/application.js +9 -154
- package/dist/server/assets.d.ts +2 -0
- package/dist/server/assets.js +6 -0
- package/dist/server/dispatch.d.ts +5 -0
- package/dist/server/dispatch.js +50 -0
- package/dist/server/handler.d.ts +3 -0
- package/dist/server/http.d.ts +2 -0
- package/dist/server/http.js +2 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +2 -1
- package/dist/server/node.d.ts +5 -1
- package/dist/server/preact.d.ts +5 -1
- package/dist/server/preact.js +2 -1
- package/dist/server/react.d.ts +10 -0
- package/dist/server/react.js +22 -0
- package/dist/server/render-application.d.ts +31 -0
- package/dist/server/render-application.js +118 -0
- package/dist/vite/application/lazy-files.d.ts +1 -1
- package/dist/vite/application/lazy-files.js +2 -2
- package/dist/vite/application/vite.d.ts +2 -1
- package/dist/vite/application/vite.js +12 -5
- package/dist/vite/react.d.ts +4 -0
- package/dist/vite/react.js +9 -1
- package/jsr.json +10 -6
- package/package.json +21 -12
- package/.generated/core/resource-types.ts +0 -44
- package/.generated/core/resources.ts +0 -444
- package/.generated/react/data.ts +0 -25
- package/.generated/react/resources/components.tsx +0 -404
- package/dist/core/resource-types.d.ts +0 -37
- package/dist/core/resources.d.ts +0 -19
- package/dist/core/resources.js +0 -345
- package/dist/react/data.d.ts +0 -5
- package/dist/react/data.js +0 -3
- package/dist/react/link.js +0 -27
- package/dist/react/resources/components.d.ts +0 -64
- package/dist/react/resources/components.js +0 -239
|
@@ -14,8 +14,15 @@ import {
|
|
|
14
14
|
type Router,
|
|
15
15
|
type SubmitOptions,
|
|
16
16
|
} from "../core/index.ts";
|
|
17
|
+
import {
|
|
18
|
+
type Submission,
|
|
19
|
+
type SubmissionOptions,
|
|
20
|
+
submissionBody,
|
|
21
|
+
submissionSearch,
|
|
22
|
+
} from "../core/submission.ts";
|
|
17
23
|
import { useRouter } from "./context.ts";
|
|
18
24
|
import { Form, type FormProps } from "./form.ts";
|
|
25
|
+
import type { RouteData } from "./route-types.ts";
|
|
19
26
|
|
|
20
27
|
import {
|
|
21
28
|
type ComponentType,
|
|
@@ -26,7 +33,7 @@ import {
|
|
|
26
33
|
} from "./runtime.ts";
|
|
27
34
|
|
|
28
35
|
/** Action method and optional explicit route URL for a fetcher submission. */
|
|
29
|
-
export interface FetcherSubmitOptions extends
|
|
36
|
+
export interface FetcherSubmitOptions extends SubmissionOptions {
|
|
30
37
|
action?: string;
|
|
31
38
|
}
|
|
32
39
|
|
|
@@ -35,7 +42,7 @@ export interface TypedFetcher<T> extends Omit<FetcherSnapshot, "data"> {
|
|
|
35
42
|
data?: T;
|
|
36
43
|
Form: ComponentType<FormProps>;
|
|
37
44
|
load: (url?: string) => Promise<Result>;
|
|
38
|
-
submit: (body:
|
|
45
|
+
submit: (body: Submission, options?: FetcherSubmitOptions) => Promise<Result>;
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
/** Create a fetcher for independent route reads and mutations; its data stays local. */
|
|
@@ -47,9 +54,7 @@ export function useFetcher<T extends RouteModule>(
|
|
|
47
54
|
>;
|
|
48
55
|
|
|
49
56
|
/** Create a fetcher for independent route reads and mutations; its data stays local. */
|
|
50
|
-
export function useFetcher<
|
|
51
|
-
T extends AnyDataFunction = DataFunction,
|
|
52
|
-
>(): TypedFetcher<DataOf<T>>;
|
|
57
|
+
export function useFetcher<T = DataFunction>(): TypedFetcher<RouteData<T>>;
|
|
53
58
|
|
|
54
59
|
/** Create a fetcher for independent route reads and mutations; its data stays local. */
|
|
55
60
|
export function useFetcher(route?: RouteDefinition): TypedFetcher<unknown> {
|
|
@@ -68,8 +73,22 @@ export function useFetcher(route?: RouteDefinition): TypedFetcher<unknown> {
|
|
|
68
73
|
Form: (props: FormProps) =>
|
|
69
74
|
createElement(Form, { action: target(), ...props, fetcher: store }),
|
|
70
75
|
load: (url?: string) => store.load(url ?? target()),
|
|
71
|
-
submit: (body:
|
|
72
|
-
|
|
76
|
+
submit: (body: Submission, options: FetcherSubmitOptions = {}) => {
|
|
77
|
+
const action = options.action ?? target();
|
|
78
|
+
const data = submissionBody(body, options.encType);
|
|
79
|
+
const method = options.method?.toUpperCase();
|
|
80
|
+
|
|
81
|
+
if (method === "GET") {
|
|
82
|
+
const url = new URL(action, router.getSnapshot().url);
|
|
83
|
+
url.search = submissionSearch(data);
|
|
84
|
+
|
|
85
|
+
return store.load(url.href);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return store.submit(action, data, {
|
|
89
|
+
method: method as SubmitOptions["method"],
|
|
90
|
+
});
|
|
91
|
+
},
|
|
73
92
|
};
|
|
74
93
|
}, [router, path]);
|
|
75
94
|
|
|
@@ -83,3 +102,6 @@ export function useFetcher(route?: RouteDefinition): TypedFetcher<unknown> {
|
|
|
83
102
|
|
|
84
103
|
return { ...fetcher, ...state };
|
|
85
104
|
}
|
|
105
|
+
|
|
106
|
+
/** A fetcher with a bound form and inferred result data. */
|
|
107
|
+
export type FetcherWithComponents<T> = TypedFetcher<RouteData<T>>;
|
|
@@ -5,43 +5,43 @@
|
|
|
5
5
|
|
|
6
6
|
import type {
|
|
7
7
|
AnyDataFunction,
|
|
8
|
-
DataOf,
|
|
9
8
|
Route,
|
|
10
9
|
RouteModule,
|
|
11
10
|
Router,
|
|
12
11
|
Snapshot,
|
|
13
12
|
} from "../core/index.ts";
|
|
14
|
-
|
|
15
13
|
import { useRouter, useSnapshot } from "./context.ts";
|
|
14
|
+
import type { RouteData } from "./route-types.ts";
|
|
16
15
|
|
|
17
16
|
/** A route module or loader function used for data inference. */
|
|
18
17
|
export type LoaderSource = RouteModule | AnyDataFunction;
|
|
19
18
|
|
|
20
19
|
/** Infer server and client loader data from a route module or function. */
|
|
21
20
|
export type LoaderData<T> = T extends AnyDataFunction
|
|
22
|
-
?
|
|
21
|
+
? RouteData<T>
|
|
23
22
|
: T extends RouteModule
|
|
24
|
-
?
|
|
25
|
-
:
|
|
23
|
+
? RouteData<Extract<T["loader"] | T["clientLoader"], AnyDataFunction>>
|
|
24
|
+
: RouteData<T>;
|
|
26
25
|
|
|
27
26
|
/** Infer server and client action data from a route module or function. */
|
|
28
27
|
export type ActionData<T> = T extends AnyDataFunction
|
|
29
|
-
?
|
|
28
|
+
? RouteData<T>
|
|
30
29
|
: T extends RouteModule
|
|
31
|
-
?
|
|
32
|
-
:
|
|
30
|
+
? RouteData<Extract<T["action"] | T["clientAction"], AnyDataFunction>>
|
|
31
|
+
: RouteData<T>;
|
|
33
32
|
/** Pass a route for inference, or use useLoaderData<typeof loader>() in its module. */
|
|
34
|
-
export function useLoaderData<T
|
|
35
|
-
route?: T,
|
|
36
|
-
): LoaderData<T>
|
|
37
|
-
return useSnapshot(
|
|
38
|
-
|
|
39
|
-
| undefined
|
|
33
|
+
export function useLoaderData<T = RouteModule>(
|
|
34
|
+
route?: T & LoaderSource,
|
|
35
|
+
): LoaderData<T> {
|
|
36
|
+
return useSnapshot(
|
|
37
|
+
"data",
|
|
38
|
+
(route as Route | undefined)?.path,
|
|
39
|
+
) as LoaderData<T>;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/** Read the last page action result, optionally inferring its type from a route. */
|
|
43
|
-
export function useActionData<T
|
|
44
|
-
route?: T,
|
|
43
|
+
export function useActionData<T = RouteModule>(
|
|
44
|
+
route?: T & LoaderSource,
|
|
45
45
|
): ActionData<T> | undefined {
|
|
46
46
|
return useSnapshot("actionData", (route as Route | undefined)?.path) as
|
|
47
47
|
| ActionData<T>
|
|
@@ -49,8 +49,10 @@ export function useActionData<T extends LoaderSource = RouteModule>(
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/** Read path parameters for the active route. */
|
|
52
|
-
export function useParams
|
|
53
|
-
|
|
52
|
+
export function useParams<
|
|
53
|
+
T extends Snapshot["params"] = Snapshot["params"],
|
|
54
|
+
>(): T {
|
|
55
|
+
return useSnapshot("params") as T;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/** Read the current path, search, and hash together with the navigate function. */
|
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* React routing components, typed data hooks and forms.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
2
6
|
export type * from "../core/index.ts";
|
|
3
7
|
export {
|
|
4
8
|
createDataRouter,
|
|
5
9
|
createRouter,
|
|
6
10
|
defineRoute,
|
|
7
11
|
} from "../core/index.ts";
|
|
12
|
+
export type { ApplicationEnvironment } from "../core/environment.ts";
|
|
13
|
+
export * from "../core/responses.ts";
|
|
14
|
+
export {
|
|
15
|
+
readSubmission,
|
|
16
|
+
type Submission,
|
|
17
|
+
type SubmissionOptions,
|
|
18
|
+
} from "../core/submission.ts";
|
|
8
19
|
export {
|
|
9
20
|
RouterProvider,
|
|
10
21
|
type RouterProviderProps,
|
|
@@ -13,5 +24,20 @@ export {
|
|
|
13
24
|
export * from "./fetcher.ts";
|
|
14
25
|
export * from "./form.ts";
|
|
15
26
|
export * from "./hooks.ts";
|
|
16
|
-
export * from "./
|
|
27
|
+
export * from "./navigation.ts";
|
|
28
|
+
export {
|
|
29
|
+
Outlet,
|
|
30
|
+
type OutletProps,
|
|
31
|
+
useMatches,
|
|
32
|
+
useOutletContext,
|
|
33
|
+
} from "./outlet.ts";
|
|
34
|
+
export {
|
|
35
|
+
useNavigation,
|
|
36
|
+
useRootData,
|
|
37
|
+
useRouteError,
|
|
38
|
+
useRouteLoaderData,
|
|
39
|
+
useSubmit,
|
|
40
|
+
} from "./route-hooks.ts";
|
|
41
|
+
export * from "./route-types.ts";
|
|
42
|
+
export { type ApplicationProps, RouterApplication } from "./router.ts";
|
|
17
43
|
export * from "./routes.ts";
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-demand React route modules with shared intent prefetching.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { serverRoute } from "../client/index.ts";
|
|
7
|
+
import type { DataArgs, RouteModule, Router } from "../core/index.ts";
|
|
8
|
+
import { useRouter } from "./context.ts";
|
|
9
|
+
import type { AppRoute } from "./route-types.ts";
|
|
10
|
+
import type { ErrorProps } from "./routes.ts";
|
|
11
|
+
import {
|
|
12
|
+
type ComponentType,
|
|
13
|
+
createElement,
|
|
14
|
+
useSyncExternalStore,
|
|
15
|
+
} from "./runtime.ts";
|
|
16
|
+
|
|
17
|
+
/** Lazy route path, server transport endpoint and available handler capabilities. */
|
|
18
|
+
export interface LazyRouteOptions {
|
|
19
|
+
path: string;
|
|
20
|
+
dataEndpoint?: string;
|
|
21
|
+
id?: string;
|
|
22
|
+
loader?: boolean;
|
|
23
|
+
action?: boolean;
|
|
24
|
+
clientAction?: boolean;
|
|
25
|
+
meta?: boolean;
|
|
26
|
+
shouldRevalidate?: boolean;
|
|
27
|
+
ErrorBoundary?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type LoadedRoute = RouteModule & Pick<AppRoute, "meta">;
|
|
31
|
+
|
|
32
|
+
interface LazyAppRoute extends AppRoute {
|
|
33
|
+
resolveComponent: () => ComponentType | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// A shared wrapper preserves component identity across routes using the same view.
|
|
37
|
+
function LazyView() {
|
|
38
|
+
const router = useRouter<Router>();
|
|
39
|
+
const { path } = useSyncExternalStore(
|
|
40
|
+
router.subscribe,
|
|
41
|
+
router.getSnapshot,
|
|
42
|
+
router.getSnapshot,
|
|
43
|
+
);
|
|
44
|
+
const route = router.routes.find((item) => item.path === path) as
|
|
45
|
+
| LazyAppRoute
|
|
46
|
+
| undefined;
|
|
47
|
+
const Component = route?.resolveComponent();
|
|
48
|
+
|
|
49
|
+
return Component && createElement(Component, {});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Create a route that imports its module on demand and shares the import with intent prefetching. */
|
|
53
|
+
export function lazyRoute(
|
|
54
|
+
options: LazyRouteOptions,
|
|
55
|
+
importer: () => Promise<LoadedRoute>,
|
|
56
|
+
): LazyAppRoute {
|
|
57
|
+
let module: LoadedRoute | undefined;
|
|
58
|
+
let pending: Promise<void> | undefined;
|
|
59
|
+
|
|
60
|
+
function preload() {
|
|
61
|
+
pending ??= importer()
|
|
62
|
+
.then((loaded) => {
|
|
63
|
+
module = serverRoute(options.path, loaded, {
|
|
64
|
+
loader: options.loader,
|
|
65
|
+
action: options.action,
|
|
66
|
+
dataEndpoint: options.dataEndpoint,
|
|
67
|
+
});
|
|
68
|
+
})
|
|
69
|
+
.catch((error: unknown) => {
|
|
70
|
+
pending = undefined;
|
|
71
|
+
throw error;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return pending;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function run(args: DataArgs, action = false) {
|
|
78
|
+
await preload();
|
|
79
|
+
|
|
80
|
+
if (args.signal.aborted) return new Error("Navigation cancelled");
|
|
81
|
+
|
|
82
|
+
const handler = action
|
|
83
|
+
? (module!.clientAction ?? module!.action)
|
|
84
|
+
: (module!.clientLoader ?? module!.loader);
|
|
85
|
+
|
|
86
|
+
// serverRoute supplies serverLoader/serverAction to client handlers.
|
|
87
|
+
return (handler as ((args: DataArgs) => unknown) | undefined)?.(args);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function ErrorBoundary(props: ErrorProps) {
|
|
91
|
+
const Component = module?.ErrorBoundary as
|
|
92
|
+
| ComponentType<ErrorProps>
|
|
93
|
+
| undefined;
|
|
94
|
+
|
|
95
|
+
if (!Component) return props.error.message;
|
|
96
|
+
|
|
97
|
+
return createElement(Component, props);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
path: options.path,
|
|
102
|
+
id: options.id,
|
|
103
|
+
preload,
|
|
104
|
+
default: LazyView,
|
|
105
|
+
resolveComponent: () => module?.default as ComponentType | undefined,
|
|
106
|
+
clientLoader: (args) => run(args),
|
|
107
|
+
...((options.action || options.clientAction) && {
|
|
108
|
+
clientAction: (args: DataArgs) => run(args, true),
|
|
109
|
+
}),
|
|
110
|
+
...(options.meta && { meta: (args) => module?.meta?.(args) ?? [] }),
|
|
111
|
+
...(options.shouldRevalidate && {
|
|
112
|
+
shouldRevalidate: (args) => module?.shouldRevalidate?.(args) ?? true,
|
|
113
|
+
}),
|
|
114
|
+
...(options.ErrorBoundary && { ErrorBoundary }),
|
|
115
|
+
};
|
|
116
|
+
}
|
package/.generated/react/link.ts
CHANGED
|
@@ -1,38 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* React components and typed data hooks. Import from @kasperrt/smolter/react.
|
|
3
|
-
* @module
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { internalURL, navigationClick } from "../core/url.ts";
|
|
7
|
-
import { useRouter } from "./context.ts";
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
type AnchorHTMLAttributes,
|
|
11
|
-
createElement,
|
|
12
|
-
type MouseEvent,
|
|
13
|
-
type ReactNode,
|
|
14
|
-
} from "./runtime.ts";
|
|
15
|
-
|
|
16
|
-
/** Anchor attributes with internal navigation and optional history replacement. */
|
|
17
|
-
export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
18
|
-
href: string;
|
|
19
|
-
replace?: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Render an anchor that handles internal navigation and preserves native modified clicks. */
|
|
23
|
-
export function Link(options: LinkProps): ReactNode {
|
|
24
|
-
const { href, replace, onClick, ...props } = options;
|
|
25
|
-
const router = useRouter();
|
|
26
|
-
const click = (event: MouseEvent<HTMLAnchorElement>) => {
|
|
27
|
-
onClick?.(event);
|
|
28
|
-
|
|
29
|
-
if (!navigationClick(event, event.currentTarget)) return;
|
|
30
|
-
|
|
31
|
-
if (!internalURL(router.getSnapshot().url, href)) return;
|
|
32
|
-
|
|
33
|
-
event.preventDefault();
|
|
34
|
-
void router.navigate(href, options);
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
return createElement("a", { ...props, href, onClick: click });
|
|
38
|
-
}
|
|
1
|
+
export { Link, type LinkProps } from "./navigation.ts";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Router } from "../core/index.ts";
|
|
2
|
+
import { navigationStore } from "../core/navigation.ts";
|
|
3
|
+
import { useSyncExternalStore } from "./runtime.ts";
|
|
4
|
+
|
|
5
|
+
export function useNavigationSnapshot(router: Router) {
|
|
6
|
+
const store = navigationStore(router);
|
|
7
|
+
|
|
8
|
+
return useSyncExternalStore(
|
|
9
|
+
store.subscribe,
|
|
10
|
+
store.getSnapshot,
|
|
11
|
+
store.getSnapshot,
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { matchRoutes, type NavigateOptions } from "../core/index.ts";
|
|
2
|
+
import {
|
|
3
|
+
handoffDocument,
|
|
4
|
+
resolveApplicationURL,
|
|
5
|
+
} from "../core/environment.ts";
|
|
6
|
+
import { navigationClick } from "../core/url.ts";
|
|
7
|
+
import { useRouter, useSnapshot } from "./context.ts";
|
|
8
|
+
import { useNavigation, useURLLocation } from "./route-hooks.ts";
|
|
9
|
+
import type { AppRoute } from "./route-types.ts";
|
|
10
|
+
import type { AnchorHTMLAttributes, MouseEvent, ReactNode } from "./runtime.ts";
|
|
11
|
+
import { createElement } from "./runtime.ts";
|
|
12
|
+
|
|
13
|
+
export type { NavigateOptions } from "../core/index.ts";
|
|
14
|
+
|
|
15
|
+
export type { NavigationType } from "../core/navigation.ts";
|
|
16
|
+
|
|
17
|
+
/** Navigate to a URL or move through browser history using a numeric offset. */
|
|
18
|
+
export type NavigateFunction = (
|
|
19
|
+
to: string | number | Partial<Pick<URL, "pathname" | "search" | "hash">>,
|
|
20
|
+
options?: NavigateOptions,
|
|
21
|
+
) => void;
|
|
22
|
+
|
|
23
|
+
/** Anchor properties and router navigation options, including document reload and prefetch controls. */
|
|
24
|
+
export interface LinkProps
|
|
25
|
+
extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
|
|
26
|
+
to?: string;
|
|
27
|
+
href?: string;
|
|
28
|
+
replace?: boolean;
|
|
29
|
+
prefetch?: string;
|
|
30
|
+
state?: unknown;
|
|
31
|
+
preventScrollReset?: boolean;
|
|
32
|
+
viewTransition?: boolean;
|
|
33
|
+
reloadDocument?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Active and pending flags supplied to NavLink render callbacks. */
|
|
37
|
+
export interface NavLinkRenderProps {
|
|
38
|
+
isActive: boolean;
|
|
39
|
+
isPending: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Link options with matching rules and state-aware class or children callbacks. */
|
|
43
|
+
export interface NavLinkProps
|
|
44
|
+
extends Omit<LinkProps, "className" | "children"> {
|
|
45
|
+
end?: boolean;
|
|
46
|
+
className?: string | ((state: NavLinkRenderProps) => string);
|
|
47
|
+
children?: ReactNode | ((state: NavLinkRenderProps) => ReactNode);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Render an anchor with same-origin router navigation and optional intent prefetching. */
|
|
51
|
+
export function Link(options: LinkProps): ReactNode {
|
|
52
|
+
const {
|
|
53
|
+
href: hrefProp,
|
|
54
|
+
to = hrefProp ?? "/",
|
|
55
|
+
replace,
|
|
56
|
+
preventScrollReset,
|
|
57
|
+
viewTransition,
|
|
58
|
+
prefetch,
|
|
59
|
+
state,
|
|
60
|
+
reloadDocument,
|
|
61
|
+
...props
|
|
62
|
+
} = options;
|
|
63
|
+
const router = useRouter();
|
|
64
|
+
const current = new URL(useSnapshot("url"));
|
|
65
|
+
const destination = resolveApplicationURL(new URL(to, current));
|
|
66
|
+
const internal = destination.origin === current.origin;
|
|
67
|
+
const preload = () => {
|
|
68
|
+
const route = matchRoutes(router.routes, destination.pathname)?.route as
|
|
69
|
+
| AppRoute
|
|
70
|
+
| undefined;
|
|
71
|
+
|
|
72
|
+
void route?.preload?.().catch(() => {});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (!reloadDocument && prefetch !== "none" && internal) {
|
|
76
|
+
for (const name of ["onPointerEnter", "onFocus", "onTouchStart"] as const) {
|
|
77
|
+
const handler = props[name] as ((event: unknown) => void) | undefined;
|
|
78
|
+
|
|
79
|
+
props[name] = (event) => {
|
|
80
|
+
handler?.(event);
|
|
81
|
+
preload();
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return createElement("a", {
|
|
87
|
+
...props,
|
|
88
|
+
href: destination.href,
|
|
89
|
+
onClick(event: MouseEvent<HTMLAnchorElement>) {
|
|
90
|
+
props.onClick?.(event);
|
|
91
|
+
|
|
92
|
+
if (reloadDocument || !navigationClick(event, event.currentTarget))
|
|
93
|
+
return;
|
|
94
|
+
|
|
95
|
+
if (!internal) {
|
|
96
|
+
handoffDocument(event.nativeEvent, destination);
|
|
97
|
+
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
void router.navigate(destination.href, options);
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Render a Link with active-route state and aria-current for the matching destination. */
|
|
108
|
+
export function NavLink({
|
|
109
|
+
href,
|
|
110
|
+
to = href ?? "/",
|
|
111
|
+
end,
|
|
112
|
+
className,
|
|
113
|
+
children,
|
|
114
|
+
...props
|
|
115
|
+
}: NavLinkProps): ReactNode {
|
|
116
|
+
const location = useURLLocation();
|
|
117
|
+
const destination = resolveApplicationURL(new URL(to, location.href));
|
|
118
|
+
const target = destination.pathname;
|
|
119
|
+
const isActive =
|
|
120
|
+
destination.origin === location.origin &&
|
|
121
|
+
(location.pathname === target ||
|
|
122
|
+
(!end && target !== "/" && location.pathname.startsWith(`${target}/`)));
|
|
123
|
+
const pending = useNavigation().location;
|
|
124
|
+
const isPending =
|
|
125
|
+
!!pending &&
|
|
126
|
+
pending.origin === destination.origin &&
|
|
127
|
+
(pending.pathname === target ||
|
|
128
|
+
(!end && target !== "/" && pending.pathname.startsWith(`${target}/`)));
|
|
129
|
+
const state = { isActive, isPending };
|
|
130
|
+
|
|
131
|
+
return createElement(Link, {
|
|
132
|
+
...props,
|
|
133
|
+
to,
|
|
134
|
+
className: typeof className === "function" ? className(state) : className,
|
|
135
|
+
...(isActive && { "aria-current": "page" }),
|
|
136
|
+
children: typeof children === "function" ? children(state) : children,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export {
|
|
141
|
+
useNavigate,
|
|
142
|
+
useNavigationType,
|
|
143
|
+
useSearchParams,
|
|
144
|
+
useURLLocation,
|
|
145
|
+
} from "./route-hooks.ts";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useRouter, useSnapshot } from "./context.ts";
|
|
2
|
+
import { Routes } from "./routes.ts";
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
createElement,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
useContext,
|
|
8
|
+
} from "./runtime.ts";
|
|
9
|
+
|
|
10
|
+
const OutletContext = createContext<unknown>(undefined);
|
|
11
|
+
|
|
12
|
+
/** Optional children and context shared with descendants through useOutletContext. */
|
|
13
|
+
export interface OutletProps {
|
|
14
|
+
context?: unknown;
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Render the active route under a layout's shared context. */
|
|
19
|
+
export function Outlet({ context, children }: OutletProps): ReactNode {
|
|
20
|
+
const inherited = useContext(OutletContext);
|
|
21
|
+
return createElement(
|
|
22
|
+
OutletContext.Provider,
|
|
23
|
+
{ value: context ?? inherited },
|
|
24
|
+
children ??
|
|
25
|
+
createElement(Routes, {
|
|
26
|
+
fallback: createElement("p", { role: "status" }, "Loading…"),
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Read the nearest Outlet's context. */
|
|
32
|
+
export function useOutletContext<T>(): T {
|
|
33
|
+
return useContext(OutletContext) as T;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Read the active flat route match; this is not a nested layout match chain. */
|
|
37
|
+
export function useMatches(): {
|
|
38
|
+
id: string;
|
|
39
|
+
data: unknown;
|
|
40
|
+
params: import("../core/index.ts").Params;
|
|
41
|
+
}[] {
|
|
42
|
+
const router = useRouter();
|
|
43
|
+
const snapshot = useSnapshot();
|
|
44
|
+
const route = router.routes.find((item) => item.path === snapshot.path);
|
|
45
|
+
return [
|
|
46
|
+
{ id: route?.id ?? "", data: snapshot.data, params: snapshot.params },
|
|
47
|
+
];
|
|
48
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Router } from "../core/index.ts";
|
|
2
|
+
import type { AppRoute, RootModule } from "./route-types.ts";
|
|
3
|
+
import { createContext, useContext } from "./runtime.ts";
|
|
4
|
+
|
|
5
|
+
export interface ApplicationContext {
|
|
6
|
+
router: Router;
|
|
7
|
+
routes: AppRoute[];
|
|
8
|
+
root: unknown;
|
|
9
|
+
rootError?: boolean;
|
|
10
|
+
entry?: string;
|
|
11
|
+
streaming?: boolean;
|
|
12
|
+
initialData?: string;
|
|
13
|
+
rootModule?: RootModule;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const Application = createContext<ApplicationContext | null>(null);
|
|
17
|
+
|
|
18
|
+
export function useApplication() {
|
|
19
|
+
const app = useContext(Application);
|
|
20
|
+
if (!app) throw new Error("Root data requires RouterApplication.");
|
|
21
|
+
return app;
|
|
22
|
+
}
|