@alepha/react 0.9.4 → 0.10.0
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 +101 -7
- package/dist/index.browser.js +290 -86
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +352 -110
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +321 -183
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +318 -180
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +352 -110
- package/dist/index.js.map +1 -1
- package/package.json +17 -14
- package/src/components/Link.tsx +2 -5
- package/src/components/NestedView.tsx +159 -16
- package/src/descriptors/$page.ts +169 -1
- package/src/hooks/useActive.ts +0 -1
- package/src/hooks/useAlepha.ts +1 -1
- package/src/hooks/useQueryParams.ts +9 -5
- package/src/hooks/useRouterEvents.ts +27 -19
- package/src/hooks/useStore.ts +5 -5
- package/src/index.browser.ts +3 -0
- package/src/index.ts +6 -1
- package/src/providers/ReactBrowserProvider.ts +21 -16
- package/src/providers/ReactBrowserRendererProvider.ts +22 -0
- package/src/providers/ReactBrowserRouterProvider.ts +11 -6
- package/src/providers/ReactPageProvider.ts +45 -1
- package/src/providers/ReactServerProvider.ts +105 -38
- package/src/services/ReactRouter.ts +6 -9
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.
|
|
4
|
+
"version": "0.10.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22.0.0"
|
|
@@ -17,30 +17,33 @@
|
|
|
17
17
|
"src"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@alepha/core": "0.
|
|
21
|
-
"@alepha/datetime": "0.
|
|
22
|
-
"@alepha/logger": "0.
|
|
23
|
-
"@alepha/router": "0.
|
|
24
|
-
"@alepha/server": "0.
|
|
25
|
-
"@alepha/server-cache": "0.
|
|
26
|
-
"@alepha/server-links": "0.
|
|
27
|
-
"@alepha/server-static": "0.
|
|
28
|
-
"react-dom": "^19.1.1"
|
|
20
|
+
"@alepha/core": "0.10.0",
|
|
21
|
+
"@alepha/datetime": "0.10.0",
|
|
22
|
+
"@alepha/logger": "0.10.0",
|
|
23
|
+
"@alepha/router": "0.10.0",
|
|
24
|
+
"@alepha/server": "0.10.0",
|
|
25
|
+
"@alepha/server-cache": "0.10.0",
|
|
26
|
+
"@alepha/server-links": "0.10.0",
|
|
27
|
+
"@alepha/server-static": "0.10.0"
|
|
29
28
|
},
|
|
30
29
|
"devDependencies": {
|
|
31
|
-
"@
|
|
32
|
-
"@types/react
|
|
30
|
+
"@biomejs/biome": "^2.2.4",
|
|
31
|
+
"@types/react": "^19.1.13",
|
|
32
|
+
"@types/react-dom": "^19.1.9",
|
|
33
33
|
"react": "^19.1.1",
|
|
34
|
-
"
|
|
34
|
+
"react-dom": "^19.1.1",
|
|
35
|
+
"tsdown": "^0.15.3",
|
|
35
36
|
"typescript": "^5.9.2",
|
|
36
37
|
"vitest": "^3.2.4"
|
|
37
38
|
},
|
|
38
39
|
"peerDependencies": {
|
|
39
|
-
"react": "
|
|
40
|
+
"react": "*",
|
|
41
|
+
"react-dom": "*"
|
|
40
42
|
},
|
|
41
43
|
"scripts": {
|
|
42
44
|
"check": "tsc",
|
|
43
45
|
"test": "vitest run",
|
|
46
|
+
"lint": "biome check --write --unsafe",
|
|
44
47
|
"build": "tsdown -c ../../tsdown.config.ts"
|
|
45
48
|
},
|
|
46
49
|
"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,13 +1,15 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { useContext, useState } from "react";
|
|
1
|
+
import { memo, type ReactNode, use, useRef, useState } from "react";
|
|
3
2
|
import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
|
|
3
|
+
import type { PageAnimation } from "../descriptors/$page.ts";
|
|
4
4
|
import { Redirection } from "../errors/Redirection.ts";
|
|
5
|
-
import { useAlepha } from "../hooks/useAlepha.ts";
|
|
6
5
|
import { useRouterEvents } from "../hooks/useRouterEvents.ts";
|
|
6
|
+
import { useRouterState } from "../hooks/useRouterState.ts";
|
|
7
|
+
import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
|
|
7
8
|
import ErrorBoundary from "./ErrorBoundary.tsx";
|
|
8
9
|
|
|
9
10
|
export interface NestedViewProps {
|
|
10
11
|
children?: ReactNode;
|
|
12
|
+
errorBoundary?: false | ((error: Error) => ReactNode);
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -17,7 +19,7 @@ export interface NestedViewProps {
|
|
|
17
19
|
*
|
|
18
20
|
* @example
|
|
19
21
|
* ```tsx
|
|
20
|
-
* import { NestedView } from "
|
|
22
|
+
* import { NestedView } from "alepha/react";
|
|
21
23
|
*
|
|
22
24
|
* class App {
|
|
23
25
|
* parent = $page({
|
|
@@ -32,30 +34,115 @@ export interface NestedViewProps {
|
|
|
32
34
|
* ```
|
|
33
35
|
*/
|
|
34
36
|
const NestedView = (props: NestedViewProps) => {
|
|
35
|
-
const
|
|
36
|
-
const
|
|
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
|
+
const index = use(RouterLayerContext)?.index ?? 0;
|
|
38
|
+
const state = useRouterState();
|
|
42
39
|
|
|
43
40
|
const [view, setView] = useState<ReactNode | undefined>(
|
|
44
41
|
state.layers[index]?.element,
|
|
45
42
|
);
|
|
46
43
|
|
|
44
|
+
const [animation, setAnimation] = useState("");
|
|
45
|
+
const animationExitDuration = useRef<number>(0);
|
|
46
|
+
const animationExitNow = useRef<number>(0);
|
|
47
|
+
|
|
47
48
|
useRouterEvents(
|
|
48
49
|
{
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
+
}
|
|
87
|
+
}
|
|
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 ---------
|
|
52
106
|
}
|
|
53
107
|
},
|
|
54
108
|
},
|
|
55
109
|
[],
|
|
56
110
|
);
|
|
57
111
|
|
|
58
|
-
|
|
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
|
+
);
|
|
134
|
+
}
|
|
135
|
+
// --------- Animations End ---------
|
|
136
|
+
|
|
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
|
+
}
|
|
59
146
|
|
|
60
147
|
return (
|
|
61
148
|
<ErrorBoundary
|
|
@@ -72,4 +159,60 @@ const NestedView = (props: NestedViewProps) => {
|
|
|
72
159
|
);
|
|
73
160
|
};
|
|
74
161
|
|
|
75
|
-
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,
|
|
@@ -15,6 +16,91 @@ import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
|
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Main descriptor for defining a React route in the application.
|
|
19
|
+
*
|
|
20
|
+
* The $page descriptor is the core building block for creating type-safe, SSR-enabled React routes.
|
|
21
|
+
* It provides a declarative way to define pages with powerful features:
|
|
22
|
+
*
|
|
23
|
+
* **Routing & Navigation**
|
|
24
|
+
* - URL pattern matching with parameters (e.g., `/users/:id`)
|
|
25
|
+
* - Nested routing with parent-child relationships
|
|
26
|
+
* - Type-safe URL parameter and query string validation
|
|
27
|
+
*
|
|
28
|
+
* **Data Loading**
|
|
29
|
+
* - Server-side data fetching with the `resolve` function
|
|
30
|
+
* - Automatic serialization and hydration for SSR
|
|
31
|
+
* - Access to request context, URL params, and parent data
|
|
32
|
+
*
|
|
33
|
+
* **Component Loading**
|
|
34
|
+
* - Direct component rendering or lazy loading for code splitting
|
|
35
|
+
* - Client-only rendering when browser APIs are needed
|
|
36
|
+
* - Automatic fallback handling during hydration
|
|
37
|
+
*
|
|
38
|
+
* **Performance Optimization**
|
|
39
|
+
* - Static generation for pre-rendered pages at build time
|
|
40
|
+
* - Server-side caching with configurable TTL and providers
|
|
41
|
+
* - Code splitting through lazy component loading
|
|
42
|
+
*
|
|
43
|
+
* **Error Handling**
|
|
44
|
+
* - Custom error handlers with support for redirects
|
|
45
|
+
* - Hierarchical error handling (child → parent)
|
|
46
|
+
* - HTTP status code handling (404, 401, etc.)
|
|
47
|
+
*
|
|
48
|
+
* **Page Animations**
|
|
49
|
+
* - CSS-based enter/exit animations
|
|
50
|
+
* - Dynamic animations based on page state
|
|
51
|
+
* - Custom timing and easing functions
|
|
52
|
+
*
|
|
53
|
+
* **Lifecycle Management**
|
|
54
|
+
* - Server response hooks for headers and status codes
|
|
55
|
+
* - Page leave handlers for cleanup (browser only)
|
|
56
|
+
* - Permission-based access control
|
|
57
|
+
*
|
|
58
|
+
* @example Simple page with data fetching
|
|
59
|
+
* ```typescript
|
|
60
|
+
* const userProfile = $page({
|
|
61
|
+
* path: "/users/:id",
|
|
62
|
+
* schema: {
|
|
63
|
+
* params: t.object({ id: t.int() }),
|
|
64
|
+
* query: t.object({ tab: t.optional(t.string()) })
|
|
65
|
+
* },
|
|
66
|
+
* resolve: async ({ params }) => {
|
|
67
|
+
* const user = await userApi.getUser(params.id);
|
|
68
|
+
* return { user };
|
|
69
|
+
* },
|
|
70
|
+
* lazy: () => import("./UserProfile.tsx")
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @example Nested routing with error handling
|
|
75
|
+
* ```typescript
|
|
76
|
+
* const projectSection = $page({
|
|
77
|
+
* path: "/projects/:id",
|
|
78
|
+
* children: () => [projectBoard, projectSettings],
|
|
79
|
+
* resolve: async ({ params }) => {
|
|
80
|
+
* const project = await projectApi.get(params.id);
|
|
81
|
+
* return { project };
|
|
82
|
+
* },
|
|
83
|
+
* errorHandler: (error) => {
|
|
84
|
+
* if (HttpError.is(error, 404)) {
|
|
85
|
+
* return <ProjectNotFound />;
|
|
86
|
+
* }
|
|
87
|
+
* }
|
|
88
|
+
* });
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* @example Static generation with caching
|
|
92
|
+
* ```typescript
|
|
93
|
+
* const blogPost = $page({
|
|
94
|
+
* path: "/blog/:slug",
|
|
95
|
+
* static: {
|
|
96
|
+
* entries: posts.map(p => ({ params: { slug: p.slug } }))
|
|
97
|
+
* },
|
|
98
|
+
* resolve: async ({ params }) => {
|
|
99
|
+
* const post = await loadPost(params.slug);
|
|
100
|
+
* return { post };
|
|
101
|
+
* }
|
|
102
|
+
* });
|
|
103
|
+
* ```
|
|
18
104
|
*/
|
|
19
105
|
export const $page = <
|
|
20
106
|
TConfig extends PageConfigSchema = PageConfigSchema,
|
|
@@ -179,6 +265,50 @@ export interface PageDescriptorOptions<
|
|
|
179
265
|
* Called when user leaves the page. (browser only)
|
|
180
266
|
*/
|
|
181
267
|
onLeave?: () => void;
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* @experimental
|
|
271
|
+
*
|
|
272
|
+
* Add a css animation when the page is loaded or unloaded.
|
|
273
|
+
* It uses CSS animations, so you need to define the keyframes in your CSS.
|
|
274
|
+
*
|
|
275
|
+
* @example Simple animation name
|
|
276
|
+
* ```ts
|
|
277
|
+
* animation: "fadeIn"
|
|
278
|
+
* ```
|
|
279
|
+
*
|
|
280
|
+
* CSS example:
|
|
281
|
+
* ```css
|
|
282
|
+
* @keyframes fadeIn {
|
|
283
|
+
* from { opacity: 0; }
|
|
284
|
+
* to { opacity: 1; }
|
|
285
|
+
* }
|
|
286
|
+
* ```
|
|
287
|
+
*
|
|
288
|
+
* @example Detailed animation
|
|
289
|
+
* ```ts
|
|
290
|
+
* animation: {
|
|
291
|
+
* enter: { name: "fadeIn", duration: 300 },
|
|
292
|
+
* exit: { name: "fadeOut", duration: 200, timing: "ease-in-out" },
|
|
293
|
+
* }
|
|
294
|
+
* ```
|
|
295
|
+
*
|
|
296
|
+
* @example Only exit animation
|
|
297
|
+
* ```ts
|
|
298
|
+
* animation: {
|
|
299
|
+
* exit: "fadeOut"
|
|
300
|
+
* }
|
|
301
|
+
* ```
|
|
302
|
+
*
|
|
303
|
+
* @example With custom timing function
|
|
304
|
+
* ```ts
|
|
305
|
+
* animation: {
|
|
306
|
+
* enter: { name: "fadeIn", duration: 300, timing: "cubic-bezier(0.4, 0, 0.2, 1)" },
|
|
307
|
+
* exit: { name: "fadeOut", duration: 200, timing: "ease-in-out" },
|
|
308
|
+
* }
|
|
309
|
+
* ```
|
|
310
|
+
*/
|
|
311
|
+
animation?: PageAnimation;
|
|
182
312
|
}
|
|
183
313
|
|
|
184
314
|
export type ErrorHandler = (
|
|
@@ -211,7 +341,18 @@ export class PageDescriptor<
|
|
|
211
341
|
public async render(
|
|
212
342
|
options?: PageDescriptorRenderOptions,
|
|
213
343
|
): Promise<PageDescriptorRenderResult> {
|
|
214
|
-
throw new
|
|
344
|
+
throw new AlephaError(
|
|
345
|
+
"render() method is not implemented in this environment",
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
public async fetch(options?: PageDescriptorRenderOptions): Promise<{
|
|
350
|
+
html: string;
|
|
351
|
+
response: Response;
|
|
352
|
+
}> {
|
|
353
|
+
throw new AlephaError(
|
|
354
|
+
"fetch() method is not implemented in this environment",
|
|
355
|
+
);
|
|
215
356
|
}
|
|
216
357
|
|
|
217
358
|
public match(url: string): boolean {
|
|
@@ -241,6 +382,13 @@ export type TPropsParentDefault = {};
|
|
|
241
382
|
export interface PageDescriptorRenderOptions {
|
|
242
383
|
params?: Record<string, string>;
|
|
243
384
|
query?: Record<string, string>;
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* If true, the HTML layout will be included in the response.
|
|
388
|
+
* If false, only the page content will be returned.
|
|
389
|
+
*
|
|
390
|
+
* @default true
|
|
391
|
+
*/
|
|
244
392
|
html?: boolean;
|
|
245
393
|
hydration?: boolean;
|
|
246
394
|
}
|
|
@@ -248,6 +396,7 @@ export interface PageDescriptorRenderOptions {
|
|
|
248
396
|
export interface PageDescriptorRenderResult {
|
|
249
397
|
html: string;
|
|
250
398
|
state: ReactRouterState;
|
|
399
|
+
redirect?: string;
|
|
251
400
|
}
|
|
252
401
|
|
|
253
402
|
export interface PageRequestConfig<
|
|
@@ -268,3 +417,22 @@ export type PageResolve<
|
|
|
268
417
|
> = PageRequestConfig<TConfig> &
|
|
269
418
|
TPropsParent &
|
|
270
419
|
Omit<ReactRouterState, "layers" | "onError">;
|
|
420
|
+
|
|
421
|
+
export type PageAnimation =
|
|
422
|
+
| PageAnimationObject
|
|
423
|
+
| ((state: ReactRouterState) => PageAnimationObject | undefined);
|
|
424
|
+
|
|
425
|
+
type PageAnimationObject =
|
|
426
|
+
| CssAnimationName
|
|
427
|
+
| {
|
|
428
|
+
enter?: CssAnimation | CssAnimationName;
|
|
429
|
+
exit?: CssAnimation | CssAnimationName;
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
type CssAnimationName = string;
|
|
433
|
+
|
|
434
|
+
type CssAnimation = {
|
|
435
|
+
name: string;
|
|
436
|
+
duration?: number;
|
|
437
|
+
timing?: string;
|
|
438
|
+
};
|
package/src/hooks/useActive.ts
CHANGED
package/src/hooks/useAlepha.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { AlephaContext } from "../contexts/AlephaContext.ts";
|
|
|
11
11
|
*
|
|
12
12
|
* - alepha.state() for state management
|
|
13
13
|
* - alepha.inject() for dependency injection
|
|
14
|
-
* - alepha.emit() for event handling
|
|
14
|
+
* - alepha.events.emit() for event handling
|
|
15
15
|
* etc...
|
|
16
16
|
*/
|
|
17
17
|
export const useAlepha = (): Alepha => {
|
|
@@ -9,14 +9,14 @@ import { useRouter } from "./useRouter.ts";
|
|
|
9
9
|
export const useQueryParams = <T extends TObject>(
|
|
10
10
|
schema: T,
|
|
11
11
|
options: UseQueryParamsHookOptions = {},
|
|
12
|
-
): [Static<T
|
|
12
|
+
): [Partial<Static<T>>, (data: Static<T>) => void] => {
|
|
13
13
|
const alepha = useAlepha();
|
|
14
14
|
|
|
15
15
|
const key = options.key ?? "q";
|
|
16
16
|
const router = useRouter();
|
|
17
17
|
const querystring = router.query[key];
|
|
18
18
|
|
|
19
|
-
const [queryParams, setQueryParams] = useState(
|
|
19
|
+
const [queryParams = {}, setQueryParams] = useState<Static<T> | undefined>(
|
|
20
20
|
decode(alepha, schema, router.query[key]),
|
|
21
21
|
);
|
|
22
22
|
|
|
@@ -47,10 +47,14 @@ const encode = (alepha: Alepha, schema: TObject, data: any) => {
|
|
|
47
47
|
return btoa(JSON.stringify(alepha.parse(schema, data)));
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
const decode =
|
|
50
|
+
const decode = <T extends TObject>(
|
|
51
|
+
alepha: Alepha,
|
|
52
|
+
schema: T,
|
|
53
|
+
data: any,
|
|
54
|
+
): Static<T> | undefined => {
|
|
51
55
|
try {
|
|
52
56
|
return alepha.parse(schema, JSON.parse(atob(decodeURIComponent(data))));
|
|
53
|
-
} catch
|
|
54
|
-
return
|
|
57
|
+
} catch {
|
|
58
|
+
return;
|
|
55
59
|
}
|
|
56
60
|
};
|
|
@@ -1,15 +1,23 @@
|
|
|
1
|
+
import type { Hooks } from "@alepha/core";
|
|
1
2
|
import { useEffect } from "react";
|
|
2
|
-
import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
|
|
3
3
|
import { useAlepha } from "./useAlepha.ts";
|
|
4
4
|
|
|
5
|
+
type Hook<T extends keyof Hooks> =
|
|
6
|
+
| ((ev: Hooks[T]) => void)
|
|
7
|
+
| {
|
|
8
|
+
priority?: "first" | "last";
|
|
9
|
+
callback: (ev: Hooks[T]) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
5
12
|
/**
|
|
6
13
|
* Subscribe to various router events.
|
|
7
14
|
*/
|
|
8
15
|
export const useRouterEvents = (
|
|
9
16
|
opts: {
|
|
10
|
-
onBegin?:
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
onBegin?: Hook<"react:transition:begin">;
|
|
18
|
+
onError?: Hook<"react:transition:error">;
|
|
19
|
+
onEnd?: Hook<"react:transition:end">;
|
|
20
|
+
onSuccess?: Hook<"react:transition:success">;
|
|
13
21
|
} = {},
|
|
14
22
|
deps: any[] = [],
|
|
15
23
|
) => {
|
|
@@ -20,33 +28,33 @@ export const useRouterEvents = (
|
|
|
20
28
|
return;
|
|
21
29
|
}
|
|
22
30
|
|
|
31
|
+
const cb = <T extends keyof Hooks>(callback: Hook<T>) => {
|
|
32
|
+
if (typeof callback === "function") {
|
|
33
|
+
return { callback };
|
|
34
|
+
}
|
|
35
|
+
return callback;
|
|
36
|
+
};
|
|
37
|
+
|
|
23
38
|
const subs: Function[] = [];
|
|
24
39
|
const onBegin = opts.onBegin;
|
|
25
40
|
const onEnd = opts.onEnd;
|
|
26
41
|
const onError = opts.onError;
|
|
42
|
+
const onSuccess = opts.onSuccess;
|
|
27
43
|
|
|
28
44
|
if (onBegin) {
|
|
29
|
-
subs.push(
|
|
30
|
-
alepha.on("react:transition:begin", {
|
|
31
|
-
callback: onBegin,
|
|
32
|
-
}),
|
|
33
|
-
);
|
|
45
|
+
subs.push(alepha.events.on("react:transition:begin", cb(onBegin)));
|
|
34
46
|
}
|
|
35
47
|
|
|
36
48
|
if (onEnd) {
|
|
37
|
-
subs.push(
|
|
38
|
-
alepha.on("react:transition:end", {
|
|
39
|
-
callback: onEnd,
|
|
40
|
-
}),
|
|
41
|
-
);
|
|
49
|
+
subs.push(alepha.events.on("react:transition:end", cb(onEnd)));
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
if (onError) {
|
|
45
|
-
subs.push(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
);
|
|
53
|
+
subs.push(alepha.events.on("react:transition:error", cb(onError)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (onSuccess) {
|
|
57
|
+
subs.push(alepha.events.on("react:transition:success", cb(onSuccess)));
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
return () => {
|
package/src/hooks/useStore.ts
CHANGED
|
@@ -12,19 +12,19 @@ export const useStore = <Key extends keyof State>(
|
|
|
12
12
|
const alepha = useAlepha();
|
|
13
13
|
|
|
14
14
|
useMemo(() => {
|
|
15
|
-
if (defaultValue != null && alepha.state(key) == null) {
|
|
16
|
-
alepha.state(key, defaultValue);
|
|
15
|
+
if (defaultValue != null && alepha.state.get(key) == null) {
|
|
16
|
+
alepha.state.set(key, defaultValue);
|
|
17
17
|
}
|
|
18
18
|
}, [defaultValue]);
|
|
19
19
|
|
|
20
|
-
const [state, setState] = useState(alepha.state(key));
|
|
20
|
+
const [state, setState] = useState(alepha.state.get(key));
|
|
21
21
|
|
|
22
22
|
useEffect(() => {
|
|
23
23
|
if (!alepha.isBrowser()) {
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
return alepha.on("state:mutate", (ev) => {
|
|
27
|
+
return alepha.events.on("state:mutate", (ev) => {
|
|
28
28
|
if (ev.key === key) {
|
|
29
29
|
setState(ev.value);
|
|
30
30
|
}
|
|
@@ -34,7 +34,7 @@ export const useStore = <Key extends keyof State>(
|
|
|
34
34
|
return [
|
|
35
35
|
state,
|
|
36
36
|
(value: State[Key]) => {
|
|
37
|
-
alepha.state(key, value);
|
|
37
|
+
alepha.state.set(key, value);
|
|
38
38
|
},
|
|
39
39
|
] as const;
|
|
40
40
|
};
|
package/src/index.browser.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { AlephaServer } from "@alepha/server";
|
|
|
3
3
|
import { AlephaServerLinks } from "@alepha/server-links";
|
|
4
4
|
import { $page } from "./descriptors/$page.ts";
|
|
5
5
|
import { ReactBrowserProvider } from "./providers/ReactBrowserProvider.ts";
|
|
6
|
+
import { ReactBrowserRendererProvider } from "./providers/ReactBrowserRendererProvider.ts";
|
|
6
7
|
import { ReactBrowserRouterProvider } from "./providers/ReactBrowserRouterProvider.ts";
|
|
7
8
|
import { ReactPageProvider } from "./providers/ReactPageProvider.ts";
|
|
8
9
|
import { ReactRouter } from "./services/ReactRouter.ts";
|
|
@@ -24,6 +25,7 @@ export const AlephaReact = $module({
|
|
|
24
25
|
ReactBrowserRouterProvider,
|
|
25
26
|
ReactBrowserProvider,
|
|
26
27
|
ReactRouter,
|
|
28
|
+
ReactBrowserRendererProvider,
|
|
27
29
|
],
|
|
28
30
|
register: (alepha) =>
|
|
29
31
|
alepha
|
|
@@ -32,5 +34,6 @@ export const AlephaReact = $module({
|
|
|
32
34
|
.with(ReactPageProvider)
|
|
33
35
|
.with(ReactBrowserProvider)
|
|
34
36
|
.with(ReactBrowserRouterProvider)
|
|
37
|
+
.with(ReactBrowserRendererProvider)
|
|
35
38
|
.with(ReactRouter),
|
|
36
39
|
});
|