@alepha/react 0.11.10 → 0.11.12
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 -183
- package/dist/auth/index.browser.js +1460 -0
- package/dist/auth/index.browser.js.map +1 -0
- package/dist/auth/index.cjs +3647 -0
- package/dist/auth/index.cjs.map +1 -0
- package/dist/auth/index.d.cts +564 -0
- package/dist/auth/index.d.cts.map +1 -0
- package/dist/auth/index.d.ts +564 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +3615 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/{index.browser.js → core/index.browser.js} +36 -35
- package/dist/core/index.browser.js.map +1 -0
- package/dist/{index.cjs → core/index.cjs} +141 -140
- package/dist/core/index.cjs.map +1 -0
- package/dist/{index.d.cts → core/index.d.cts} +68 -68
- package/dist/core/index.d.cts.map +1 -0
- package/dist/{index.d.ts → core/index.d.ts} +68 -68
- package/dist/core/index.d.ts.map +1 -0
- package/dist/{index.js → core/index.js} +39 -38
- package/dist/core/index.js.map +1 -0
- package/dist/form/index.cjs +2054 -0
- package/dist/form/index.cjs.map +1 -0
- package/dist/form/index.d.cts +211 -0
- package/dist/form/index.d.cts.map +1 -0
- package/dist/form/index.d.ts +211 -0
- package/dist/form/index.d.ts.map +1 -0
- package/dist/form/index.js +2026 -0
- package/dist/form/index.js.map +1 -0
- package/dist/head/index.browser.js +1503 -0
- package/dist/head/index.browser.js.map +1 -0
- package/dist/head/index.cjs +1908 -0
- package/dist/head/index.cjs.map +1 -0
- package/dist/head/index.d.cts +595 -0
- package/dist/head/index.d.cts.map +1 -0
- package/dist/head/index.d.ts +601 -0
- package/dist/head/index.d.ts.map +1 -0
- package/dist/head/index.js +1880 -0
- package/dist/head/index.js.map +1 -0
- package/dist/i18n/index.cjs +1886 -0
- package/dist/i18n/index.cjs.map +1 -0
- package/dist/i18n/index.d.cts +168 -0
- package/dist/i18n/index.d.cts.map +1 -0
- package/dist/i18n/index.d.ts +168 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +1857 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/websocket/index.cjs +1774 -0
- package/dist/websocket/index.cjs.map +1 -0
- package/dist/websocket/index.d.cts +118 -0
- package/dist/websocket/index.d.cts.map +1 -0
- package/dist/websocket/index.d.ts +118 -0
- package/dist/websocket/index.d.ts.map +1 -0
- package/dist/websocket/index.js +1750 -0
- package/dist/websocket/index.js.map +1 -0
- package/package.json +89 -67
- package/src/auth/descriptors/$auth.ts +436 -0
- package/src/auth/descriptors/$authApple.ts +8 -0
- package/src/auth/descriptors/$authGithub.ts +81 -0
- package/src/auth/descriptors/$authGoogle.ts +38 -0
- package/src/auth/errors/SessionExpiredError.ts +6 -0
- package/src/auth/hooks/useAuth.ts +31 -0
- package/src/auth/index.browser.ts +16 -0
- package/src/auth/index.shared.ts +3 -0
- package/src/auth/index.ts +47 -0
- package/src/auth/providers/ReactAuthProvider.ts +629 -0
- package/src/auth/schemas/tokenResponseSchema.ts +11 -0
- package/src/auth/schemas/tokensSchema.ts +21 -0
- package/src/auth/schemas/userinfoResponseSchema.ts +10 -0
- package/src/auth/services/ReactAuth.ts +124 -0
- package/src/{components → core/components}/ErrorViewer.tsx +3 -2
- package/src/{components → core/components}/NestedView.tsx +1 -1
- package/src/{contexts → core/contexts}/AlephaContext.ts +1 -1
- package/src/{descriptors → core/descriptors}/$page.ts +4 -4
- package/src/{hooks → core/hooks}/useAction.ts +1 -1
- package/src/{hooks → core/hooks}/useAlepha.ts +1 -1
- package/src/{hooks → core/hooks}/useClient.ts +1 -1
- package/src/{hooks → core/hooks}/useEvents.ts +1 -1
- package/src/{hooks → core/hooks}/useInject.ts +1 -1
- package/src/{hooks → core/hooks}/useQueryParams.ts +1 -1
- package/src/{hooks → core/hooks}/useRouterState.ts +1 -1
- package/src/{hooks → core/hooks}/useSchema.ts +3 -3
- package/src/{hooks → core/hooks}/useStore.ts +2 -2
- package/src/{index.browser.ts → core/index.browser.ts} +4 -4
- package/src/{index.ts → core/index.ts} +6 -6
- package/src/{providers → core/providers}/ReactBrowserProvider.ts +6 -6
- package/src/{providers → core/providers}/ReactBrowserRendererProvider.ts +2 -2
- package/src/{providers → core/providers}/ReactBrowserRouterProvider.ts +3 -3
- package/src/{providers → core/providers}/ReactPageProvider.ts +3 -3
- package/src/{providers → core/providers}/ReactServerProvider.ts +7 -7
- package/src/{services → core/services}/ReactPageServerService.ts +2 -2
- package/src/{services → core/services}/ReactPageService.ts +1 -1
- package/src/{services → core/services}/ReactRouter.ts +1 -1
- package/src/form/components/FormState.tsx +17 -0
- package/src/form/hooks/useForm.ts +47 -0
- package/src/form/hooks/useFormState.ts +130 -0
- package/src/form/index.ts +38 -0
- package/src/form/services/FormModel.ts +548 -0
- package/src/head/descriptors/$head.ts +25 -0
- package/src/head/hooks/useHead.ts +62 -0
- package/src/head/index.browser.ts +25 -0
- package/src/head/index.ts +47 -0
- package/src/head/interfaces/Head.ts +46 -0
- package/src/head/providers/BrowserHeadProvider.ts +105 -0
- package/src/head/providers/HeadProvider.ts +73 -0
- package/src/head/providers/ServerHeadProvider.ts +109 -0
- package/src/i18n/README.md +76 -0
- package/src/i18n/components/Localize.tsx +35 -0
- package/src/i18n/descriptors/$dictionary.ts +65 -0
- package/src/i18n/hooks/useI18n.ts +18 -0
- package/src/i18n/index.ts +34 -0
- package/src/i18n/providers/I18nProvider.ts +277 -0
- package/src/websocket/hooks/useRoom.tsx +223 -0
- package/src/websocket/index.ts +7 -0
- package/dist/index.browser.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- /package/src/{components → core/components}/ClientOnly.tsx +0 -0
- /package/src/{components → core/components}/ErrorBoundary.tsx +0 -0
- /package/src/{components → core/components}/Link.tsx +0 -0
- /package/src/{components → core/components}/NotFound.tsx +0 -0
- /package/src/{contexts → core/contexts}/RouterLayerContext.ts +0 -0
- /package/src/{errors → core/errors}/Redirection.ts +0 -0
- /package/src/{hooks → core/hooks}/useActive.ts +0 -0
- /package/src/{hooks → core/hooks}/useRouter.ts +0 -0
- /package/src/{index.shared.ts → core/index.shared.ts} +0 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { ReactBrowserProvider, Redirection } from "@alepha/react";
|
|
2
|
+
import { $hook, $inject, Alepha } from "alepha";
|
|
3
|
+
import { $logger } from "alepha/logger";
|
|
4
|
+
import type { UserAccountToken } from "alepha/security";
|
|
5
|
+
import { HttpClient } from "alepha/server";
|
|
6
|
+
import { tokenResponseSchema } from "../schemas/tokenResponseSchema.ts";
|
|
7
|
+
import type { Tokens } from "../schemas/tokensSchema.ts";
|
|
8
|
+
import { userinfoResponseSchema } from "../schemas/userinfoResponseSchema.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Browser, SSR friendly, service to handle authentication.
|
|
12
|
+
*/
|
|
13
|
+
export class ReactAuth {
|
|
14
|
+
protected readonly log = $logger();
|
|
15
|
+
protected readonly alepha = $inject(Alepha);
|
|
16
|
+
protected readonly httpClient = $inject(HttpClient);
|
|
17
|
+
|
|
18
|
+
static path = {
|
|
19
|
+
login: "/oauth/login",
|
|
20
|
+
callback: "/oauth/callback",
|
|
21
|
+
logout: "/oauth/logout",
|
|
22
|
+
token: "/_auth/token",
|
|
23
|
+
refresh: "/_auth/refresh",
|
|
24
|
+
userinfo: "/_auth/userinfo",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
protected readonly onBeginTransition = $hook({
|
|
28
|
+
on: "react:transition:begin",
|
|
29
|
+
handler: async (event) => {
|
|
30
|
+
if (this.alepha.isBrowser()) {
|
|
31
|
+
Object.defineProperty(event.state, "user", {
|
|
32
|
+
get: () => this.user,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
protected readonly onFetchRequest = $hook({
|
|
39
|
+
on: "client:onRequest",
|
|
40
|
+
handler: async ({ request }) => {
|
|
41
|
+
if (this.alepha.isBrowser() && this.user) {
|
|
42
|
+
// ensure cookies are sent with requests and refresh-able
|
|
43
|
+
request.credentials ??= "include";
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the current authenticated user.
|
|
50
|
+
*
|
|
51
|
+
* Alias for `alepha.state.get("user")`
|
|
52
|
+
*/
|
|
53
|
+
public get user(): UserAccountToken | undefined {
|
|
54
|
+
return this.alepha.state.get("alepha.server.request.user");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public async ping() {
|
|
58
|
+
const { data } = await this.httpClient.fetch(ReactAuth.path.userinfo, {
|
|
59
|
+
schema: { response: userinfoResponseSchema },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
this.alepha.state.set("alepha.server.request.apiLinks", data.api);
|
|
63
|
+
this.alepha.state.set("alepha.server.request.user", data.user);
|
|
64
|
+
|
|
65
|
+
return data.user;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public async login(
|
|
69
|
+
provider: string,
|
|
70
|
+
options: {
|
|
71
|
+
hostname?: string;
|
|
72
|
+
username?: string;
|
|
73
|
+
password?: string;
|
|
74
|
+
redirect?: string;
|
|
75
|
+
[extra: string]: any;
|
|
76
|
+
},
|
|
77
|
+
): Promise<Tokens> {
|
|
78
|
+
if (options.username || options.password) {
|
|
79
|
+
const { data } = await this.httpClient.fetch(
|
|
80
|
+
`${options.hostname || ""}${ReactAuth.path.token}?provider=${provider}`,
|
|
81
|
+
{
|
|
82
|
+
method: "POST",
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
username: options.username,
|
|
85
|
+
password: options.password,
|
|
86
|
+
...options,
|
|
87
|
+
}),
|
|
88
|
+
schema: { response: tokenResponseSchema },
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
this.alepha.state.set("alepha.server.request.apiLinks", data.api);
|
|
93
|
+
this.alepha.state.set("alepha.server.request.user", data.user);
|
|
94
|
+
|
|
95
|
+
return data;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (this.alepha.isBrowser()) {
|
|
99
|
+
const browser = this.alepha.inject(ReactBrowserProvider);
|
|
100
|
+
const redirect =
|
|
101
|
+
options.redirect ||
|
|
102
|
+
(browser.transitioning
|
|
103
|
+
? window.location.origin + browser.transitioning.to
|
|
104
|
+
: window.location.href);
|
|
105
|
+
|
|
106
|
+
const href = `${window.location.origin}${ReactAuth.path.login}?provider=${provider}&redirect_uri=${encodeURIComponent(redirect)}`;
|
|
107
|
+
|
|
108
|
+
if (browser.transitioning) {
|
|
109
|
+
throw new Redirection(href);
|
|
110
|
+
} else {
|
|
111
|
+
window.location.href = href;
|
|
112
|
+
return {} as Tokens;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
throw new Redirection(
|
|
117
|
+
`${ReactAuth.path.login}?provider=${provider}&redirect_uri=${options.redirect || "/"}`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public logout() {
|
|
122
|
+
window.location.href = `${ReactAuth.path.logout}?post_logout_redirect_uri=${encodeURIComponent(window.location.origin)}`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Alepha } from "
|
|
1
|
+
import type { Alepha } from "alepha";
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
|
|
4
4
|
interface ErrorViewerProps {
|
|
@@ -97,6 +97,7 @@ const ErrorViewer = ({ error, alepha }: ErrorViewerProps) => {
|
|
|
97
97
|
<div style={styles.sectionHeader}>
|
|
98
98
|
<span>Stack trace</span>
|
|
99
99
|
<button
|
|
100
|
+
type="button"
|
|
100
101
|
onClick={() => copyToClipboard(error.stack!)}
|
|
101
102
|
style={styles.copyButton}
|
|
102
103
|
>
|
|
@@ -108,7 +109,7 @@ const ErrorViewer = ({ error, alepha }: ErrorViewerProps) => {
|
|
|
108
109
|
<div key={i}>{line}</div>
|
|
109
110
|
))}
|
|
110
111
|
{!expanded && hiddenLineCount > 0 && (
|
|
111
|
-
<div
|
|
112
|
+
<div style={styles.expandLine} onClick={() => setExpanded(true)}>
|
|
112
113
|
+ {hiddenLineCount} more lines...
|
|
113
114
|
</div>
|
|
114
115
|
)}
|
|
@@ -6,9 +6,9 @@ import {
|
|
|
6
6
|
KIND,
|
|
7
7
|
type Static,
|
|
8
8
|
type TSchema,
|
|
9
|
-
} from "
|
|
10
|
-
import type { ServerRequest } from "
|
|
11
|
-
import type { ServerRouteCache } from "
|
|
9
|
+
} from "alepha";
|
|
10
|
+
import type { ServerRequest } from "alepha/server";
|
|
11
|
+
import type { ServerRouteCache } from "alepha/server/cache";
|
|
12
12
|
import type { FC, ReactNode } from "react";
|
|
13
13
|
import type { ClientOnlyProps } from "../components/ClientOnly.tsx";
|
|
14
14
|
import type { Redirection } from "../errors/Redirection.ts";
|
|
@@ -234,7 +234,7 @@ export interface PageDescriptorOptions<
|
|
|
234
234
|
* If true, the page will be considered as a static page, immutable and cacheable.
|
|
235
235
|
* Replace boolean by an object to define static entries. (e.g. list of params/query)
|
|
236
236
|
*
|
|
237
|
-
* Browser-side: it only works with
|
|
237
|
+
* Browser-side: it only works with `alepha/vite`, which can pre-render the page at build time.
|
|
238
238
|
*
|
|
239
239
|
* Server-side: It will act as timeless cached page. You can use `cache` to configure the cache behavior.
|
|
240
240
|
*/
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { Alepha } from "
|
|
1
|
+
import type { Alepha } from "alepha";
|
|
2
2
|
import {
|
|
3
3
|
type FetchOptions,
|
|
4
4
|
HttpClient,
|
|
5
5
|
type RequestConfigSchema,
|
|
6
|
-
} from "
|
|
7
|
-
import { LinkProvider, type VirtualAction } from "
|
|
6
|
+
} from "alepha/server";
|
|
7
|
+
import { LinkProvider, type VirtualAction } from "alepha/server/links";
|
|
8
8
|
import { useEffect, useState } from "react";
|
|
9
9
|
import { useAlepha } from "./useAlepha.ts";
|
|
10
10
|
import { useInject } from "./useInject.ts";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { State, Static, TAtomObject } from "
|
|
2
|
-
import { Atom } from "
|
|
1
|
+
import type { State, Static, TAtomObject } from "alepha";
|
|
2
|
+
import { Atom } from "alepha";
|
|
3
3
|
import { useEffect, useMemo, useState } from "react";
|
|
4
4
|
import { useAlepha } from "./useAlepha.ts";
|
|
5
5
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { $module } from "
|
|
2
|
-
import { AlephaDateTime } from "
|
|
3
|
-
import { AlephaServer } from "
|
|
4
|
-
import { AlephaServerLinks } from "
|
|
1
|
+
import { $module } from "alepha";
|
|
2
|
+
import { AlephaDateTime } from "alepha/datetime";
|
|
3
|
+
import { AlephaServer } from "alepha/server";
|
|
4
|
+
import { AlephaServerLinks } from "alepha/server/links";
|
|
5
5
|
import { $page } from "./descriptors/$page.ts";
|
|
6
6
|
import { ReactBrowserProvider } from "./providers/ReactBrowserProvider.ts";
|
|
7
7
|
import { ReactBrowserRendererProvider } from "./providers/ReactBrowserRendererProvider.ts";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { $module } from "
|
|
2
|
-
import { AlephaDateTime } from "
|
|
3
|
-
import { AlephaServer, type ServerRequest } from "
|
|
4
|
-
import { AlephaServerCache } from "
|
|
5
|
-
import { AlephaServerLinks } from "
|
|
1
|
+
import { $module } from "alepha";
|
|
2
|
+
import { AlephaDateTime } from "alepha/datetime";
|
|
3
|
+
import { AlephaServer, type ServerRequest } from "alepha/server";
|
|
4
|
+
import { AlephaServerCache } from "alepha/server/cache";
|
|
5
|
+
import { AlephaServerLinks } from "alepha/server/links";
|
|
6
6
|
import type { ReactNode } from "react";
|
|
7
7
|
import { $page, type PageAnimation } from "./descriptors/$page.ts";
|
|
8
8
|
import type { ReactHydrationState } from "./providers/ReactBrowserProvider.ts";
|
|
@@ -24,7 +24,7 @@ export * from "./providers/ReactServerProvider.ts";
|
|
|
24
24
|
|
|
25
25
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
26
26
|
|
|
27
|
-
declare module "
|
|
27
|
+
declare module "alepha" {
|
|
28
28
|
interface State {
|
|
29
29
|
"alepha.react.router.state"?: ReactRouterState;
|
|
30
30
|
}
|
|
@@ -8,10 +8,10 @@ import {
|
|
|
8
8
|
type State,
|
|
9
9
|
type Static,
|
|
10
10
|
t,
|
|
11
|
-
} from "
|
|
12
|
-
import { DateTimeProvider } from "
|
|
13
|
-
import { $logger } from "
|
|
14
|
-
import { LinkProvider } from "
|
|
11
|
+
} from "alepha";
|
|
12
|
+
import { DateTimeProvider } from "alepha/datetime";
|
|
13
|
+
import { $logger } from "alepha/logger";
|
|
14
|
+
import { LinkProvider } from "alepha/server/links";
|
|
15
15
|
import { ReactBrowserRouterProvider } from "./ReactBrowserRouterProvider.ts";
|
|
16
16
|
import type {
|
|
17
17
|
PreviousLayerData,
|
|
@@ -25,7 +25,7 @@ const envSchema = t.object({
|
|
|
25
25
|
REACT_ROOT_ID: t.text({ default: "root" }),
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
declare module "
|
|
28
|
+
declare module "alepha" {
|
|
29
29
|
interface Env extends Partial<Static<typeof envSchema>> {}
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -46,7 +46,7 @@ export type ReactBrowserRendererOptions = Static<
|
|
|
46
46
|
typeof reactBrowserOptions.schema
|
|
47
47
|
>;
|
|
48
48
|
|
|
49
|
-
declare module "
|
|
49
|
+
declare module "alepha" {
|
|
50
50
|
interface State {
|
|
51
51
|
[reactBrowserOptions.key]: ReactBrowserRendererOptions;
|
|
52
52
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { $hook } from "
|
|
2
|
-
import { $logger } from "
|
|
1
|
+
import { $hook } from "alepha";
|
|
2
|
+
import { $logger } from "alepha/logger";
|
|
3
3
|
import { createRoot, hydrateRoot, type Root } from "react-dom/client";
|
|
4
4
|
|
|
5
5
|
export class ReactBrowserRendererProvider {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { $hook, $inject, Alepha } from "
|
|
2
|
-
import { $logger } from "
|
|
3
|
-
import { type Route, RouterProvider } from "
|
|
1
|
+
import { $hook, $inject, Alepha } from "alepha";
|
|
2
|
+
import { $logger } from "alepha/logger";
|
|
3
|
+
import { type Route, RouterProvider } from "alepha/router";
|
|
4
4
|
import { createElement, type ReactNode } from "react";
|
|
5
5
|
import NotFoundPage from "../components/NotFound.tsx";
|
|
6
6
|
import {
|
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
type Static,
|
|
8
8
|
type TSchema,
|
|
9
9
|
t,
|
|
10
|
-
} from "
|
|
11
|
-
import { $logger } from "
|
|
10
|
+
} from "alepha";
|
|
11
|
+
import { $logger } from "alepha/logger";
|
|
12
12
|
import { createElement, type ReactNode, StrictMode } from "react";
|
|
13
13
|
import ClientOnly from "../components/ClientOnly.tsx";
|
|
14
14
|
import ErrorViewer from "../components/ErrorViewer.tsx";
|
|
@@ -28,7 +28,7 @@ const envSchema = t.object({
|
|
|
28
28
|
REACT_STRICT_MODE: t.boolean({ default: true }),
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
declare module "
|
|
31
|
+
declare module "alepha" {
|
|
32
32
|
export interface Env extends Partial<Static<typeof envSchema>> {}
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -10,16 +10,16 @@ import {
|
|
|
10
10
|
AlephaError,
|
|
11
11
|
type Static,
|
|
12
12
|
t,
|
|
13
|
-
} from "
|
|
14
|
-
import { $logger } from "
|
|
13
|
+
} from "alepha";
|
|
14
|
+
import { $logger } from "alepha/logger";
|
|
15
15
|
import {
|
|
16
16
|
type ServerHandler,
|
|
17
17
|
ServerProvider,
|
|
18
18
|
ServerRouterProvider,
|
|
19
19
|
ServerTimingProvider,
|
|
20
|
-
} from "
|
|
21
|
-
import { ServerLinksProvider } from "
|
|
22
|
-
import { ServerStaticProvider } from "
|
|
20
|
+
} from "alepha/server";
|
|
21
|
+
import { ServerLinksProvider } from "alepha/server/links";
|
|
22
|
+
import { ServerStaticProvider } from "alepha/server/static";
|
|
23
23
|
import { renderToString } from "react-dom/server";
|
|
24
24
|
import {
|
|
25
25
|
$page,
|
|
@@ -46,7 +46,7 @@ const envSchema = t.object({
|
|
|
46
46
|
),
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
declare module "
|
|
49
|
+
declare module "alepha" {
|
|
50
50
|
interface Env extends Partial<Static<typeof envSchema>> {}
|
|
51
51
|
interface State {
|
|
52
52
|
"alepha.react.server.ssr"?: boolean;
|
|
@@ -80,7 +80,7 @@ export type ReactServerProviderOptions = Static<
|
|
|
80
80
|
typeof reactServerOptions.schema
|
|
81
81
|
>;
|
|
82
82
|
|
|
83
|
-
declare module "
|
|
83
|
+
declare module "alepha" {
|
|
84
84
|
interface State {
|
|
85
85
|
[reactServerOptions.key]: ReactServerProviderOptions;
|
|
86
86
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { $inject, AlephaError } from "
|
|
2
|
-
import { ServerProvider } from "
|
|
1
|
+
import { $inject, AlephaError } from "alepha";
|
|
2
|
+
import { ServerProvider } from "alepha/server";
|
|
3
3
|
import type {
|
|
4
4
|
PageDescriptorRenderOptions,
|
|
5
5
|
PageDescriptorRenderResult,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { TObject } from "alepha";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { useFormState } from "../hooks/useFormState.ts";
|
|
4
|
+
import type { FormModel } from "../services/FormModel.ts";
|
|
5
|
+
|
|
6
|
+
const FormState = <T extends TObject>(props: {
|
|
7
|
+
form: FormModel<T>;
|
|
8
|
+
children: (state: { loading: boolean; dirty: boolean }) => ReactNode;
|
|
9
|
+
}) => {
|
|
10
|
+
const formState = useFormState(props.form);
|
|
11
|
+
return props.children({
|
|
12
|
+
loading: formState.loading,
|
|
13
|
+
dirty: formState.dirty,
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default FormState;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useAlepha } from "@alepha/react";
|
|
2
|
+
import type { TObject } from "alepha";
|
|
3
|
+
import { useId, useMemo } from "react";
|
|
4
|
+
import { FormCtrlOptions, FormModel } from "../services/FormModel.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Custom hook to create a form with validation and field management.
|
|
8
|
+
* This hook uses TypeBox schemas to define the structure and validation rules for the form.
|
|
9
|
+
* It provides a way to handle form submission, field creation, and value management.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* import { t } from "alepha";
|
|
14
|
+
*
|
|
15
|
+
* const form = useForm({
|
|
16
|
+
* schema: t.object({
|
|
17
|
+
* username: t.text(),
|
|
18
|
+
* password: t.text(),
|
|
19
|
+
* }),
|
|
20
|
+
* handler: (values) => {
|
|
21
|
+
* console.log("Form submitted with values:", values);
|
|
22
|
+
* },
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* return (
|
|
26
|
+
* <form {...form.props}>
|
|
27
|
+
* <input {...form.input.username.props} />
|
|
28
|
+
* <input {...form.input.password.props} />
|
|
29
|
+
* <button type="submit">Submit</button>
|
|
30
|
+
* </form>
|
|
31
|
+
* );
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export const useForm = <T extends TObject>(
|
|
35
|
+
options: FormCtrlOptions<T>,
|
|
36
|
+
deps: any[] = [],
|
|
37
|
+
): FormModel<T> => {
|
|
38
|
+
const alepha = useAlepha();
|
|
39
|
+
const formId = useId();
|
|
40
|
+
|
|
41
|
+
return useMemo(() => {
|
|
42
|
+
return alepha.inject(FormModel<T>, {
|
|
43
|
+
lifetime: "transient",
|
|
44
|
+
args: [options.id || formId, options],
|
|
45
|
+
});
|
|
46
|
+
}, deps);
|
|
47
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { useAlepha } from "@alepha/react";
|
|
2
|
+
import type { FormModel } from "../services/FormModel.ts";
|
|
3
|
+
import { type TObject, TypeBoxError } from "alepha";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
|
|
6
|
+
export interface UseFormStateReturn {
|
|
7
|
+
loading: boolean;
|
|
8
|
+
dirty: boolean;
|
|
9
|
+
values?: Record<string, any>;
|
|
10
|
+
error?: Error;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const useFormState = <
|
|
14
|
+
T extends TObject,
|
|
15
|
+
Keys extends keyof UseFormStateReturn,
|
|
16
|
+
>(
|
|
17
|
+
target: FormModel<T> | { form: FormModel<T>; path: string },
|
|
18
|
+
_events: Keys[] = ["loading", "dirty", "error"] as Keys[],
|
|
19
|
+
): Pick<UseFormStateReturn, Keys> => {
|
|
20
|
+
const alepha = useAlepha();
|
|
21
|
+
const events = _events as string[];
|
|
22
|
+
|
|
23
|
+
const [dirty, setDirty] = useState(false);
|
|
24
|
+
const [loading, setLoading] = useState(false);
|
|
25
|
+
const [error, setError] = useState<Error | undefined>(undefined);
|
|
26
|
+
const [values, setValues] = useState<Record<string, any> | undefined>(
|
|
27
|
+
undefined,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const form = "form" in target ? target.form : target;
|
|
31
|
+
const path = "path" in target ? target.path : undefined;
|
|
32
|
+
|
|
33
|
+
const hasValues = events.includes("values");
|
|
34
|
+
const hasErrors = events.includes("error");
|
|
35
|
+
const hasDirty = events.includes("dirty");
|
|
36
|
+
const hasLoading = events.includes("loading");
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const listeners: Function[] = [];
|
|
40
|
+
|
|
41
|
+
if (hasErrors || hasValues || hasDirty) {
|
|
42
|
+
listeners.push(
|
|
43
|
+
alepha.events.on("form:change", (event) => {
|
|
44
|
+
if (event.id === form.id) {
|
|
45
|
+
if (!path || event.path === path) {
|
|
46
|
+
if (hasDirty) {
|
|
47
|
+
setDirty(true);
|
|
48
|
+
}
|
|
49
|
+
if (hasErrors) {
|
|
50
|
+
setError(undefined);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (hasValues) {
|
|
54
|
+
setValues(form.currentValues);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (hasValues) {
|
|
62
|
+
listeners.push(
|
|
63
|
+
alepha.events.on("form:reset", (event) => {
|
|
64
|
+
if (event.id === form.id) {
|
|
65
|
+
setValues(event.values);
|
|
66
|
+
}
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (hasLoading) {
|
|
72
|
+
listeners.push(
|
|
73
|
+
alepha.events.on("form:submit:begin", (event) => {
|
|
74
|
+
if (event.id === form.id) {
|
|
75
|
+
setLoading(true);
|
|
76
|
+
}
|
|
77
|
+
}),
|
|
78
|
+
alepha.events.on("form:submit:end", (event) => {
|
|
79
|
+
if (event.id === form.id) {
|
|
80
|
+
setLoading(false);
|
|
81
|
+
}
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (hasValues || hasDirty) {
|
|
87
|
+
listeners.push(
|
|
88
|
+
alepha.events.on("form:submit:success", (event) => {
|
|
89
|
+
if (event.id === form.id) {
|
|
90
|
+
if (hasValues) {
|
|
91
|
+
setValues(event.values);
|
|
92
|
+
}
|
|
93
|
+
if (hasDirty) {
|
|
94
|
+
setDirty(false);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (hasErrors) {
|
|
102
|
+
listeners.push(
|
|
103
|
+
alepha.events.on("form:submit:error", (event) => {
|
|
104
|
+
if (event.id === form.id) {
|
|
105
|
+
if (
|
|
106
|
+
!path ||
|
|
107
|
+
(event.error instanceof TypeBoxError &&
|
|
108
|
+
event.error.value.path === path)
|
|
109
|
+
) {
|
|
110
|
+
setError(event.error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return () => {
|
|
118
|
+
for (const unsub of listeners) {
|
|
119
|
+
unsub();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
dirty,
|
|
126
|
+
loading,
|
|
127
|
+
error,
|
|
128
|
+
values,
|
|
129
|
+
} as Pick<UseFormStateReturn, Keys>;
|
|
130
|
+
};
|