@alepha/react 0.7.0 → 0.7.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 +1 -1
- package/dist/index.browser.cjs +21 -21
- package/dist/index.browser.js +2 -3
- package/dist/index.cjs +151 -83
- package/dist/index.d.ts +360 -205
- package/dist/index.js +129 -62
- package/dist/{useActive-DjpZBEuB.cjs → useRouterState-AdK-XeM2.cjs} +270 -81
- package/dist/{useActive-BX41CqY8.js → useRouterState-qoMq7Y9J.js} +272 -84
- package/package.json +11 -10
- package/src/components/ClientOnly.tsx +35 -0
- package/src/components/ErrorBoundary.tsx +1 -1
- package/src/components/ErrorViewer.tsx +161 -0
- package/src/components/Link.tsx +9 -3
- package/src/components/NestedView.tsx +18 -3
- package/src/descriptors/$page.ts +139 -30
- package/src/errors/RedirectionError.ts +4 -1
- package/src/hooks/RouterHookApi.ts +42 -5
- package/src/hooks/useAlepha.ts +12 -0
- package/src/hooks/useClient.ts +8 -6
- package/src/hooks/useInject.ts +2 -2
- package/src/hooks/useQueryParams.ts +1 -1
- package/src/hooks/useRouter.ts +6 -0
- package/src/index.browser.ts +1 -1
- package/src/index.shared.ts +11 -5
- package/src/index.ts +3 -4
- package/src/providers/BrowserRouterProvider.ts +1 -1
- package/src/providers/PageDescriptorProvider.ts +72 -21
- package/src/providers/ReactBrowserProvider.ts +5 -8
- package/src/providers/ReactServerProvider.ts +197 -80
- package/dist/index.browser.cjs.map +0 -1
- package/dist/index.browser.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/useActive-BX41CqY8.js.map +0 -1
- package/dist/useActive-DjpZBEuB.cjs.map +0 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useAlepha } from "../hooks/useAlepha.ts";
|
|
3
|
+
|
|
4
|
+
interface ErrorViewerProps {
|
|
5
|
+
error: Error;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// TODO: design this better
|
|
9
|
+
|
|
10
|
+
const ErrorViewer = ({ error }: ErrorViewerProps) => {
|
|
11
|
+
const [expanded, setExpanded] = useState(false);
|
|
12
|
+
const isProduction = useAlepha().isProduction();
|
|
13
|
+
// const status = isHttpError(error) ? error.status : 500;
|
|
14
|
+
|
|
15
|
+
if (isProduction) {
|
|
16
|
+
return <ErrorViewerProduction />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const stackLines = error.stack?.split("\n") ?? [];
|
|
20
|
+
const previewLines = stackLines.slice(0, 5);
|
|
21
|
+
const hiddenLineCount = stackLines.length - previewLines.length;
|
|
22
|
+
|
|
23
|
+
const copyToClipboard = (text: string) => {
|
|
24
|
+
navigator.clipboard.writeText(text).catch((err) => {
|
|
25
|
+
console.error("Clipboard error:", err);
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const styles = {
|
|
30
|
+
container: {
|
|
31
|
+
padding: "24px",
|
|
32
|
+
backgroundColor: "#FEF2F2",
|
|
33
|
+
color: "#7F1D1D",
|
|
34
|
+
border: "1px solid #FECACA",
|
|
35
|
+
borderRadius: "16px",
|
|
36
|
+
boxShadow: "0 8px 24px rgba(0,0,0,0.05)",
|
|
37
|
+
fontFamily: "monospace",
|
|
38
|
+
maxWidth: "768px",
|
|
39
|
+
margin: "40px auto",
|
|
40
|
+
},
|
|
41
|
+
heading: {
|
|
42
|
+
fontSize: "20px",
|
|
43
|
+
fontWeight: "bold",
|
|
44
|
+
marginBottom: "4px",
|
|
45
|
+
},
|
|
46
|
+
name: {
|
|
47
|
+
fontSize: "16px",
|
|
48
|
+
fontWeight: 600,
|
|
49
|
+
},
|
|
50
|
+
message: {
|
|
51
|
+
fontSize: "14px",
|
|
52
|
+
marginBottom: "16px",
|
|
53
|
+
},
|
|
54
|
+
sectionHeader: {
|
|
55
|
+
display: "flex",
|
|
56
|
+
justifyContent: "space-between",
|
|
57
|
+
alignItems: "center",
|
|
58
|
+
fontSize: "12px",
|
|
59
|
+
marginBottom: "4px",
|
|
60
|
+
color: "#991B1B",
|
|
61
|
+
},
|
|
62
|
+
copyButton: {
|
|
63
|
+
fontSize: "12px",
|
|
64
|
+
color: "#DC2626",
|
|
65
|
+
background: "none",
|
|
66
|
+
border: "none",
|
|
67
|
+
cursor: "pointer",
|
|
68
|
+
textDecoration: "underline",
|
|
69
|
+
},
|
|
70
|
+
stackContainer: {
|
|
71
|
+
backgroundColor: "#FEE2E2",
|
|
72
|
+
padding: "12px",
|
|
73
|
+
borderRadius: "8px",
|
|
74
|
+
fontSize: "13px",
|
|
75
|
+
lineHeight: "1.4",
|
|
76
|
+
overflowX: "auto" as const,
|
|
77
|
+
whiteSpace: "pre-wrap" as const,
|
|
78
|
+
},
|
|
79
|
+
expandLine: {
|
|
80
|
+
color: "#F87171",
|
|
81
|
+
cursor: "pointer",
|
|
82
|
+
marginTop: "8px",
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div style={styles.container}>
|
|
88
|
+
<div>
|
|
89
|
+
<div style={styles.heading}>🔥 Error</div>
|
|
90
|
+
<div style={styles.name}>{error.name}</div>
|
|
91
|
+
<div style={styles.message}>{error.message}</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{stackLines.length > 0 && (
|
|
95
|
+
<div>
|
|
96
|
+
<div style={styles.sectionHeader}>
|
|
97
|
+
<span>Stack trace</span>
|
|
98
|
+
<button
|
|
99
|
+
onClick={() => copyToClipboard(error.stack!)}
|
|
100
|
+
style={styles.copyButton}
|
|
101
|
+
>
|
|
102
|
+
Copy all
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
<pre style={styles.stackContainer}>
|
|
106
|
+
{(expanded ? stackLines : previewLines).map((line, i) => (
|
|
107
|
+
<div key={i}>{line}</div>
|
|
108
|
+
))}
|
|
109
|
+
{!expanded && hiddenLineCount > 0 && (
|
|
110
|
+
<div style={styles.expandLine} onClick={() => setExpanded(true)}>
|
|
111
|
+
+ {hiddenLineCount} more lines...
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
</pre>
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export default ErrorViewer;
|
|
122
|
+
|
|
123
|
+
const ErrorViewerProduction = () => {
|
|
124
|
+
const styles = {
|
|
125
|
+
container: {
|
|
126
|
+
padding: "24px",
|
|
127
|
+
backgroundColor: "#FEF2F2",
|
|
128
|
+
color: "#7F1D1D",
|
|
129
|
+
border: "1px solid #FECACA",
|
|
130
|
+
borderRadius: "16px",
|
|
131
|
+
boxShadow: "0 8px 24px rgba(0,0,0,0.05)",
|
|
132
|
+
fontFamily: "monospace",
|
|
133
|
+
maxWidth: "768px",
|
|
134
|
+
margin: "40px auto",
|
|
135
|
+
textAlign: "center" as const,
|
|
136
|
+
},
|
|
137
|
+
heading: {
|
|
138
|
+
fontSize: "20px",
|
|
139
|
+
fontWeight: "bold",
|
|
140
|
+
marginBottom: "8px",
|
|
141
|
+
},
|
|
142
|
+
name: {
|
|
143
|
+
fontSize: "16px",
|
|
144
|
+
fontWeight: 600,
|
|
145
|
+
marginBottom: "4px",
|
|
146
|
+
},
|
|
147
|
+
message: {
|
|
148
|
+
fontSize: "14px",
|
|
149
|
+
opacity: 0.85,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<div style={styles.container}>
|
|
155
|
+
<div style={styles.heading}>🚨 An error occurred</div>
|
|
156
|
+
<div style={styles.message}>
|
|
157
|
+
Something went wrong. Please try again later.
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
};
|
package/src/components/Link.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { OPTIONS } from "@alepha/core";
|
|
2
|
-
import React from "react";
|
|
3
2
|
import type { AnchorHTMLAttributes } from "react";
|
|
3
|
+
import React from "react";
|
|
4
4
|
import { RouterContext } from "../contexts/RouterContext.ts";
|
|
5
5
|
import type { PageDescriptor } from "../descriptors/$page.ts";
|
|
6
6
|
import { useRouter } from "../hooks/useRouter.ts";
|
|
@@ -13,6 +13,8 @@ export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
|
13
13
|
const Link = (props: LinkProps) => {
|
|
14
14
|
React.useContext(RouterContext);
|
|
15
15
|
|
|
16
|
+
const router = useRouter();
|
|
17
|
+
|
|
16
18
|
const to = typeof props.to === "string" ? props.to : props.to[OPTIONS].path;
|
|
17
19
|
if (!to) {
|
|
18
20
|
return null;
|
|
@@ -26,9 +28,13 @@ const Link = (props: LinkProps) => {
|
|
|
26
28
|
const name =
|
|
27
29
|
typeof props.to === "string" ? undefined : props.to[OPTIONS].name;
|
|
28
30
|
|
|
29
|
-
const
|
|
31
|
+
const anchorProps = {
|
|
32
|
+
...props,
|
|
33
|
+
to: undefined,
|
|
34
|
+
};
|
|
35
|
+
|
|
30
36
|
return (
|
|
31
|
-
<a {...router.anchor(to)} {...
|
|
37
|
+
<a {...router.anchor(to)} {...anchorProps}>
|
|
32
38
|
{props.children ?? name}
|
|
33
39
|
</a>
|
|
34
40
|
);
|
|
@@ -10,10 +10,25 @@ export interface NestedViewProps {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* A component that renders the current view of the nested router layer.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* To be simple, it renders the `element` of the current child page of a parent page.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* import { NestedView } from "@alepha/react";
|
|
20
|
+
*
|
|
21
|
+
* class App {
|
|
22
|
+
* parent = $page({
|
|
23
|
+
* component: () => <NestedView />,
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* child = $page({
|
|
27
|
+
* parent: this.root,
|
|
28
|
+
* component: () => <div>Child Page</div>,
|
|
29
|
+
* });
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
17
32
|
*/
|
|
18
33
|
const NestedView = (props: NestedViewProps) => {
|
|
19
34
|
const app = useContext(RouterContext);
|
package/src/descriptors/$page.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
__descriptor,
|
|
3
|
+
type Async,
|
|
4
|
+
KIND,
|
|
5
|
+
NotImplementedError,
|
|
6
|
+
OPTIONS,
|
|
7
|
+
type Static,
|
|
8
|
+
type TSchema,
|
|
9
|
+
} from "@alepha/core";
|
|
10
|
+
import type { ServerRoute } from "@alepha/server";
|
|
3
11
|
import type { FC, ReactNode } from "react";
|
|
4
|
-
import type {
|
|
12
|
+
import type { ClientOnlyProps } from "../components/ClientOnly.tsx";
|
|
5
13
|
import type { PageReactContext } from "../providers/PageDescriptorProvider.ts";
|
|
6
14
|
|
|
7
15
|
const KEY = "PAGE";
|
|
@@ -13,34 +21,100 @@ export interface PageConfigSchema {
|
|
|
13
21
|
|
|
14
22
|
export type TPropsDefault = any;
|
|
15
23
|
|
|
16
|
-
export type TPropsParentDefault =
|
|
24
|
+
export type TPropsParentDefault = {};
|
|
17
25
|
|
|
18
26
|
export interface PageDescriptorOptions<
|
|
19
27
|
TConfig extends PageConfigSchema = PageConfigSchema,
|
|
20
28
|
TProps extends object = TPropsDefault,
|
|
21
29
|
TPropsParent extends object = TPropsParentDefault,
|
|
22
|
-
> {
|
|
30
|
+
> extends Pick<ServerRoute, "cache"> {
|
|
31
|
+
/**
|
|
32
|
+
* Name your page.
|
|
33
|
+
*
|
|
34
|
+
* @default Descriptor key
|
|
35
|
+
*/
|
|
23
36
|
name?: string;
|
|
24
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Optional description of the page.
|
|
40
|
+
*/
|
|
41
|
+
description?: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Add a pathname to the page.
|
|
45
|
+
*
|
|
46
|
+
* Pathname can contain parameters, like `/post/:slug`.
|
|
47
|
+
*
|
|
48
|
+
* @default ""
|
|
49
|
+
*/
|
|
25
50
|
path?: string;
|
|
26
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Add an input schema to define:
|
|
54
|
+
* - `params`: parameters from the pathname.
|
|
55
|
+
* - `query`: query parameters from the URL.
|
|
56
|
+
*/
|
|
27
57
|
schema?: TConfig;
|
|
28
58
|
|
|
29
|
-
|
|
30
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Load data before rendering the page.
|
|
61
|
+
*
|
|
62
|
+
* This function receives
|
|
63
|
+
* - the request context and
|
|
64
|
+
* - the parent props (if page has a parent)
|
|
65
|
+
*
|
|
66
|
+
* In SSR, the returned data will be serialized and sent to the client, then reused during the client-side hydration.
|
|
67
|
+
*
|
|
68
|
+
* Resolve can be stopped by throwing an error, which will be handled by the `errorHandler` function.
|
|
69
|
+
* It's common to throw a `NotFoundError` to display a 404 page.
|
|
70
|
+
*
|
|
71
|
+
* RedirectError can be thrown to redirect the user to another page.
|
|
72
|
+
*/
|
|
73
|
+
resolve?: (context: PageResolve<TConfig, TPropsParent>) => Async<TProps>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The component to render when the page is loaded.
|
|
77
|
+
*
|
|
78
|
+
* If `lazy` is defined, this will be ignored.
|
|
79
|
+
* Prefer using `lazy` to improve the initial loading time.
|
|
80
|
+
*/
|
|
31
81
|
component?: FC<TProps & TPropsParent>;
|
|
32
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Lazy load the component when the page is loaded.
|
|
85
|
+
*
|
|
86
|
+
* It's recommended to use this for components to improve the initial loading time
|
|
87
|
+
* and enable code-splitting.
|
|
88
|
+
*/
|
|
33
89
|
lazy?: () => Promise<{ default: FC<TProps & TPropsParent> }>;
|
|
34
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Set some children pages and make the page a parent page.
|
|
93
|
+
*
|
|
94
|
+
* /!\ Parent page can't be rendered directly. /!\
|
|
95
|
+
*
|
|
96
|
+
* If you still want to render at this pathname, add a child page with an empty path.
|
|
97
|
+
*/
|
|
35
98
|
children?: Array<{ [OPTIONS]: PageDescriptorOptions }>;
|
|
36
99
|
|
|
37
|
-
parent?: { [OPTIONS]: PageDescriptorOptions<
|
|
100
|
+
parent?: { [OPTIONS]: PageDescriptorOptions<PageConfigSchema, TPropsParent> };
|
|
38
101
|
|
|
39
102
|
can?: () => boolean;
|
|
40
103
|
|
|
41
104
|
head?: Head | ((props: TProps, previous?: Head) => Head);
|
|
42
105
|
|
|
43
106
|
errorHandler?: (error: Error) => ReactNode;
|
|
107
|
+
|
|
108
|
+
prerender?:
|
|
109
|
+
| boolean
|
|
110
|
+
| {
|
|
111
|
+
entries?: Array<Partial<PageRequestConfig<TConfig>>>;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* If true, the page will be rendered on the client-side.
|
|
116
|
+
*/
|
|
117
|
+
client?: boolean | ClientOnlyProps;
|
|
44
118
|
}
|
|
45
119
|
|
|
46
120
|
export interface PageDescriptor<
|
|
@@ -51,18 +125,18 @@ export interface PageDescriptor<
|
|
|
51
125
|
[KIND]: typeof KEY;
|
|
52
126
|
[OPTIONS]: PageDescriptorOptions<TConfig, TProps, TPropsParent>;
|
|
53
127
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
onClick: () => void;
|
|
62
|
-
};
|
|
63
|
-
can: () => boolean;
|
|
128
|
+
/**
|
|
129
|
+
* For testing or build purposes, this will render the page (with or without the HTML layout) and return the HTML and context.
|
|
130
|
+
* Only valid for server-side rendering, it will throw an error if called on the client-side.
|
|
131
|
+
*/
|
|
132
|
+
render: (
|
|
133
|
+
options?: PageDescriptorRenderOptions,
|
|
134
|
+
) => Promise<PageDescriptorRenderResult>;
|
|
64
135
|
}
|
|
65
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Main descriptor for defining a React route in the application.
|
|
139
|
+
*/
|
|
66
140
|
export const $page = <
|
|
67
141
|
TConfig extends PageConfigSchema = PageConfigSchema,
|
|
68
142
|
TProps extends object = TPropsDefault,
|
|
@@ -93,18 +167,6 @@ export const $page = <
|
|
|
93
167
|
render: () => {
|
|
94
168
|
throw new NotImplementedError(KEY);
|
|
95
169
|
},
|
|
96
|
-
go: () => {
|
|
97
|
-
throw new NotImplementedError(KEY);
|
|
98
|
-
},
|
|
99
|
-
createAnchorProps: () => {
|
|
100
|
-
throw new NotImplementedError(KEY);
|
|
101
|
-
},
|
|
102
|
-
can: () => {
|
|
103
|
-
if (options.can) {
|
|
104
|
-
return options.can();
|
|
105
|
-
}
|
|
106
|
-
return true;
|
|
107
|
-
},
|
|
108
170
|
};
|
|
109
171
|
};
|
|
110
172
|
|
|
@@ -112,12 +174,59 @@ $page[KIND] = KEY;
|
|
|
112
174
|
|
|
113
175
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
114
176
|
|
|
177
|
+
export interface PageDescriptorRenderOptions {
|
|
178
|
+
params?: Record<string, string>;
|
|
179
|
+
query?: Record<string, string>;
|
|
180
|
+
withLayout?: boolean;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface PageDescriptorRenderResult {
|
|
184
|
+
html: string;
|
|
185
|
+
context: PageReactContext;
|
|
186
|
+
}
|
|
187
|
+
|
|
115
188
|
export interface Head {
|
|
116
189
|
title?: string;
|
|
190
|
+
description?: string;
|
|
117
191
|
titleSeparator?: string;
|
|
118
192
|
htmlAttributes?: Record<string, string>;
|
|
119
193
|
bodyAttributes?: Record<string, string>;
|
|
120
194
|
meta?: Array<{ name: string; content: string }>;
|
|
195
|
+
|
|
196
|
+
// TODO
|
|
197
|
+
keywords?: string[];
|
|
198
|
+
author?: string;
|
|
199
|
+
robots?: string;
|
|
200
|
+
themeColor?: string;
|
|
201
|
+
viewport?:
|
|
202
|
+
| string
|
|
203
|
+
| {
|
|
204
|
+
width?: string;
|
|
205
|
+
height?: string;
|
|
206
|
+
initialScale?: string;
|
|
207
|
+
maximumScale?: string;
|
|
208
|
+
userScalable?: "no" | "yes" | "0" | "1";
|
|
209
|
+
interactiveWidget?:
|
|
210
|
+
| "resizes-visual"
|
|
211
|
+
| "resizes-content"
|
|
212
|
+
| "overlays-content";
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
og?: {
|
|
216
|
+
title?: string;
|
|
217
|
+
description?: string;
|
|
218
|
+
image?: string;
|
|
219
|
+
url?: string;
|
|
220
|
+
type?: string;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
twitter?: {
|
|
224
|
+
card?: string;
|
|
225
|
+
title?: string;
|
|
226
|
+
description?: string;
|
|
227
|
+
image?: string;
|
|
228
|
+
site?: string;
|
|
229
|
+
};
|
|
121
230
|
}
|
|
122
231
|
|
|
123
232
|
export interface PageRequestConfig<
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { HrefLike } from "../hooks/RouterHookApi.ts";
|
|
2
2
|
|
|
3
3
|
export class RedirectionError extends Error {
|
|
4
|
-
|
|
4
|
+
public readonly page: HrefLike;
|
|
5
|
+
|
|
6
|
+
constructor(page: HrefLike) {
|
|
5
7
|
super("Redirection");
|
|
8
|
+
this.page = page;
|
|
6
9
|
}
|
|
7
10
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PageDescriptor } from "../descriptors/$page.ts";
|
|
2
2
|
import type {
|
|
3
3
|
AnchorProps,
|
|
4
|
+
PageRoute,
|
|
4
5
|
RouterState,
|
|
5
6
|
} from "../providers/PageDescriptorProvider.ts";
|
|
6
7
|
import type {
|
|
@@ -10,6 +11,7 @@ import type {
|
|
|
10
11
|
|
|
11
12
|
export class RouterHookApi {
|
|
12
13
|
constructor(
|
|
14
|
+
private readonly pages: PageRoute[],
|
|
13
15
|
private readonly state: RouterState,
|
|
14
16
|
private readonly layer: {
|
|
15
17
|
path: string;
|
|
@@ -74,11 +76,21 @@ export class RouterHookApi {
|
|
|
74
76
|
* @param pathname
|
|
75
77
|
* @param layer
|
|
76
78
|
*/
|
|
77
|
-
public createHref(
|
|
79
|
+
public createHref(
|
|
80
|
+
pathname: HrefLike,
|
|
81
|
+
layer: { path: string } = this.layer,
|
|
82
|
+
options: { params?: Record<string, any> } = {},
|
|
83
|
+
) {
|
|
78
84
|
if (typeof pathname === "object") {
|
|
79
85
|
pathname = pathname.options.path ?? "";
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
if (options.params) {
|
|
89
|
+
for (const [key, value] of Object.entries(options.params)) {
|
|
90
|
+
pathname = pathname.replace(`:${key}`, String(value));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
82
94
|
return pathname.startsWith("/")
|
|
83
95
|
? pathname
|
|
84
96
|
: `${layer.path}/${pathname}`.replace(/\/\/+/g, "/");
|
|
@@ -90,18 +102,43 @@ export class RouterHookApi {
|
|
|
90
102
|
options?: RouterGoOptions,
|
|
91
103
|
): Promise<void>;
|
|
92
104
|
public async go(path: string, options?: RouterGoOptions): Promise<void> {
|
|
93
|
-
|
|
105
|
+
for (const page of this.pages) {
|
|
106
|
+
if (page.name === path) {
|
|
107
|
+
path = page.path ?? "";
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await this.browser?.go(this.createHref(path, this.layer, options), options);
|
|
94
113
|
}
|
|
95
114
|
|
|
96
|
-
public anchor(
|
|
97
|
-
|
|
115
|
+
public anchor(
|
|
116
|
+
path: string,
|
|
117
|
+
options?: { params?: Record<string, any> },
|
|
118
|
+
): AnchorProps;
|
|
119
|
+
public anchor<T extends object>(
|
|
120
|
+
path: keyof VirtualRouter<T>,
|
|
121
|
+
options?: { params?: Record<string, any> },
|
|
122
|
+
): AnchorProps;
|
|
123
|
+
public anchor(
|
|
124
|
+
path: string,
|
|
125
|
+
options: { params?: Record<string, any> } = {},
|
|
126
|
+
): AnchorProps {
|
|
127
|
+
for (const page of this.pages) {
|
|
128
|
+
if (page.name === path) {
|
|
129
|
+
path = page.path ?? "";
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const href = this.createHref(path, this.layer, options);
|
|
98
135
|
return {
|
|
99
136
|
href,
|
|
100
137
|
onClick: (ev: any) => {
|
|
101
138
|
ev.stopPropagation();
|
|
102
139
|
ev.preventDefault();
|
|
103
140
|
|
|
104
|
-
this.go(path).catch(console.error);
|
|
141
|
+
this.go(path, options).catch(console.error);
|
|
105
142
|
},
|
|
106
143
|
};
|
|
107
144
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Alepha } from "@alepha/core";
|
|
2
|
+
import { useContext } from "react";
|
|
3
|
+
import { RouterContext } from "../contexts/RouterContext.ts";
|
|
4
|
+
|
|
5
|
+
export const useAlepha = (): Alepha => {
|
|
6
|
+
const routerContext = useContext(RouterContext);
|
|
7
|
+
if (!routerContext) {
|
|
8
|
+
throw new Error("useAlepha must be used within a RouterProvider");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return routerContext.alepha;
|
|
12
|
+
};
|
package/src/hooks/useClient.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type ClientScope,
|
|
3
|
+
HttpClient,
|
|
4
|
+
type HttpVirtualClient,
|
|
5
|
+
} from "@alepha/server";
|
|
2
6
|
import { useInject } from "./useInject.ts";
|
|
3
7
|
|
|
4
|
-
export const useClient =
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export const useApi = <T extends object>() => {
|
|
8
|
+
export const useClient = <T extends object>(
|
|
9
|
+
_scope?: ClientScope,
|
|
10
|
+
): HttpVirtualClient<T> => {
|
|
9
11
|
return useInject(HttpClient).of<T>();
|
|
10
12
|
};
|
package/src/hooks/useInject.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Service } from "@alepha/core";
|
|
2
2
|
import { useContext, useMemo } from "react";
|
|
3
3
|
import { RouterContext } from "../contexts/RouterContext.ts";
|
|
4
4
|
|
|
5
|
-
export const useInject = <T extends object>(clazz:
|
|
5
|
+
export const useInject = <T extends object>(clazz: Service<T>): T => {
|
|
6
6
|
const ctx = useContext(RouterContext);
|
|
7
7
|
if (!ctx) {
|
|
8
8
|
throw new Error("useRouter must be used within a <RouterProvider>");
|
|
@@ -50,7 +50,7 @@ const encode = (alepha: Alepha, schema: TObject, data: any) => {
|
|
|
50
50
|
const decode = (alepha: Alepha, schema: TObject, data: any) => {
|
|
51
51
|
try {
|
|
52
52
|
return alepha.parse(schema, JSON.parse(atob(decodeURIComponent(data))));
|
|
53
|
-
} catch (
|
|
53
|
+
} catch (_error) {
|
|
54
54
|
return {};
|
|
55
55
|
}
|
|
56
56
|
};
|
package/src/hooks/useRouter.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useContext, useMemo } from "react";
|
|
2
2
|
import { RouterContext } from "../contexts/RouterContext.ts";
|
|
3
3
|
import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
|
|
4
|
+
import { PageDescriptorProvider } from "../providers/PageDescriptorProvider.ts";
|
|
4
5
|
import { ReactBrowserProvider } from "../providers/ReactBrowserProvider.ts";
|
|
5
6
|
import { RouterHookApi } from "./RouterHookApi.ts";
|
|
6
7
|
|
|
@@ -11,9 +12,14 @@ export const useRouter = (): RouterHookApi => {
|
|
|
11
12
|
throw new Error("useRouter must be used within a RouterProvider");
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
const pages = useMemo(() => {
|
|
16
|
+
return ctx.alepha.get(PageDescriptorProvider).getPages();
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
14
19
|
return useMemo(
|
|
15
20
|
() =>
|
|
16
21
|
new RouterHookApi(
|
|
22
|
+
pages,
|
|
17
23
|
ctx.state,
|
|
18
24
|
layer,
|
|
19
25
|
ctx.alepha.isBrowser()
|
package/src/index.browser.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $inject, Alepha
|
|
1
|
+
import { __bind, $inject, Alepha } from "@alepha/core";
|
|
2
2
|
import { $page } from "./descriptors/$page.ts";
|
|
3
3
|
import { BrowserRouterProvider } from "./providers/BrowserRouterProvider.ts";
|
|
4
4
|
import { PageDescriptorProvider } from "./providers/PageDescriptorProvider.ts";
|
package/src/index.shared.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
export { default as
|
|
2
|
-
export { default as Link } from "./components/Link.tsx";
|
|
1
|
+
export { default as ClientOnly } from "./components/ClientOnly.tsx";
|
|
3
2
|
export { default as ErrorBoundary } from "./components/ErrorBoundary.tsx";
|
|
3
|
+
export * from "./components/ErrorViewer.tsx";
|
|
4
|
+
export { default as Link } from "./components/Link.tsx";
|
|
5
|
+
export { default as NestedView } from "./components/NestedView.tsx";
|
|
4
6
|
|
|
5
7
|
export * from "./contexts/RouterContext.ts";
|
|
6
8
|
export * from "./contexts/RouterLayerContext.ts";
|
|
9
|
+
|
|
7
10
|
export * from "./descriptors/$page.ts";
|
|
11
|
+
|
|
12
|
+
export * from "./errors/RedirectionError.ts";
|
|
13
|
+
|
|
8
14
|
export * from "./hooks/RouterHookApi.ts";
|
|
9
|
-
export * from "./hooks/
|
|
15
|
+
export * from "./hooks/useActive.ts";
|
|
16
|
+
export * from "./hooks/useAlepha.ts";
|
|
10
17
|
export * from "./hooks/useClient.ts";
|
|
18
|
+
export * from "./hooks/useInject.ts";
|
|
11
19
|
export * from "./hooks/useQueryParams.ts";
|
|
12
20
|
export * from "./hooks/useRouter.ts";
|
|
13
21
|
export * from "./hooks/useRouterEvents.ts";
|
|
14
22
|
export * from "./hooks/useRouterState.ts";
|
|
15
|
-
export * from "./hooks/useActive.ts";
|
|
16
|
-
export * from "./errors/RedirectionError.ts";
|