@alepha/react 0.5.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/LICENSE +21 -0
- package/README.md +28 -0
- package/package.json +43 -0
- package/src/components/NestedView.tsx +36 -0
- package/src/contexts/RouterContext.ts +15 -0
- package/src/contexts/RouterLayerContext.ts +10 -0
- package/src/descriptors/$page.ts +90 -0
- package/src/hooks/RouterHookApi.ts +154 -0
- package/src/hooks/useActive.ts +57 -0
- package/src/hooks/useClient.ts +6 -0
- package/src/hooks/useInject.ts +12 -0
- package/src/hooks/useQueryParams.ts +59 -0
- package/src/hooks/useRouter.ts +28 -0
- package/src/hooks/useRouterEvents.ts +43 -0
- package/src/hooks/useRouterState.ts +23 -0
- package/src/index.browser.ts +19 -0
- package/src/index.shared.ts +17 -0
- package/src/index.ts +29 -0
- package/src/providers/PageDescriptorProvider.ts +52 -0
- package/src/providers/ReactBrowserProvider.ts +228 -0
- package/src/providers/ReactServerProvider.ts +244 -0
- package/src/providers/ReactSessionProvider.ts +363 -0
- package/src/services/Router.ts +742 -0
- package/test/$page.spec.tsx +42 -0
- package/test/Router.spec.tsx +138 -0
- package/tsconfig.json +6 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Feunard
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# @alepha/server
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @alepha/server
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { Alepha, route } from "@alepha/server";
|
|
13
|
+
|
|
14
|
+
class App {
|
|
15
|
+
index = route({
|
|
16
|
+
handler: () => "Hello, World!",
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Alepha
|
|
21
|
+
.create({
|
|
22
|
+
SERVER_PORT: 3000,
|
|
23
|
+
SERVER_OPENAPI_ENABLED: true,
|
|
24
|
+
SERVER_SECURITY_ENABLED: true,
|
|
25
|
+
})
|
|
26
|
+
.with(App)
|
|
27
|
+
.start();
|
|
28
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alepha/react",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.cts",
|
|
9
|
+
"browser": "./dist/index.browser.mjs",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@alepha/cache": "0.5.0",
|
|
12
|
+
"@alepha/core": "0.5.0",
|
|
13
|
+
"@alepha/security": "0.5.0",
|
|
14
|
+
"@alepha/server": "0.5.0",
|
|
15
|
+
"openid-client": "^6.4.1",
|
|
16
|
+
"path-to-regexp": "^8.2.0",
|
|
17
|
+
"react": "^18.3.1",
|
|
18
|
+
"react-dom": "^18.3.1"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/react": "^18.3.20",
|
|
22
|
+
"@types/react-dom": "^18.3.5",
|
|
23
|
+
"pkgroll": "^2.12.1",
|
|
24
|
+
"vitest": "^3.1.1"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "pkgroll --clean-dist"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
"require": {
|
|
31
|
+
"types": "./dist/index.d.cts",
|
|
32
|
+
"default": "./dist/index.cjs"
|
|
33
|
+
},
|
|
34
|
+
"import": {
|
|
35
|
+
"types": "./dist/index.d.mts",
|
|
36
|
+
"default": "./dist/index.mjs"
|
|
37
|
+
},
|
|
38
|
+
"browser": {
|
|
39
|
+
"types": "./dist/index.d.mts",
|
|
40
|
+
"default": "./dist/index.browser.mjs"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { useContext, useEffect, useState } from "react";
|
|
3
|
+
import { RouterContext } from "../contexts/RouterContext";
|
|
4
|
+
import { RouterLayerContext } from "../contexts/RouterLayerContext";
|
|
5
|
+
|
|
6
|
+
export interface NestedViewProps {
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Nested view component
|
|
12
|
+
*
|
|
13
|
+
* @param props
|
|
14
|
+
* @constructor
|
|
15
|
+
*/
|
|
16
|
+
const NestedView = (props: NestedViewProps) => {
|
|
17
|
+
const app = useContext(RouterContext);
|
|
18
|
+
const layer = useContext(RouterLayerContext);
|
|
19
|
+
const index = layer?.index ?? 0;
|
|
20
|
+
|
|
21
|
+
const [view, setView] = useState<ReactNode | undefined>(
|
|
22
|
+
app?.state.layers[index]?.element,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (app?.alepha.isBrowser()) {
|
|
27
|
+
return app?.router.on("end", ({ layers }) => {
|
|
28
|
+
setView(layers[index]?.element);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}, [app]);
|
|
32
|
+
|
|
33
|
+
return view ?? props.children ?? null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default NestedView;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Alepha } from "@alepha/core";
|
|
2
|
+
import { createContext } from "react";
|
|
3
|
+
import type { Session } from "../providers/ReactSessionProvider";
|
|
4
|
+
import type { Router, RouterState } from "../services/Router";
|
|
5
|
+
|
|
6
|
+
export interface RouterContextValue {
|
|
7
|
+
router: Router;
|
|
8
|
+
alepha: Alepha;
|
|
9
|
+
state: RouterState;
|
|
10
|
+
session?: Session;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const RouterContext = createContext<RouterContextValue | undefined>(
|
|
14
|
+
undefined,
|
|
15
|
+
);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Async, Static, TSchema } from "@alepha/core";
|
|
2
|
+
import { KIND, NotImplementedError, __descriptor } from "@alepha/core";
|
|
3
|
+
import type { UserAccountToken } from "@alepha/security";
|
|
4
|
+
import type { FC } from "react";
|
|
5
|
+
import type { RouterHookApi } from "../hooks/RouterHookApi";
|
|
6
|
+
|
|
7
|
+
export const pageDescriptorKey = "PAGE";
|
|
8
|
+
|
|
9
|
+
export interface PageDescriptorConfigSchema {
|
|
10
|
+
query?: TSchema;
|
|
11
|
+
params?: TSchema;
|
|
12
|
+
}
|
|
13
|
+
export type TPropsDefault = any;
|
|
14
|
+
export type TPropsParentDefault = object;
|
|
15
|
+
|
|
16
|
+
export interface PageDescriptorOptions<
|
|
17
|
+
TConfig extends PageDescriptorConfigSchema = PageDescriptorConfigSchema,
|
|
18
|
+
TProps extends object = TPropsDefault,
|
|
19
|
+
TPropsParent extends object = TPropsParentDefault,
|
|
20
|
+
> {
|
|
21
|
+
parent?: { options: PageDescriptorOptions<any, TPropsParent> };
|
|
22
|
+
name?: string;
|
|
23
|
+
path?: string;
|
|
24
|
+
schema?: TConfig;
|
|
25
|
+
abstract?: boolean;
|
|
26
|
+
resolve?: (
|
|
27
|
+
config: PageDescriptorConfigValue<TConfig> &
|
|
28
|
+
TPropsParent & { user?: UserAccountToken },
|
|
29
|
+
) => Async<TProps>;
|
|
30
|
+
component?: FC<TProps & TPropsParent>;
|
|
31
|
+
lazy?: () => Promise<{ default: FC<TProps & TPropsParent> }>;
|
|
32
|
+
children?: () => Array<{ options: PageDescriptorOptions }>;
|
|
33
|
+
notFoundHandler?: FC<{ error: Error }>;
|
|
34
|
+
errorHandler?: FC<{ error: Error; url: string }>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PageDescriptorConfigValue<
|
|
38
|
+
TConfig extends PageDescriptorConfigSchema = PageDescriptorConfigSchema,
|
|
39
|
+
> {
|
|
40
|
+
query: TConfig["query"] extends TSchema
|
|
41
|
+
? Static<TConfig["query"]>
|
|
42
|
+
: Record<string, string>;
|
|
43
|
+
params: TConfig["params"] extends TSchema
|
|
44
|
+
? Static<TConfig["params"]>
|
|
45
|
+
: Record<string, string>;
|
|
46
|
+
pathname: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface PageDescriptor<
|
|
50
|
+
TConfig extends PageDescriptorConfigSchema = PageDescriptorConfigSchema,
|
|
51
|
+
TProps extends object = TPropsDefault,
|
|
52
|
+
TPropsParent extends object = TPropsParentDefault,
|
|
53
|
+
> {
|
|
54
|
+
[KIND]: typeof pageDescriptorKey;
|
|
55
|
+
render: (options?: {
|
|
56
|
+
params?: Record<string, string>;
|
|
57
|
+
query?: Record<string, string>;
|
|
58
|
+
}) => Promise<string>;
|
|
59
|
+
go: () => void;
|
|
60
|
+
createAnchorProps: (routerHook: RouterHookApi) => {
|
|
61
|
+
href: string;
|
|
62
|
+
onClick: () => void;
|
|
63
|
+
};
|
|
64
|
+
options: PageDescriptorOptions<TConfig, TProps, TPropsParent>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const $page = <
|
|
68
|
+
TConfig extends PageDescriptorConfigSchema = PageDescriptorConfigSchema,
|
|
69
|
+
TProps extends object = TPropsDefault,
|
|
70
|
+
TPropsParent extends object = TPropsParentDefault,
|
|
71
|
+
>(
|
|
72
|
+
options: PageDescriptorOptions<TConfig, TProps, TPropsParent>,
|
|
73
|
+
): PageDescriptor<TConfig, TProps, TPropsParent> => {
|
|
74
|
+
__descriptor(pageDescriptorKey);
|
|
75
|
+
return {
|
|
76
|
+
[KIND]: pageDescriptorKey,
|
|
77
|
+
options,
|
|
78
|
+
render: () => {
|
|
79
|
+
throw new NotImplementedError(pageDescriptorKey);
|
|
80
|
+
},
|
|
81
|
+
go: () => {
|
|
82
|
+
throw new NotImplementedError(pageDescriptorKey);
|
|
83
|
+
},
|
|
84
|
+
createAnchorProps: () => {
|
|
85
|
+
throw new NotImplementedError(pageDescriptorKey);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
$page[KIND] = pageDescriptorKey;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {} from "react";
|
|
2
|
+
import type {
|
|
3
|
+
ReactBrowserProvider,
|
|
4
|
+
RouterGoOptions,
|
|
5
|
+
} from "../providers/ReactBrowserProvider";
|
|
6
|
+
import type { AnchorProps, RouterState } from "../services/Router";
|
|
7
|
+
|
|
8
|
+
export class RouterHookApi {
|
|
9
|
+
constructor(
|
|
10
|
+
private readonly state: RouterState,
|
|
11
|
+
private readonly layer: {
|
|
12
|
+
path: string;
|
|
13
|
+
},
|
|
14
|
+
private readonly browser?: ReactBrowserProvider,
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
*
|
|
19
|
+
*/
|
|
20
|
+
public get current(): RouterState {
|
|
21
|
+
return this.state;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
*
|
|
26
|
+
*/
|
|
27
|
+
public get pathname(): string {
|
|
28
|
+
return this.state.pathname;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
*
|
|
33
|
+
*/
|
|
34
|
+
public get query(): Record<string, string> {
|
|
35
|
+
const query: Record<string, string> = {};
|
|
36
|
+
|
|
37
|
+
for (const [key, value] of new URLSearchParams(
|
|
38
|
+
this.state.search,
|
|
39
|
+
).entries()) {
|
|
40
|
+
query[key] = String(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return query;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
*
|
|
48
|
+
*/
|
|
49
|
+
public async back() {
|
|
50
|
+
this.browser?.history.back();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
*
|
|
55
|
+
*/
|
|
56
|
+
public async forward() {
|
|
57
|
+
this.browser?.history.forward();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
*
|
|
62
|
+
* @param props
|
|
63
|
+
*/
|
|
64
|
+
public async invalidate(props?: Record<string, any>) {
|
|
65
|
+
await this.browser?.invalidate(props);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create a valid href for the given pathname.
|
|
70
|
+
*
|
|
71
|
+
* @param pathname
|
|
72
|
+
* @param layer
|
|
73
|
+
*/
|
|
74
|
+
public createHref(pathname: HrefLike, layer: { path: string } = this.layer) {
|
|
75
|
+
if (typeof pathname === "object") {
|
|
76
|
+
pathname = pathname.options.path ?? "";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return pathname.startsWith("/")
|
|
80
|
+
? pathname
|
|
81
|
+
: `${layer.path}/${pathname}`.replace(/\/\/+/g, "/");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
*
|
|
86
|
+
* @param path
|
|
87
|
+
* @param options
|
|
88
|
+
*/
|
|
89
|
+
public async go(
|
|
90
|
+
path: HrefLike,
|
|
91
|
+
options: RouterGoOptions = {},
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
return await this.browser?.go(this.createHref(path, this.layer), options);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
*
|
|
98
|
+
* @param path
|
|
99
|
+
*/
|
|
100
|
+
public createAnchorProps(path: string): AnchorProps {
|
|
101
|
+
const href = this.createHref(path, this.layer);
|
|
102
|
+
return {
|
|
103
|
+
href,
|
|
104
|
+
onClick: (ev: any) => {
|
|
105
|
+
ev.stopPropagation();
|
|
106
|
+
ev.preventDefault();
|
|
107
|
+
|
|
108
|
+
this.go(path).catch(console.error);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Set query params.
|
|
115
|
+
*
|
|
116
|
+
* @param record
|
|
117
|
+
* @param options
|
|
118
|
+
*/
|
|
119
|
+
public setQueryParams(
|
|
120
|
+
record: Record<string, any>,
|
|
121
|
+
options: {
|
|
122
|
+
/**
|
|
123
|
+
* If true, this will merge current query params with the new ones.
|
|
124
|
+
*/
|
|
125
|
+
merge?: boolean;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* If true, this will add a new entry to the history stack.
|
|
129
|
+
*/
|
|
130
|
+
push?: boolean;
|
|
131
|
+
} = {},
|
|
132
|
+
) {
|
|
133
|
+
const search = new URLSearchParams(
|
|
134
|
+
options.merge
|
|
135
|
+
? {
|
|
136
|
+
...this.query,
|
|
137
|
+
...record,
|
|
138
|
+
}
|
|
139
|
+
: {
|
|
140
|
+
...record,
|
|
141
|
+
},
|
|
142
|
+
).toString();
|
|
143
|
+
|
|
144
|
+
const state = search ? `${this.pathname}?${search}` : this.pathname;
|
|
145
|
+
|
|
146
|
+
if (options.push) {
|
|
147
|
+
window.history.pushState({}, "", state);
|
|
148
|
+
} else {
|
|
149
|
+
window.history.replaceState({}, "", state);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type HrefLike = string | { options: { path?: string; name?: string } };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useContext, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { RouterContext } from "../contexts/RouterContext";
|
|
3
|
+
import { RouterLayerContext } from "../contexts/RouterLayerContext";
|
|
4
|
+
import type { AnchorProps } from "../services/Router";
|
|
5
|
+
import type { HrefLike } from "./RouterHookApi";
|
|
6
|
+
import { useRouter } from "./useRouter";
|
|
7
|
+
|
|
8
|
+
export const useActive = (path: HrefLike): UseActiveHook => {
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
const ctx = useContext(RouterContext);
|
|
11
|
+
const layer = useContext(RouterLayerContext);
|
|
12
|
+
if (!ctx || !layer) {
|
|
13
|
+
throw new Error("useRouter must be used within a RouterProvider");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let name: string | undefined;
|
|
17
|
+
if (typeof path === "object" && path.options.name) {
|
|
18
|
+
name = path.options.name;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const [current, setCurrent] = useState(ctx.state.pathname);
|
|
22
|
+
const href = useMemo(() => router.createHref(path, layer), [path, layer]);
|
|
23
|
+
const [isPending, setPending] = useState(false);
|
|
24
|
+
const isActive = current === href;
|
|
25
|
+
|
|
26
|
+
useEffect(
|
|
27
|
+
() => ctx.router.on("end", ({ pathname }) => setCurrent(pathname)),
|
|
28
|
+
[],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
name,
|
|
33
|
+
isPending,
|
|
34
|
+
isActive,
|
|
35
|
+
anchorProps: {
|
|
36
|
+
href,
|
|
37
|
+
onClick: (ev: any) => {
|
|
38
|
+
ev.stopPropagation();
|
|
39
|
+
ev.preventDefault();
|
|
40
|
+
if (isActive) return;
|
|
41
|
+
if (isPending) return;
|
|
42
|
+
|
|
43
|
+
setPending(true);
|
|
44
|
+
router.go(href).then(() => {
|
|
45
|
+
setPending(false);
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export interface UseActiveHook {
|
|
53
|
+
isActive: boolean;
|
|
54
|
+
anchorProps: AnchorProps;
|
|
55
|
+
isPending: boolean;
|
|
56
|
+
name?: string;
|
|
57
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ClassEntry } from "@alepha/core";
|
|
2
|
+
import { useContext } from "react";
|
|
3
|
+
import { RouterContext } from "../contexts/RouterContext";
|
|
4
|
+
|
|
5
|
+
export const useInject = <T extends object>(classEntry: ClassEntry<T>): T => {
|
|
6
|
+
const ctx = useContext(RouterContext);
|
|
7
|
+
if (!ctx) {
|
|
8
|
+
throw new Error("useRouter must be used within a <RouterProvider>");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return ctx.alepha.get(classEntry);
|
|
12
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Alepha, Static, TObject } from "@alepha/core";
|
|
2
|
+
import { useContext, useEffect, useState } from "react";
|
|
3
|
+
import { RouterContext } from "../contexts/RouterContext";
|
|
4
|
+
import { useRouter } from "./useRouter";
|
|
5
|
+
|
|
6
|
+
export interface UseQueryParamsHookOptions {
|
|
7
|
+
format?: "base64" | "querystring";
|
|
8
|
+
key?: string;
|
|
9
|
+
push?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const useQueryParams = <T extends TObject>(
|
|
13
|
+
schema: T,
|
|
14
|
+
options: UseQueryParamsHookOptions = {},
|
|
15
|
+
): [Static<T>, (data: Static<T>) => void] => {
|
|
16
|
+
const ctx = useContext(RouterContext);
|
|
17
|
+
if (!ctx) {
|
|
18
|
+
throw new Error("useQueryParams must be used within a RouterProvider");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const key = options.key ?? "q";
|
|
22
|
+
const router = useRouter();
|
|
23
|
+
const querystring = router.query[key];
|
|
24
|
+
|
|
25
|
+
const [queryParams, setQueryParams] = useState(
|
|
26
|
+
decode(ctx.alepha, schema, router.query[key]),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
setQueryParams(decode(ctx.alepha, schema, querystring));
|
|
31
|
+
}, [querystring]);
|
|
32
|
+
|
|
33
|
+
return [
|
|
34
|
+
queryParams,
|
|
35
|
+
(queryParams: Static<T>) => {
|
|
36
|
+
setQueryParams(queryParams);
|
|
37
|
+
router.setQueryParams(
|
|
38
|
+
{ [key]: encode(ctx.alepha, schema, queryParams) },
|
|
39
|
+
{
|
|
40
|
+
merge: true,
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
const encode = (alepha: Alepha, schema: TObject, data: any) => {
|
|
50
|
+
return btoa(JSON.stringify(alepha.parse(schema, data)));
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const decode = (alepha: Alepha, schema: TObject, data: any) => {
|
|
54
|
+
try {
|
|
55
|
+
return alepha.parse(schema, JSON.parse(atob(decodeURIComponent(data))));
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useContext, useMemo } from "react";
|
|
2
|
+
import { RouterContext } from "../contexts/RouterContext";
|
|
3
|
+
import { RouterLayerContext } from "../contexts/RouterLayerContext";
|
|
4
|
+
import { ReactBrowserProvider } from "../providers/ReactBrowserProvider";
|
|
5
|
+
import { RouterHookApi } from "./RouterHookApi";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
*/
|
|
10
|
+
export const useRouter = (): RouterHookApi => {
|
|
11
|
+
const ctx = useContext(RouterContext);
|
|
12
|
+
const layer = useContext(RouterLayerContext);
|
|
13
|
+
if (!ctx || !layer) {
|
|
14
|
+
throw new Error("useRouter must be used within a RouterProvider");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return useMemo(
|
|
18
|
+
() =>
|
|
19
|
+
new RouterHookApi(
|
|
20
|
+
ctx.state,
|
|
21
|
+
layer,
|
|
22
|
+
ctx.alepha.isBrowser()
|
|
23
|
+
? ctx.alepha.get(ReactBrowserProvider)
|
|
24
|
+
: undefined,
|
|
25
|
+
),
|
|
26
|
+
[ctx.router, layer],
|
|
27
|
+
);
|
|
28
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useContext, useEffect } from "react";
|
|
2
|
+
import { RouterContext } from "../contexts/RouterContext";
|
|
3
|
+
import { RouterLayerContext } from "../contexts/RouterLayerContext";
|
|
4
|
+
import type { RouterState } from "../services/Router";
|
|
5
|
+
|
|
6
|
+
export const useRouterEvents = (
|
|
7
|
+
opts: {
|
|
8
|
+
onBegin?: () => void;
|
|
9
|
+
onEnd?: (it: RouterState) => void;
|
|
10
|
+
onError?: (it: Error) => void;
|
|
11
|
+
} = {},
|
|
12
|
+
) => {
|
|
13
|
+
const ctx = useContext(RouterContext);
|
|
14
|
+
const layer = useContext(RouterLayerContext);
|
|
15
|
+
if (!ctx || !layer) {
|
|
16
|
+
throw new Error("useRouter must be used within a RouterProvider");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const subs: Function[] = [];
|
|
21
|
+
const onBegin = opts.onBegin;
|
|
22
|
+
const onEnd = opts.onEnd;
|
|
23
|
+
const onError = opts.onError;
|
|
24
|
+
|
|
25
|
+
if (onBegin) {
|
|
26
|
+
subs.push(ctx.router.on("begin", onBegin));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (onEnd) {
|
|
30
|
+
subs.push(ctx.router.on("end", onEnd));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (onError) {
|
|
34
|
+
subs.push(ctx.router.on("error", onError));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return () => {
|
|
38
|
+
for (const sub of subs) {
|
|
39
|
+
sub();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}, []);
|
|
43
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useContext, useEffect, useState } from "react";
|
|
2
|
+
import { RouterContext } from "../contexts/RouterContext";
|
|
3
|
+
import { RouterLayerContext } from "../contexts/RouterLayerContext";
|
|
4
|
+
import type { RouterState } from "../services/Router";
|
|
5
|
+
|
|
6
|
+
export const useRouterState = (): RouterState => {
|
|
7
|
+
const ctx = useContext(RouterContext);
|
|
8
|
+
const layer = useContext(RouterLayerContext);
|
|
9
|
+
if (!ctx || !layer) {
|
|
10
|
+
throw new Error("useRouter must be used within a RouterProvider");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const [state, setState] = useState(ctx.state);
|
|
14
|
+
useEffect(
|
|
15
|
+
() =>
|
|
16
|
+
ctx.router.on("end", (it) => {
|
|
17
|
+
setState({ ...it });
|
|
18
|
+
}),
|
|
19
|
+
[],
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
return state;
|
|
23
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { $inject, Alepha, autoInject } from "@alepha/core";
|
|
2
|
+
import { $page } from "./descriptors/$page";
|
|
3
|
+
import { PageDescriptorProvider } from "./providers/PageDescriptorProvider";
|
|
4
|
+
import { ReactBrowserProvider } from "./providers/ReactBrowserProvider";
|
|
5
|
+
|
|
6
|
+
export * from "./index.shared";
|
|
7
|
+
export * from "./providers/ReactBrowserProvider";
|
|
8
|
+
|
|
9
|
+
export class ReactModule {
|
|
10
|
+
protected readonly alepha = $inject(Alepha);
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
this.alepha //
|
|
14
|
+
.with(PageDescriptorProvider)
|
|
15
|
+
.with(ReactBrowserProvider);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
autoInject($page, ReactModule);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { default as NestedView } from "./components/NestedView";
|
|
2
|
+
|
|
3
|
+
export * from "./contexts/RouterContext";
|
|
4
|
+
export * from "./contexts/RouterLayerContext";
|
|
5
|
+
|
|
6
|
+
export * from "./descriptors/$page";
|
|
7
|
+
|
|
8
|
+
export * from "./hooks/useActive";
|
|
9
|
+
export * from "./hooks/useClient";
|
|
10
|
+
export * from "./hooks/useInject";
|
|
11
|
+
export * from "./hooks/useQueryParams";
|
|
12
|
+
export * from "./hooks/RouterHookApi";
|
|
13
|
+
export * from "./hooks/useRouter";
|
|
14
|
+
export * from "./hooks/useRouterEvents";
|
|
15
|
+
export * from "./hooks/useRouterState";
|
|
16
|
+
|
|
17
|
+
export * from "./services/Router";
|