@alepha/react 0.9.3 → 0.9.5
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 +64 -6
- package/dist/index.browser.js +442 -328
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +644 -482
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +402 -339
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +412 -349
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +641 -484
- package/dist/index.js.map +1 -1
- package/package.json +16 -11
- package/src/components/Link.tsx +2 -5
- package/src/components/NestedView.tsx +164 -19
- package/src/components/NotFound.tsx +1 -1
- package/src/descriptors/$page.ts +100 -5
- package/src/errors/Redirection.ts +8 -5
- package/src/hooks/useActive.ts +25 -35
- package/src/hooks/useAlepha.ts +16 -2
- package/src/hooks/useClient.ts +7 -4
- package/src/hooks/useInject.ts +4 -1
- package/src/hooks/useQueryParams.ts +9 -6
- package/src/hooks/useRouter.ts +18 -31
- package/src/hooks/useRouterEvents.ts +30 -22
- package/src/hooks/useRouterState.ts +8 -20
- package/src/hooks/useSchema.ts +10 -15
- package/src/hooks/useStore.ts +0 -7
- package/src/index.browser.ts +14 -11
- package/src/index.shared.ts +2 -3
- package/src/index.ts +27 -31
- package/src/providers/ReactBrowserProvider.ts +151 -62
- package/src/providers/ReactBrowserRendererProvider.ts +22 -0
- package/src/providers/ReactBrowserRouterProvider.ts +137 -0
- package/src/providers/{PageDescriptorProvider.ts → ReactPageProvider.ts} +121 -104
- package/src/providers/ReactServerProvider.ts +90 -76
- package/src/{hooks/RouterHookApi.ts → services/ReactRouter.ts} +49 -62
- package/src/contexts/RouterContext.ts +0 -14
- package/src/providers/BrowserRouterProvider.ts +0 -155
- package/src/providers/ReactBrowserRenderer.ts +0 -93
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alepha/react",
|
|
3
3
|
"description": "Build server-side rendered (SSR) or single-page React applications.",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22.0.0"
|
|
@@ -17,27 +17,32 @@
|
|
|
17
17
|
"src"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@alepha/core": "0.9.
|
|
21
|
-
"@alepha/
|
|
22
|
-
"@alepha/
|
|
23
|
-
"@alepha/
|
|
24
|
-
"@alepha/server
|
|
25
|
-
"@alepha/server-
|
|
20
|
+
"@alepha/core": "0.9.5",
|
|
21
|
+
"@alepha/datetime": "0.9.5",
|
|
22
|
+
"@alepha/logger": "0.9.5",
|
|
23
|
+
"@alepha/router": "0.9.5",
|
|
24
|
+
"@alepha/server": "0.9.5",
|
|
25
|
+
"@alepha/server-cache": "0.9.5",
|
|
26
|
+
"@alepha/server-links": "0.9.5",
|
|
27
|
+
"@alepha/server-static": "0.9.5",
|
|
26
28
|
"react-dom": "^19.1.1"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|
|
29
|
-
"@
|
|
30
|
-
"@types/react
|
|
31
|
+
"@biomejs/biome": "^2.2.4",
|
|
32
|
+
"@types/react": "^19.1.13",
|
|
33
|
+
"@types/react-dom": "^19.1.9",
|
|
31
34
|
"react": "^19.1.1",
|
|
32
|
-
"tsdown": "^0.
|
|
35
|
+
"tsdown": "^0.15.1",
|
|
36
|
+
"typescript": "^5.9.2",
|
|
33
37
|
"vitest": "^3.2.4"
|
|
34
38
|
},
|
|
35
39
|
"peerDependencies": {
|
|
36
|
-
"@types/react": "^19",
|
|
37
40
|
"react": "^19"
|
|
38
41
|
},
|
|
39
42
|
"scripts": {
|
|
43
|
+
"check": "tsc",
|
|
40
44
|
"test": "vitest run",
|
|
45
|
+
"lint": "biome check --write --unsafe",
|
|
41
46
|
"build": "tsdown -c ../../tsdown.config.ts"
|
|
42
47
|
},
|
|
43
48
|
"homepage": "https://github.com/feunard/alepha",
|
package/src/components/Link.tsx
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import type React from "react";
|
|
2
1
|
import type { AnchorHTMLAttributes } from "react";
|
|
3
2
|
import { useRouter } from "../hooks/useRouter.ts";
|
|
4
3
|
|
|
5
4
|
export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
6
|
-
|
|
7
|
-
children?: React.ReactNode;
|
|
5
|
+
href: string;
|
|
8
6
|
}
|
|
9
7
|
|
|
10
8
|
const Link = (props: LinkProps) => {
|
|
11
9
|
const router = useRouter();
|
|
12
|
-
const { to, ...anchorProps } = props;
|
|
13
10
|
|
|
14
11
|
return (
|
|
15
|
-
<a {...router.anchor(
|
|
12
|
+
<a {...props} {...router.anchor(props.href)}>
|
|
16
13
|
{props.children}
|
|
17
14
|
</a>
|
|
18
15
|
);
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { useContext, useState } from "react";
|
|
3
|
-
import { RouterContext } from "../contexts/RouterContext.ts";
|
|
1
|
+
import { memo, type ReactNode, use, useRef, useState } from "react";
|
|
4
2
|
import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
|
|
3
|
+
import type { PageAnimation } from "../descriptors/$page.ts";
|
|
4
|
+
import { Redirection } from "../errors/Redirection.ts";
|
|
5
5
|
import { useRouterEvents } from "../hooks/useRouterEvents.ts";
|
|
6
|
+
import { useRouterState } from "../hooks/useRouterState.ts";
|
|
7
|
+
import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
|
|
6
8
|
import ErrorBoundary from "./ErrorBoundary.tsx";
|
|
7
9
|
|
|
8
10
|
export interface NestedViewProps {
|
|
9
11
|
children?: ReactNode;
|
|
12
|
+
errorBoundary?: false | ((error: Error) => ReactNode);
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
/**
|
|
@@ -16,7 +19,7 @@ export interface NestedViewProps {
|
|
|
16
19
|
*
|
|
17
20
|
* @example
|
|
18
21
|
* ```tsx
|
|
19
|
-
* import { NestedView } from "
|
|
22
|
+
* import { NestedView } from "alepha/react";
|
|
20
23
|
*
|
|
21
24
|
* class App {
|
|
22
25
|
* parent = $page({
|
|
@@ -31,38 +34,124 @@ export interface NestedViewProps {
|
|
|
31
34
|
* ```
|
|
32
35
|
*/
|
|
33
36
|
const NestedView = (props: NestedViewProps) => {
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const index = layer?.index ?? 0;
|
|
37
|
+
const index = use(RouterLayerContext)?.index ?? 0;
|
|
38
|
+
const state = useRouterState();
|
|
37
39
|
|
|
38
40
|
const [view, setView] = useState<ReactNode | undefined>(
|
|
39
|
-
|
|
41
|
+
state.layers[index]?.element,
|
|
40
42
|
);
|
|
41
43
|
|
|
44
|
+
const [animation, setAnimation] = useState("");
|
|
45
|
+
const animationExitDuration = useRef<number>(0);
|
|
46
|
+
const animationExitNow = useRef<number>(0);
|
|
47
|
+
|
|
42
48
|
useRouterEvents(
|
|
43
49
|
{
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
onBegin: async ({ previous, state }) => {
|
|
51
|
+
// --------- Animations Begin ---------
|
|
52
|
+
const layer = previous.layers[index];
|
|
53
|
+
if (`${state.url.pathname}/`.startsWith(`${layer?.path}/`)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const animationExit = parseAnimation(
|
|
58
|
+
layer.route?.animation,
|
|
59
|
+
state,
|
|
60
|
+
"exit",
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (animationExit) {
|
|
64
|
+
const duration = animationExit.duration || 200;
|
|
65
|
+
animationExitNow.current = Date.now();
|
|
66
|
+
animationExitDuration.current = duration;
|
|
67
|
+
setAnimation(animationExit.animation);
|
|
68
|
+
} else {
|
|
69
|
+
animationExitNow.current = 0;
|
|
70
|
+
animationExitDuration.current = 0;
|
|
71
|
+
setAnimation("");
|
|
72
|
+
}
|
|
73
|
+
// --------- Animations End ---------
|
|
74
|
+
},
|
|
75
|
+
onEnd: async ({ state }) => {
|
|
76
|
+
const layer = state.layers[index];
|
|
77
|
+
|
|
78
|
+
// --------- Animations Begin ---------
|
|
79
|
+
if (animationExitNow.current) {
|
|
80
|
+
const duration = animationExitDuration.current;
|
|
81
|
+
const diff = Date.now() - animationExitNow.current;
|
|
82
|
+
if (diff < duration) {
|
|
83
|
+
await new Promise((resolve) =>
|
|
84
|
+
setTimeout(resolve, duration - diff),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
47
87
|
}
|
|
48
|
-
|
|
49
|
-
|
|
88
|
+
// --------- Animations End ---------
|
|
89
|
+
|
|
90
|
+
if (!layer?.cache) {
|
|
91
|
+
setView(layer?.element);
|
|
92
|
+
|
|
93
|
+
// --------- Animations Begin ---------
|
|
94
|
+
const animationEnter = parseAnimation(
|
|
95
|
+
layer?.route?.animation,
|
|
96
|
+
state,
|
|
97
|
+
"enter",
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (animationEnter) {
|
|
101
|
+
setAnimation(animationEnter.animation);
|
|
102
|
+
} else {
|
|
103
|
+
setAnimation("");
|
|
104
|
+
}
|
|
105
|
+
// --------- Animations End ---------
|
|
50
106
|
}
|
|
51
107
|
},
|
|
52
108
|
},
|
|
53
|
-
[
|
|
109
|
+
[],
|
|
54
110
|
);
|
|
55
111
|
|
|
56
|
-
|
|
57
|
-
|
|
112
|
+
let element = view ?? props.children ?? null;
|
|
113
|
+
|
|
114
|
+
// --------- Animations Begin ---------
|
|
115
|
+
if (animation) {
|
|
116
|
+
element = (
|
|
117
|
+
<div
|
|
118
|
+
style={{
|
|
119
|
+
display: "flex",
|
|
120
|
+
flex: 1,
|
|
121
|
+
height: "100%",
|
|
122
|
+
width: "100%",
|
|
123
|
+
position: "relative",
|
|
124
|
+
overflow: "hidden",
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
<div
|
|
128
|
+
style={{ height: "100%", width: "100%", display: "flex", animation }}
|
|
129
|
+
>
|
|
130
|
+
{element}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
58
134
|
}
|
|
135
|
+
// --------- Animations End ---------
|
|
59
136
|
|
|
60
|
-
|
|
137
|
+
if (props.errorBoundary === false) {
|
|
138
|
+
return <>{element}</>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (props.errorBoundary) {
|
|
142
|
+
return (
|
|
143
|
+
<ErrorBoundary fallback={props.errorBoundary}>{element}</ErrorBoundary>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
61
146
|
|
|
62
147
|
return (
|
|
63
148
|
<ErrorBoundary
|
|
64
149
|
fallback={(error) => {
|
|
65
|
-
|
|
150
|
+
const result = state.onError(error, state); // TODO: onError is not refreshed
|
|
151
|
+
if (result instanceof Redirection) {
|
|
152
|
+
return "Redirection inside ErrorBoundary is not allowed.";
|
|
153
|
+
}
|
|
154
|
+
return result as ReactNode;
|
|
66
155
|
}}
|
|
67
156
|
>
|
|
68
157
|
{element}
|
|
@@ -70,4 +159,60 @@ const NestedView = (props: NestedViewProps) => {
|
|
|
70
159
|
);
|
|
71
160
|
};
|
|
72
161
|
|
|
73
|
-
export default NestedView;
|
|
162
|
+
export default memo(NestedView);
|
|
163
|
+
|
|
164
|
+
function parseAnimation(
|
|
165
|
+
animationLike: PageAnimation | undefined,
|
|
166
|
+
state: ReactRouterState,
|
|
167
|
+
type: "enter" | "exit" = "enter",
|
|
168
|
+
):
|
|
169
|
+
| {
|
|
170
|
+
duration: number;
|
|
171
|
+
animation: string;
|
|
172
|
+
}
|
|
173
|
+
| undefined {
|
|
174
|
+
if (!animationLike) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const DEFAULT_DURATION = 300;
|
|
179
|
+
|
|
180
|
+
const animation =
|
|
181
|
+
typeof animationLike === "function" ? animationLike(state) : animationLike;
|
|
182
|
+
|
|
183
|
+
if (typeof animation === "string") {
|
|
184
|
+
if (type === "exit") {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
duration: DEFAULT_DURATION,
|
|
189
|
+
animation: `${DEFAULT_DURATION}ms ${animation}`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (typeof animation === "object") {
|
|
194
|
+
const anim = animation[type];
|
|
195
|
+
const duration =
|
|
196
|
+
typeof anim === "object"
|
|
197
|
+
? (anim.duration ?? DEFAULT_DURATION)
|
|
198
|
+
: DEFAULT_DURATION;
|
|
199
|
+
const name = typeof anim === "object" ? anim.name : anim;
|
|
200
|
+
|
|
201
|
+
if (type === "exit") {
|
|
202
|
+
const timing = typeof anim === "object" ? (anim.timing ?? "") : "";
|
|
203
|
+
return {
|
|
204
|
+
duration,
|
|
205
|
+
animation: `${duration}ms ${timing} ${name}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const timing = typeof anim === "object" ? (anim.timing ?? "") : "";
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
duration,
|
|
213
|
+
animation: `${duration}ms ${timing} ${name}`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
package/src/descriptors/$page.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AlephaError,
|
|
2
3
|
type Async,
|
|
3
4
|
createDescriptor,
|
|
4
5
|
Descriptor,
|
|
@@ -11,7 +12,7 @@ import type { ServerRouteCache } from "@alepha/server-cache";
|
|
|
11
12
|
import type { FC, ReactNode } from "react";
|
|
12
13
|
import type { ClientOnlyProps } from "../components/ClientOnly.tsx";
|
|
13
14
|
import type { Redirection } from "../errors/Redirection.ts";
|
|
14
|
-
import type {
|
|
15
|
+
import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Main descriptor for defining a React route in the application.
|
|
@@ -179,11 +180,55 @@ export interface PageDescriptorOptions<
|
|
|
179
180
|
* Called when user leaves the page. (browser only)
|
|
180
181
|
*/
|
|
181
182
|
onLeave?: () => void;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @experimental
|
|
186
|
+
*
|
|
187
|
+
* Add a css animation when the page is loaded or unloaded.
|
|
188
|
+
* It uses CSS animations, so you need to define the keyframes in your CSS.
|
|
189
|
+
*
|
|
190
|
+
* @example Simple animation name
|
|
191
|
+
* ```ts
|
|
192
|
+
* animation: "fadeIn"
|
|
193
|
+
* ```
|
|
194
|
+
*
|
|
195
|
+
* CSS example:
|
|
196
|
+
* ```css
|
|
197
|
+
* @keyframes fadeIn {
|
|
198
|
+
* from { opacity: 0; }
|
|
199
|
+
* to { opacity: 1; }
|
|
200
|
+
* }
|
|
201
|
+
* ```
|
|
202
|
+
*
|
|
203
|
+
* @example Detailed animation
|
|
204
|
+
* ```ts
|
|
205
|
+
* animation: {
|
|
206
|
+
* enter: { name: "fadeIn", duration: 300 },
|
|
207
|
+
* exit: { name: "fadeOut", duration: 200, timing: "ease-in-out" },
|
|
208
|
+
* }
|
|
209
|
+
* ```
|
|
210
|
+
*
|
|
211
|
+
* @example Only exit animation
|
|
212
|
+
* ```ts
|
|
213
|
+
* animation: {
|
|
214
|
+
* exit: "fadeOut"
|
|
215
|
+
* }
|
|
216
|
+
* ```
|
|
217
|
+
*
|
|
218
|
+
* @example With custom timing function
|
|
219
|
+
* ```ts
|
|
220
|
+
* animation: {
|
|
221
|
+
* enter: { name: "fadeIn", duration: 300, timing: "cubic-bezier(0.4, 0, 0.2, 1)" },
|
|
222
|
+
* exit: { name: "fadeOut", duration: 200, timing: "ease-in-out" },
|
|
223
|
+
* }
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
animation?: PageAnimation;
|
|
182
227
|
}
|
|
183
228
|
|
|
184
229
|
export type ErrorHandler = (
|
|
185
230
|
error: Error,
|
|
186
|
-
|
|
231
|
+
state: ReactRouterState,
|
|
187
232
|
) => ReactNode | Redirection | undefined;
|
|
188
233
|
|
|
189
234
|
export class PageDescriptor<
|
|
@@ -211,7 +256,28 @@ export class PageDescriptor<
|
|
|
211
256
|
public async render(
|
|
212
257
|
options?: PageDescriptorRenderOptions,
|
|
213
258
|
): Promise<PageDescriptorRenderResult> {
|
|
214
|
-
throw new
|
|
259
|
+
throw new AlephaError(
|
|
260
|
+
"render() method is not implemented in this environment",
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
public async fetch(options?: PageDescriptorRenderOptions): Promise<{
|
|
265
|
+
html: string;
|
|
266
|
+
response: Response;
|
|
267
|
+
}> {
|
|
268
|
+
throw new AlephaError(
|
|
269
|
+
"fetch() method is not implemented in this environment",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
public match(url: string): boolean {
|
|
274
|
+
// TODO: Implement a way to match the URL against the pathname
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
public pathname(config: any) {
|
|
279
|
+
// TODO: Implement a way to generate the pathname based on the config
|
|
280
|
+
return this.options.path || "";
|
|
215
281
|
}
|
|
216
282
|
}
|
|
217
283
|
|
|
@@ -231,13 +297,21 @@ export type TPropsParentDefault = {};
|
|
|
231
297
|
export interface PageDescriptorRenderOptions {
|
|
232
298
|
params?: Record<string, string>;
|
|
233
299
|
query?: Record<string, string>;
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* If true, the HTML layout will be included in the response.
|
|
303
|
+
* If false, only the page content will be returned.
|
|
304
|
+
*
|
|
305
|
+
* @default true
|
|
306
|
+
*/
|
|
234
307
|
html?: boolean;
|
|
235
308
|
hydration?: boolean;
|
|
236
309
|
}
|
|
237
310
|
|
|
238
311
|
export interface PageDescriptorRenderResult {
|
|
239
312
|
html: string;
|
|
240
|
-
|
|
313
|
+
state: ReactRouterState;
|
|
314
|
+
redirect?: string;
|
|
241
315
|
}
|
|
242
316
|
|
|
243
317
|
export interface PageRequestConfig<
|
|
@@ -255,4 +329,25 @@ export interface PageRequestConfig<
|
|
|
255
329
|
export type PageResolve<
|
|
256
330
|
TConfig extends PageConfigSchema = PageConfigSchema,
|
|
257
331
|
TPropsParent extends object = TPropsParentDefault,
|
|
258
|
-
> = PageRequestConfig<TConfig> &
|
|
332
|
+
> = PageRequestConfig<TConfig> &
|
|
333
|
+
TPropsParent &
|
|
334
|
+
Omit<ReactRouterState, "layers" | "onError">;
|
|
335
|
+
|
|
336
|
+
export type PageAnimation =
|
|
337
|
+
| PageAnimationObject
|
|
338
|
+
| ((state: ReactRouterState) => PageAnimationObject | undefined);
|
|
339
|
+
|
|
340
|
+
type PageAnimationObject =
|
|
341
|
+
| CssAnimationName
|
|
342
|
+
| {
|
|
343
|
+
enter?: CssAnimation | CssAnimationName;
|
|
344
|
+
exit?: CssAnimation | CssAnimationName;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
type CssAnimationName = string;
|
|
348
|
+
|
|
349
|
+
type CssAnimation = {
|
|
350
|
+
name: string;
|
|
351
|
+
duration?: number;
|
|
352
|
+
timing?: string;
|
|
353
|
+
};
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Used for Redirection during the page loading.
|
|
3
|
+
*
|
|
4
|
+
* Depends on the context, it can be thrown or just returned.
|
|
5
|
+
*/
|
|
3
6
|
export class Redirection extends Error {
|
|
4
|
-
public readonly
|
|
7
|
+
public readonly redirect: string;
|
|
5
8
|
|
|
6
|
-
constructor(
|
|
9
|
+
constructor(redirect: string) {
|
|
7
10
|
super("Redirection");
|
|
8
|
-
this.
|
|
11
|
+
this.redirect = redirect;
|
|
9
12
|
}
|
|
10
13
|
}
|
package/src/hooks/useActive.ts
CHANGED
|
@@ -1,56 +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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (!ctx || !layer) {
|
|
14
|
-
throw new Error("useRouter must be used within a RouterProvider");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const [current, setCurrent] = useState(ctx.state.pathname);
|
|
18
|
-
const href = useMemo(
|
|
19
|
-
() => router.createHref(path ?? "", layer),
|
|
20
|
-
[path, layer],
|
|
21
|
-
);
|
|
6
|
+
export interface UseActiveOptions {
|
|
7
|
+
href: string;
|
|
8
|
+
startWith?: boolean;
|
|
9
|
+
}
|
|
22
10
|
|
|
11
|
+
export const useActive = (args: string | UseActiveOptions): UseActiveHook => {
|
|
12
|
+
const router = useRouter();
|
|
23
13
|
const [isPending, setPending] = useState(false);
|
|
14
|
+
const state = useRouterState();
|
|
15
|
+
const current = state.url.pathname;
|
|
24
16
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
17
|
+
const options: UseActiveOptions =
|
|
18
|
+
typeof args === "string" ? { href: args } : { ...args, href: args.href };
|
|
19
|
+
const href = options.href;
|
|
20
|
+
|
|
21
|
+
let isActive =
|
|
28
22
|
current === href || current === `${href}/` || `${current}/` === href;
|
|
29
23
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
path ? setCurrent(state.pathname) : undefined;
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
[path],
|
|
37
|
-
);
|
|
24
|
+
if (options.startWith && !isActive) {
|
|
25
|
+
isActive = current.startsWith(href);
|
|
26
|
+
}
|
|
38
27
|
|
|
39
28
|
return {
|
|
40
29
|
isPending,
|
|
41
30
|
isActive,
|
|
42
31
|
anchorProps: {
|
|
43
|
-
href,
|
|
44
|
-
onClick: (ev?: any) => {
|
|
32
|
+
href: router.base(href),
|
|
33
|
+
onClick: async (ev?: any) => {
|
|
45
34
|
ev?.stopPropagation();
|
|
46
35
|
ev?.preventDefault();
|
|
47
36
|
if (isActive) return;
|
|
48
37
|
if (isPending) return;
|
|
49
38
|
|
|
50
39
|
setPending(true);
|
|
51
|
-
|
|
40
|
+
try {
|
|
41
|
+
await router.go(href);
|
|
42
|
+
} finally {
|
|
52
43
|
setPending(false);
|
|
53
|
-
}
|
|
44
|
+
}
|
|
54
45
|
},
|
|
55
46
|
},
|
|
56
47
|
};
|
|
@@ -60,5 +51,4 @@ export interface UseActiveHook {
|
|
|
60
51
|
isActive: boolean;
|
|
61
52
|
anchorProps: AnchorProps;
|
|
62
53
|
isPending: boolean;
|
|
63
|
-
name?: string;
|
|
64
54
|
}
|
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
|
@@ -4,11 +4,14 @@ import {
|
|
|
4
4
|
LinkProvider,
|
|
5
5
|
} from "@alepha/server-links";
|
|
6
6
|
import { useInject } from "./useInject.ts";
|
|
7
|
-
import { useStore } from "./useStore.ts";
|
|
8
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
|
+
*/
|
|
9
13
|
export const useClient = <T extends object>(
|
|
10
|
-
|
|
14
|
+
scope?: ClientScope,
|
|
11
15
|
): HttpVirtualClient<T> => {
|
|
12
|
-
|
|
13
|
-
return useInject(LinkProvider).client<T>();
|
|
16
|
+
return useInject(LinkProvider).client<T>(scope);
|
|
14
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
|
};
|