@alepha/react 0.9.2 → 0.9.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/README.md +46 -0
- package/dist/index.browser.js +378 -325
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +570 -458
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +305 -213
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +304 -212
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +567 -460
- package/dist/index.js.map +1 -1
- package/package.json +16 -13
- package/src/components/ErrorViewer.tsx +1 -1
- package/src/components/Link.tsx +4 -24
- package/src/components/NestedView.tsx +20 -9
- package/src/components/NotFound.tsx +5 -2
- package/src/descriptors/$page.ts +86 -12
- package/src/errors/Redirection.ts +13 -0
- package/src/hooks/useActive.ts +28 -30
- package/src/hooks/useAlepha.ts +16 -2
- package/src/hooks/useClient.ts +7 -2
- package/src/hooks/useInject.ts +4 -1
- package/src/hooks/useQueryParams.ts +9 -6
- package/src/hooks/useRouter.ts +18 -30
- package/src/hooks/useRouterEvents.ts +7 -4
- package/src/hooks/useRouterState.ts +8 -20
- package/src/hooks/useSchema.ts +10 -15
- package/src/hooks/useStore.ts +9 -8
- package/src/index.browser.ts +11 -11
- package/src/index.shared.ts +4 -5
- package/src/index.ts +21 -30
- package/src/providers/ReactBrowserProvider.ts +155 -65
- package/src/providers/ReactBrowserRouterProvider.ts +132 -0
- package/src/providers/{PageDescriptorProvider.ts → ReactPageProvider.ts} +164 -112
- package/src/providers/ReactServerProvider.ts +100 -68
- package/src/{hooks/RouterHookApi.ts → services/ReactRouter.ts} +75 -61
- package/src/contexts/RouterContext.ts +0 -14
- package/src/errors/RedirectionError.ts +0 -10
- package/src/providers/BrowserRouterProvider.ts +0 -146
- package/src/providers/ReactBrowserRenderer.ts +0 -93
package/src/components/Link.tsx
CHANGED
|
@@ -1,39 +1,19 @@
|
|
|
1
|
+
import type React from "react";
|
|
1
2
|
import type { AnchorHTMLAttributes } from "react";
|
|
2
|
-
import React from "react";
|
|
3
|
-
import { RouterContext } from "../contexts/RouterContext.ts";
|
|
4
|
-
import type { PageDescriptor } from "../descriptors/$page.ts";
|
|
5
3
|
import { useRouter } from "../hooks/useRouter.ts";
|
|
6
4
|
|
|
7
5
|
export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
8
|
-
to: string
|
|
6
|
+
to: string;
|
|
9
7
|
children?: React.ReactNode;
|
|
10
8
|
}
|
|
11
9
|
|
|
12
10
|
const Link = (props: LinkProps) => {
|
|
13
|
-
React.useContext(RouterContext);
|
|
14
|
-
|
|
15
11
|
const router = useRouter();
|
|
16
|
-
|
|
17
|
-
const to = typeof props.to === "string" ? props.to : props.to.options.path;
|
|
18
|
-
if (!to) {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const can = typeof props.to === "string" ? undefined : props.to.options.can;
|
|
23
|
-
if (can && !can()) {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const name = typeof props.to === "string" ? undefined : props.to.options.name;
|
|
28
|
-
|
|
29
|
-
const anchorProps = {
|
|
30
|
-
...props,
|
|
31
|
-
to: undefined,
|
|
32
|
-
};
|
|
12
|
+
const { to, ...anchorProps } = props;
|
|
33
13
|
|
|
34
14
|
return (
|
|
35
15
|
<a {...router.anchor(to)} {...anchorProps}>
|
|
36
|
-
{props.children
|
|
16
|
+
{props.children}
|
|
37
17
|
</a>
|
|
38
18
|
);
|
|
39
19
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import { useContext, useState } from "react";
|
|
3
|
-
import { RouterContext } from "../contexts/RouterContext.ts";
|
|
4
3
|
import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
|
|
4
|
+
import { Redirection } from "../errors/Redirection.ts";
|
|
5
|
+
import { useAlepha } from "../hooks/useAlepha.ts";
|
|
5
6
|
import { useRouterEvents } from "../hooks/useRouterEvents.ts";
|
|
6
7
|
import ErrorBoundary from "./ErrorBoundary.tsx";
|
|
7
8
|
|
|
@@ -31,12 +32,16 @@ export interface NestedViewProps {
|
|
|
31
32
|
* ```
|
|
32
33
|
*/
|
|
33
34
|
const NestedView = (props: NestedViewProps) => {
|
|
34
|
-
const app = useContext(RouterContext);
|
|
35
35
|
const layer = useContext(RouterLayerContext);
|
|
36
36
|
const index = layer?.index ?? 0;
|
|
37
|
+
const alepha = useAlepha();
|
|
38
|
+
const state = alepha.state("react.router.state");
|
|
39
|
+
if (!state) {
|
|
40
|
+
throw new Error("<NestedView/> must be used inside a RouterLayerContext.");
|
|
41
|
+
}
|
|
37
42
|
|
|
38
43
|
const [view, setView] = useState<ReactNode | undefined>(
|
|
39
|
-
|
|
44
|
+
state.layers[index]?.element,
|
|
40
45
|
);
|
|
41
46
|
|
|
42
47
|
useRouterEvents(
|
|
@@ -47,17 +52,23 @@ const NestedView = (props: NestedViewProps) => {
|
|
|
47
52
|
}
|
|
48
53
|
},
|
|
49
54
|
},
|
|
50
|
-
[
|
|
55
|
+
[],
|
|
51
56
|
);
|
|
52
57
|
|
|
53
|
-
if (!app) {
|
|
54
|
-
throw new Error("NestedView must be used within a RouterContext.");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
58
|
const element = view ?? props.children ?? null;
|
|
58
59
|
|
|
59
60
|
return (
|
|
60
|
-
<ErrorBoundary
|
|
61
|
+
<ErrorBoundary
|
|
62
|
+
fallback={(error) => {
|
|
63
|
+
const result = state.onError(error, state); // TODO: onError is not refreshed
|
|
64
|
+
if (result instanceof Redirection) {
|
|
65
|
+
return "Redirection inside ErrorBoundary is not allowed.";
|
|
66
|
+
}
|
|
67
|
+
return result as ReactNode;
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
{element}
|
|
71
|
+
</ErrorBoundary>
|
|
61
72
|
);
|
|
62
73
|
};
|
|
63
74
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
|
|
3
|
+
export default function NotFoundPage(props: { style?: CSSProperties }) {
|
|
2
4
|
return (
|
|
3
5
|
<div
|
|
4
6
|
style={{
|
|
@@ -10,10 +12,11 @@ export default function NotFoundPage() {
|
|
|
10
12
|
textAlign: "center",
|
|
11
13
|
fontFamily: "sans-serif",
|
|
12
14
|
padding: "1rem",
|
|
15
|
+
...props.style,
|
|
13
16
|
}}
|
|
14
17
|
>
|
|
15
18
|
<h1 style={{ fontSize: "1rem", marginBottom: "0.5rem" }}>
|
|
16
|
-
This page does not exist
|
|
19
|
+
404 - This page does not exist
|
|
17
20
|
</h1>
|
|
18
21
|
</div>
|
|
19
22
|
);
|
package/src/descriptors/$page.ts
CHANGED
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
createDescriptor,
|
|
4
4
|
Descriptor,
|
|
5
5
|
KIND,
|
|
6
|
-
NotImplementedError,
|
|
7
6
|
type Static,
|
|
8
7
|
type TSchema,
|
|
9
8
|
} from "@alepha/core";
|
|
@@ -11,7 +10,8 @@ import type { ServerRequest } from "@alepha/server";
|
|
|
11
10
|
import type { ServerRouteCache } from "@alepha/server-cache";
|
|
12
11
|
import type { FC, ReactNode } from "react";
|
|
13
12
|
import type { ClientOnlyProps } from "../components/ClientOnly.tsx";
|
|
14
|
-
import type {
|
|
13
|
+
import type { Redirection } from "../errors/Redirection.ts";
|
|
14
|
+
import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Main descriptor for defining a React route in the application.
|
|
@@ -109,13 +109,52 @@ export interface PageDescriptorOptions<
|
|
|
109
109
|
|
|
110
110
|
can?: () => boolean;
|
|
111
111
|
|
|
112
|
-
errorHandler?: (error: Error) => ReactNode;
|
|
113
|
-
|
|
114
112
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
113
|
+
* Catch any error from the `resolve` function or during `rendering`.
|
|
114
|
+
*
|
|
115
|
+
* Expected to return one of the following:
|
|
116
|
+
* - a ReactNode to render an error page
|
|
117
|
+
* - a Redirection to redirect the user
|
|
118
|
+
* - undefined to let the error propagate
|
|
119
|
+
*
|
|
120
|
+
* If not defined, the error will be thrown and handled by the server or client error handler.
|
|
121
|
+
* If a leaf $page does not define an error handler, the error can be caught by parent pages.
|
|
117
122
|
*
|
|
123
|
+
* @example Catch a 404 from API and render a custom not found component:
|
|
124
|
+
* ```ts
|
|
125
|
+
* resolve: async ({ params, query }) => {
|
|
126
|
+
* api.fetch("/api/resource", { params, query });
|
|
127
|
+
* },
|
|
128
|
+
* errorHandler: (error, context) => {
|
|
129
|
+
* if (HttpError.is(error, 404)) {
|
|
130
|
+
* return <ResourceNotFound />;
|
|
131
|
+
* }
|
|
132
|
+
* }
|
|
133
|
+
* ```
|
|
134
|
+
*
|
|
135
|
+
* @example Catch an 401 error and redirect the user to the login page:
|
|
136
|
+
* ```ts
|
|
137
|
+
* resolve: async ({ params, query }) => {
|
|
138
|
+
* // but the user is not authenticated
|
|
139
|
+
* api.fetch("/api/resource", { params, query });
|
|
140
|
+
* },
|
|
141
|
+
* errorHandler: (error, context) => {
|
|
142
|
+
* if (HttpError.is(error, 401)) {
|
|
143
|
+
* // throwing a Redirection is also valid!
|
|
144
|
+
* return new Redirection("/login");
|
|
145
|
+
* }
|
|
146
|
+
* }
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
errorHandler?: ErrorHandler;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* If true, the page will be considered as a static page, immutable and cacheable.
|
|
118
153
|
* Replace boolean by an object to define static entries. (e.g. list of params/query)
|
|
154
|
+
*
|
|
155
|
+
* For now, it only works with `@alepha/vite` which can pre-render the page at build time.
|
|
156
|
+
*
|
|
157
|
+
* It will act as timeless cached page server-side. You can use `cache` to configure the cache behavior.
|
|
119
158
|
*/
|
|
120
159
|
static?:
|
|
121
160
|
| boolean
|
|
@@ -123,21 +162,44 @@ export interface PageDescriptorOptions<
|
|
|
123
162
|
entries?: Array<Partial<PageRequestConfig<TConfig>>>;
|
|
124
163
|
};
|
|
125
164
|
|
|
165
|
+
cache?: ServerRouteCache;
|
|
166
|
+
|
|
126
167
|
/**
|
|
127
|
-
* If true, the page
|
|
168
|
+
* If true, force the page to be rendered only on the client-side.
|
|
169
|
+
* It uses the `<ClientOnly/>` component to render the page.
|
|
128
170
|
*/
|
|
129
171
|
client?: boolean | ClientOnlyProps;
|
|
130
172
|
|
|
131
|
-
|
|
173
|
+
/**
|
|
174
|
+
* Called before the server response is sent to the client.
|
|
175
|
+
*/
|
|
176
|
+
onServerResponse?: (request: ServerRequest) => any;
|
|
132
177
|
|
|
133
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Called when user leaves the page. (browser only)
|
|
180
|
+
*/
|
|
181
|
+
onLeave?: () => void;
|
|
134
182
|
}
|
|
135
183
|
|
|
184
|
+
export type ErrorHandler = (
|
|
185
|
+
error: Error,
|
|
186
|
+
state: ReactRouterState,
|
|
187
|
+
) => ReactNode | Redirection | undefined;
|
|
188
|
+
|
|
136
189
|
export class PageDescriptor<
|
|
137
190
|
TConfig extends PageConfigSchema = PageConfigSchema,
|
|
138
191
|
TProps extends object = TPropsDefault,
|
|
139
192
|
TPropsParent extends object = TPropsParentDefault,
|
|
140
193
|
> extends Descriptor<PageDescriptorOptions<TConfig, TProps, TPropsParent>> {
|
|
194
|
+
protected onInit() {
|
|
195
|
+
if (this.options.static) {
|
|
196
|
+
this.options.cache ??= {
|
|
197
|
+
provider: "memory",
|
|
198
|
+
ttl: [1, "week"],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
141
203
|
public get name(): string {
|
|
142
204
|
return this.options.name ?? this.config.propertyKey;
|
|
143
205
|
}
|
|
@@ -149,7 +211,17 @@ export class PageDescriptor<
|
|
|
149
211
|
public async render(
|
|
150
212
|
options?: PageDescriptorRenderOptions,
|
|
151
213
|
): Promise<PageDescriptorRenderResult> {
|
|
152
|
-
throw new
|
|
214
|
+
throw new Error("render method is not implemented in this environment");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public match(url: string): boolean {
|
|
218
|
+
// TODO: Implement a way to match the URL against the pathname
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
public pathname(config: any) {
|
|
223
|
+
// TODO: Implement a way to generate the pathname based on the config
|
|
224
|
+
return this.options.path || "";
|
|
153
225
|
}
|
|
154
226
|
}
|
|
155
227
|
|
|
@@ -175,7 +247,7 @@ export interface PageDescriptorRenderOptions {
|
|
|
175
247
|
|
|
176
248
|
export interface PageDescriptorRenderResult {
|
|
177
249
|
html: string;
|
|
178
|
-
|
|
250
|
+
state: ReactRouterState;
|
|
179
251
|
}
|
|
180
252
|
|
|
181
253
|
export interface PageRequestConfig<
|
|
@@ -193,4 +265,6 @@ export interface PageRequestConfig<
|
|
|
193
265
|
export type PageResolve<
|
|
194
266
|
TConfig extends PageConfigSchema = PageConfigSchema,
|
|
195
267
|
TPropsParent extends object = TPropsParentDefault,
|
|
196
|
-
> = PageRequestConfig<TConfig> &
|
|
268
|
+
> = PageRequestConfig<TConfig> &
|
|
269
|
+
TPropsParent &
|
|
270
|
+
Omit<ReactRouterState, "layers" | "onError">;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Used for Redirection during the page loading.
|
|
3
|
+
*
|
|
4
|
+
* Depends on the context, it can be thrown or just returned.
|
|
5
|
+
*/
|
|
6
|
+
export class Redirection extends Error {
|
|
7
|
+
public readonly redirect: string;
|
|
8
|
+
|
|
9
|
+
constructor(redirect: string) {
|
|
10
|
+
super("Redirection");
|
|
11
|
+
this.redirect = redirect;
|
|
12
|
+
}
|
|
13
|
+
}
|
package/src/hooks/useActive.ts
CHANGED
|
@@ -1,49 +1,47 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
|
|
4
|
-
import type { AnchorProps } from "../providers/PageDescriptorProvider.ts";
|
|
5
|
-
import type { HrefLike } from "./RouterHookApi.ts";
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { AnchorProps } from "../providers/ReactPageProvider.ts";
|
|
6
3
|
import { useRouter } from "./useRouter.ts";
|
|
7
|
-
import {
|
|
4
|
+
import { useRouterState } from "./useRouterState.ts";
|
|
8
5
|
|
|
9
|
-
export
|
|
6
|
+
export interface UseActiveOptions {
|
|
7
|
+
href: string;
|
|
8
|
+
startWith?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const useActive = (args: string | UseActiveOptions): UseActiveHook => {
|
|
10
12
|
const router = useRouter();
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
throw new Error("useRouter must be used within a RouterProvider");
|
|
15
|
-
}
|
|
13
|
+
const [isPending, setPending] = useState(false);
|
|
14
|
+
const state = useRouterState();
|
|
15
|
+
const current = state.url.pathname;
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
17
|
+
const options: UseActiveOptions =
|
|
18
|
+
typeof args === "string" ? { href: args } : { ...args, href: args.href };
|
|
19
|
+
const href = options.href;
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const [isPending, setPending] = useState(false);
|
|
25
|
-
const isActive = current === href;
|
|
21
|
+
let isActive =
|
|
22
|
+
current === href || current === `${href}/` || `${current}/` === href;
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
24
|
+
if (options.startWith && !isActive) {
|
|
25
|
+
isActive = current.startsWith(href);
|
|
26
|
+
}
|
|
30
27
|
|
|
31
28
|
return {
|
|
32
|
-
name,
|
|
33
29
|
isPending,
|
|
34
30
|
isActive,
|
|
35
31
|
anchorProps: {
|
|
36
|
-
href,
|
|
37
|
-
onClick: (ev
|
|
38
|
-
ev
|
|
39
|
-
ev
|
|
32
|
+
href: router.base(href),
|
|
33
|
+
onClick: async (ev?: any) => {
|
|
34
|
+
ev?.stopPropagation();
|
|
35
|
+
ev?.preventDefault();
|
|
40
36
|
if (isActive) return;
|
|
41
37
|
if (isPending) return;
|
|
42
38
|
|
|
43
39
|
setPending(true);
|
|
44
|
-
|
|
40
|
+
try {
|
|
41
|
+
await router.go(href);
|
|
42
|
+
} finally {
|
|
45
43
|
setPending(false);
|
|
46
|
-
}
|
|
44
|
+
}
|
|
47
45
|
},
|
|
48
46
|
},
|
|
49
47
|
};
|
package/src/hooks/useAlepha.ts
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Alepha, AlephaError } from "@alepha/core";
|
|
2
2
|
import { useContext } from "react";
|
|
3
3
|
import { AlephaContext } from "../contexts/AlephaContext.ts";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Main Alepha hook.
|
|
7
|
+
*
|
|
8
|
+
* It provides access to the Alepha instance within a React component.
|
|
9
|
+
*
|
|
10
|
+
* With Alepha, you can access the core functionalities of the framework:
|
|
11
|
+
*
|
|
12
|
+
* - alepha.state() for state management
|
|
13
|
+
* - alepha.inject() for dependency injection
|
|
14
|
+
* - alepha.emit() for event handling
|
|
15
|
+
* etc...
|
|
16
|
+
*/
|
|
5
17
|
export const useAlepha = (): Alepha => {
|
|
6
18
|
const alepha = useContext(AlephaContext);
|
|
7
19
|
if (!alepha) {
|
|
8
|
-
throw new
|
|
20
|
+
throw new AlephaError(
|
|
21
|
+
"Hook 'useAlepha()' must be used within an AlephaContext.Provider",
|
|
22
|
+
);
|
|
9
23
|
}
|
|
10
24
|
|
|
11
25
|
return alepha;
|
package/src/hooks/useClient.ts
CHANGED
|
@@ -5,8 +5,13 @@ import {
|
|
|
5
5
|
} from "@alepha/server-links";
|
|
6
6
|
import { useInject } from "./useInject.ts";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Hook to get a virtual client for the specified scope.
|
|
10
|
+
*
|
|
11
|
+
* It's the React-hook version of `$client()`, from `AlephaServerLinks` module.
|
|
12
|
+
*/
|
|
8
13
|
export const useClient = <T extends object>(
|
|
9
|
-
|
|
14
|
+
scope?: ClientScope,
|
|
10
15
|
): HttpVirtualClient<T> => {
|
|
11
|
-
return useInject(LinkProvider).client<T>();
|
|
16
|
+
return useInject(LinkProvider).client<T>(scope);
|
|
12
17
|
};
|
package/src/hooks/useInject.ts
CHANGED
|
@@ -2,8 +2,11 @@ import type { Service } from "@alepha/core";
|
|
|
2
2
|
import { useMemo } from "react";
|
|
3
3
|
import { useAlepha } from "./useAlepha.ts";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Hook to inject a service instance.
|
|
7
|
+
* It's a wrapper of `useAlepha().inject(service)` with a memoization.
|
|
8
|
+
*/
|
|
5
9
|
export const useInject = <T extends object>(service: Service<T>): T => {
|
|
6
10
|
const alepha = useAlepha();
|
|
7
|
-
|
|
8
11
|
return useMemo(() => alepha.inject(service), []);
|
|
9
12
|
};
|
|
@@ -3,12 +3,9 @@ import { useEffect, useState } from "react";
|
|
|
3
3
|
import { useAlepha } from "./useAlepha.ts";
|
|
4
4
|
import { useRouter } from "./useRouter.ts";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
push?: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Not well tested. Use with caution.
|
|
8
|
+
*/
|
|
12
9
|
export const useQueryParams = <T extends TObject>(
|
|
13
10
|
schema: T,
|
|
14
11
|
options: UseQueryParamsHookOptions = {},
|
|
@@ -40,6 +37,12 @@ export const useQueryParams = <T extends TObject>(
|
|
|
40
37
|
|
|
41
38
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
42
39
|
|
|
40
|
+
export interface UseQueryParamsHookOptions {
|
|
41
|
+
format?: "base64" | "querystring";
|
|
42
|
+
key?: string;
|
|
43
|
+
push?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
43
46
|
const encode = (alepha: Alepha, schema: TObject, data: any) => {
|
|
44
47
|
return btoa(JSON.stringify(alepha.parse(schema, data)));
|
|
45
48
|
};
|
package/src/hooks/useRouter.ts
CHANGED
|
@@ -1,32 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
|
|
4
|
-
import { PageDescriptorProvider } from "../providers/PageDescriptorProvider.ts";
|
|
5
|
-
import { ReactBrowserProvider } from "../providers/ReactBrowserProvider.ts";
|
|
6
|
-
import { RouterHookApi } from "./RouterHookApi.ts";
|
|
7
|
-
import { useAlepha } from "./useAlepha.ts";
|
|
1
|
+
import { ReactRouter } from "../services/ReactRouter.ts";
|
|
2
|
+
import { useInject } from "./useInject.ts";
|
|
8
3
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
ctx.context,
|
|
26
|
-
ctx.state,
|
|
27
|
-
layer,
|
|
28
|
-
alepha.isBrowser() ? alepha.inject(ReactBrowserProvider) : undefined,
|
|
29
|
-
),
|
|
30
|
-
[layer],
|
|
31
|
-
);
|
|
4
|
+
/**
|
|
5
|
+
* Use this hook to access the React Router instance.
|
|
6
|
+
*
|
|
7
|
+
* You can add a type parameter to specify the type of your application.
|
|
8
|
+
* This will allow you to use the router in a typesafe way.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* class App {
|
|
12
|
+
* home = $page();
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* const router = useRouter<App>();
|
|
16
|
+
* router.go("home"); // typesafe
|
|
17
|
+
*/
|
|
18
|
+
export const useRouter = <T extends object = any>(): ReactRouter<T> => {
|
|
19
|
+
return useInject(ReactRouter<T>);
|
|
32
20
|
};
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
|
|
3
3
|
import { useAlepha } from "./useAlepha.ts";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Subscribe to various router events.
|
|
7
|
+
*/
|
|
5
8
|
export const useRouterEvents = (
|
|
6
9
|
opts: {
|
|
7
|
-
onBegin?: (ev: { state:
|
|
8
|
-
onEnd?: (ev: { state:
|
|
9
|
-
onError?: (ev: { state:
|
|
10
|
+
onBegin?: (ev: { state: ReactRouterState }) => void;
|
|
11
|
+
onEnd?: (ev: { state: ReactRouterState }) => void;
|
|
12
|
+
onError?: (ev: { state: ReactRouterState; error: Error }) => void;
|
|
10
13
|
} = {},
|
|
11
14
|
deps: any[] = [],
|
|
12
15
|
) => {
|
|
@@ -1,23 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const layer = useContext(RouterLayerContext);
|
|
10
|
-
if (!router || !layer) {
|
|
11
|
-
throw new Error(
|
|
12
|
-
"useRouterState must be used within a RouterContext.Provider",
|
|
13
|
-
);
|
|
1
|
+
import { AlephaError } from "@alepha/core";
|
|
2
|
+
import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
|
|
3
|
+
import { useStore } from "./useStore.ts";
|
|
4
|
+
|
|
5
|
+
export const useRouterState = (): ReactRouterState => {
|
|
6
|
+
const [state] = useStore("react.router.state");
|
|
7
|
+
if (!state) {
|
|
8
|
+
throw new AlephaError("Missing react router state");
|
|
14
9
|
}
|
|
15
|
-
|
|
16
|
-
const [state, setState] = useState(router.state);
|
|
17
|
-
|
|
18
|
-
useRouterEvents({
|
|
19
|
-
onEnd: ({ state }) => setState({ ...state }),
|
|
20
|
-
});
|
|
21
|
-
|
|
22
10
|
return state;
|
|
23
11
|
};
|
package/src/hooks/useSchema.ts
CHANGED
|
@@ -4,11 +4,7 @@ import {
|
|
|
4
4
|
HttpClient,
|
|
5
5
|
type RequestConfigSchema,
|
|
6
6
|
} from "@alepha/server";
|
|
7
|
-
import {
|
|
8
|
-
type HttpClientLink,
|
|
9
|
-
LinkProvider,
|
|
10
|
-
type VirtualAction,
|
|
11
|
-
} from "@alepha/server-links";
|
|
7
|
+
import { LinkProvider, type VirtualAction } from "@alepha/server-links";
|
|
12
8
|
import { useEffect, useState } from "react";
|
|
13
9
|
import { useAlepha } from "./useAlepha.ts";
|
|
14
10
|
import { useInject } from "./useInject.ts";
|
|
@@ -19,7 +15,6 @@ export const useSchema = <TConfig extends RequestConfigSchema>(
|
|
|
19
15
|
const name = action.name;
|
|
20
16
|
const alepha = useAlepha();
|
|
21
17
|
const httpClient = useInject(HttpClient);
|
|
22
|
-
const linkProvider = useInject(LinkProvider);
|
|
23
18
|
const [schema, setSchema] = useState<UseSchemaReturn<TConfig>>(
|
|
24
19
|
ssrSchemaLoading(alepha, name) as UseSchemaReturn<TConfig>,
|
|
25
20
|
);
|
|
@@ -34,7 +29,7 @@ export const useSchema = <TConfig extends RequestConfigSchema>(
|
|
|
34
29
|
};
|
|
35
30
|
|
|
36
31
|
httpClient
|
|
37
|
-
.fetch(`${
|
|
32
|
+
.fetch(`${LinkProvider.path.apiLinks}/${name}/schema`, {}, opts)
|
|
38
33
|
.then((it) => setSchema(it.data as UseSchemaReturn<TConfig>));
|
|
39
34
|
}, [name]);
|
|
40
35
|
|
|
@@ -54,26 +49,26 @@ export const ssrSchemaLoading = (alepha: Alepha, name: string) => {
|
|
|
54
49
|
// server-side rendering (SSR) context
|
|
55
50
|
if (!alepha.isBrowser()) {
|
|
56
51
|
// get user links
|
|
57
|
-
const
|
|
58
|
-
alepha.context.get<{ links: HttpClientLink[] }>("links")?.links ?? [];
|
|
52
|
+
const linkProvider = alepha.inject(LinkProvider);
|
|
59
53
|
|
|
60
54
|
// check if user can access the link
|
|
61
|
-
const can =
|
|
55
|
+
const can = linkProvider
|
|
56
|
+
.getServerLinks()
|
|
57
|
+
.find((link) => link.name === name);
|
|
62
58
|
|
|
63
59
|
// yes!
|
|
64
60
|
if (can) {
|
|
65
61
|
// user-links have no schema, so we need to get it from the provider
|
|
66
|
-
const schema =
|
|
67
|
-
.inject(LinkProvider)
|
|
68
|
-
.links?.find((it) => it.name === name)?.schema;
|
|
62
|
+
const schema = linkProvider.links.find((it) => it.name === name)?.schema;
|
|
69
63
|
|
|
70
64
|
// oh, we have a schema!
|
|
71
65
|
if (schema) {
|
|
72
|
-
// attach to user link, it will be used in the client during hydration
|
|
66
|
+
// attach to user link, it will be used in the client during hydration
|
|
73
67
|
can.schema = schema;
|
|
74
68
|
return schema;
|
|
75
69
|
}
|
|
76
70
|
}
|
|
71
|
+
|
|
77
72
|
return { loading: true };
|
|
78
73
|
}
|
|
79
74
|
|
|
@@ -81,7 +76,7 @@ export const ssrSchemaLoading = (alepha: Alepha, name: string) => {
|
|
|
81
76
|
// check if we have the schema already loaded
|
|
82
77
|
const schema = alepha
|
|
83
78
|
.inject(LinkProvider)
|
|
84
|
-
.links
|
|
79
|
+
.links.find((it) => it.name === name)?.schema;
|
|
85
80
|
|
|
86
81
|
// yes!
|
|
87
82
|
if (schema) {
|
package/src/hooks/useStore.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { State } from "@alepha/core";
|
|
2
|
-
import { useEffect, useState } from "react";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
3
|
import { useAlepha } from "./useAlepha.ts";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -7,8 +7,16 @@ import { useAlepha } from "./useAlepha.ts";
|
|
|
7
7
|
*/
|
|
8
8
|
export const useStore = <Key extends keyof State>(
|
|
9
9
|
key: Key,
|
|
10
|
+
defaultValue?: State[Key],
|
|
10
11
|
): [State[Key], (value: State[Key]) => void] => {
|
|
11
12
|
const alepha = useAlepha();
|
|
13
|
+
|
|
14
|
+
useMemo(() => {
|
|
15
|
+
if (defaultValue != null && alepha.state(key) == null) {
|
|
16
|
+
alepha.state(key, defaultValue);
|
|
17
|
+
}
|
|
18
|
+
}, [defaultValue]);
|
|
19
|
+
|
|
12
20
|
const [state, setState] = useState(alepha.state(key));
|
|
13
21
|
|
|
14
22
|
useEffect(() => {
|
|
@@ -23,13 +31,6 @@ export const useStore = <Key extends keyof State>(
|
|
|
23
31
|
});
|
|
24
32
|
}, []);
|
|
25
33
|
|
|
26
|
-
if (!alepha.isBrowser()) {
|
|
27
|
-
const value = alepha.context.get(key) as State[Key];
|
|
28
|
-
if (value !== null) {
|
|
29
|
-
return [value, (_: State[Key]) => {}] as const;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
34
|
return [
|
|
34
35
|
state,
|
|
35
36
|
(value: State[Key]) => {
|