@alepha/react 0.6.2 → 0.6.4

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.
Files changed (38) hide show
  1. package/README.md +1 -28
  2. package/dist/index.browser.cjs +20 -23
  3. package/dist/index.browser.cjs.map +1 -0
  4. package/dist/index.browser.js +8 -7
  5. package/dist/index.browser.js.map +1 -0
  6. package/dist/index.cjs +234 -542
  7. package/dist/index.cjs.map +1 -0
  8. package/dist/index.d.ts +218 -682
  9. package/dist/index.js +219 -522
  10. package/dist/index.js.map +1 -0
  11. package/dist/{useAuth-B9ypF48n.cjs → useActive-BGtt_RNQ.cjs} +310 -475
  12. package/dist/useActive-BGtt_RNQ.cjs.map +1 -0
  13. package/dist/{useAuth-Ps01oe8e.js → useActive-QkvcaSmu.js} +309 -471
  14. package/dist/useActive-QkvcaSmu.js.map +1 -0
  15. package/package.json +12 -10
  16. package/src/components/Link.tsx +35 -0
  17. package/src/components/NestedView.tsx +36 -0
  18. package/src/contexts/RouterContext.ts +18 -0
  19. package/src/contexts/RouterLayerContext.ts +10 -0
  20. package/src/descriptors/$page.ts +143 -0
  21. package/src/errors/RedirectionError.ts +7 -0
  22. package/src/hooks/RouterHookApi.ts +156 -0
  23. package/src/hooks/useActive.ts +57 -0
  24. package/src/hooks/useClient.ts +6 -0
  25. package/src/hooks/useInject.ts +14 -0
  26. package/src/hooks/useQueryParams.ts +59 -0
  27. package/src/hooks/useRouter.ts +25 -0
  28. package/src/hooks/useRouterEvents.ts +43 -0
  29. package/src/hooks/useRouterState.ts +23 -0
  30. package/src/index.browser.ts +21 -0
  31. package/src/index.shared.ts +15 -0
  32. package/src/index.ts +48 -0
  33. package/src/providers/BrowserHeadProvider.ts +43 -0
  34. package/src/providers/BrowserRouterProvider.ts +146 -0
  35. package/src/providers/PageDescriptorProvider.ts +534 -0
  36. package/src/providers/ReactBrowserProvider.ts +223 -0
  37. package/src/providers/ReactServerProvider.ts +278 -0
  38. package/src/providers/ServerHeadProvider.ts +91 -0
@@ -0,0 +1,57 @@
1
+ import { useContext, useEffect, useMemo, useState } from "react";
2
+ import { RouterContext } from "../contexts/RouterContext.ts";
3
+ import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
4
+ import type { AnchorProps } from "../providers/PageDescriptorProvider.ts";
5
+ import type { HrefLike } from "./RouterHookApi.ts";
6
+ import { useRouter } from "./useRouter.ts";
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.events.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,6 @@
1
+ import { HttpClient } from "@alepha/server";
2
+ import { useInject } from "./useInject.ts";
3
+
4
+ export const useClient = (): HttpClient => {
5
+ return useInject(HttpClient);
6
+ };
@@ -0,0 +1,14 @@
1
+ import type { Class } from "@alepha/core";
2
+ import { useContext } from "react";
3
+ import { RouterContext } from "../contexts/RouterContext.ts";
4
+
5
+ export const useInject = <T extends object>(clazz: Class<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(clazz, {
12
+ skipRegistration: true,
13
+ });
14
+ };
@@ -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.ts";
4
+ import { useRouter } from "./useRouter.ts";
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,25 @@
1
+ import { useContext, useMemo } from "react";
2
+ import { RouterContext } from "../contexts/RouterContext.ts";
3
+ import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
4
+ import { ReactBrowserProvider } from "../providers/ReactBrowserProvider.ts";
5
+ import { RouterHookApi } from "./RouterHookApi.ts";
6
+
7
+ export const useRouter = (): RouterHookApi => {
8
+ const ctx = useContext(RouterContext);
9
+ const layer = useContext(RouterLayerContext);
10
+ if (!ctx || !layer) {
11
+ throw new Error("useRouter must be used within a RouterProvider");
12
+ }
13
+
14
+ return useMemo(
15
+ () =>
16
+ new RouterHookApi(
17
+ ctx.state,
18
+ layer,
19
+ ctx.alepha.isBrowser()
20
+ ? ctx.alepha.get(ReactBrowserProvider)
21
+ : undefined,
22
+ ),
23
+ [layer],
24
+ );
25
+ };
@@ -0,0 +1,43 @@
1
+ import { useContext, useEffect } from "react";
2
+ import { RouterContext } from "../contexts/RouterContext.ts";
3
+ import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
4
+ import type { RouterState } from "../providers/PageDescriptorProvider.ts";
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.events.on("begin", onBegin));
27
+ }
28
+
29
+ if (onEnd) {
30
+ subs.push(ctx.events.on("end", onEnd));
31
+ }
32
+
33
+ if (onError) {
34
+ subs.push(ctx.events.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.ts";
3
+ import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
4
+ import type { RouterState } from "../providers/PageDescriptorProvider.ts";
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.events.on("end", (it) => {
17
+ setState({ ...it });
18
+ }),
19
+ [],
20
+ );
21
+
22
+ return state;
23
+ };
@@ -0,0 +1,21 @@
1
+ import { $inject, Alepha, __bind } from "@alepha/core";
2
+ import { $page } from "./descriptors/$page.ts";
3
+ import { BrowserRouterProvider } from "./providers/BrowserRouterProvider.ts";
4
+ import { PageDescriptorProvider } from "./providers/PageDescriptorProvider.ts";
5
+ import { ReactBrowserProvider } from "./providers/ReactBrowserProvider.ts";
6
+
7
+ export * from "./index.shared";
8
+ export * from "./providers/ReactBrowserProvider.ts";
9
+
10
+ export class ReactModule {
11
+ protected readonly alepha = $inject(Alepha);
12
+
13
+ constructor() {
14
+ this.alepha //
15
+ .with(PageDescriptorProvider)
16
+ .with(ReactBrowserProvider)
17
+ .with(BrowserRouterProvider);
18
+ }
19
+ }
20
+
21
+ __bind($page, ReactModule);
@@ -0,0 +1,15 @@
1
+ export { default as NestedView } from "./components/NestedView.tsx";
2
+ export { default as Link } from "./components/Link.tsx";
3
+
4
+ export * from "./contexts/RouterContext.ts";
5
+ export * from "./contexts/RouterLayerContext.ts";
6
+ export * from "./descriptors/$page.ts";
7
+ export * from "./hooks/RouterHookApi.ts";
8
+ export * from "./hooks/useInject.ts";
9
+ export * from "./hooks/useClient.ts";
10
+ export * from "./hooks/useQueryParams.ts";
11
+ export * from "./hooks/useRouter.ts";
12
+ export * from "./hooks/useRouterEvents.ts";
13
+ export * from "./hooks/useRouterState.ts";
14
+ export * from "./hooks/useActive.ts";
15
+ export * from "./errors/RedirectionError.ts";
package/src/index.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { $inject, Alepha, __bind } from "@alepha/core";
2
+ import {
3
+ ServerLinksProvider,
4
+ ServerModule,
5
+ type ServerRequest,
6
+ } from "@alepha/server";
7
+ import { $page } from "./descriptors/$page.ts";
8
+ import {
9
+ PageDescriptorProvider,
10
+ type PageReactContext,
11
+ type PageRequest,
12
+ } from "./providers/PageDescriptorProvider.ts";
13
+ import type { ReactHydrationState } from "./providers/ReactBrowserProvider.ts";
14
+ import { ReactServerProvider } from "./providers/ReactServerProvider.ts";
15
+ export { default as NestedView } from "./components/NestedView.tsx";
16
+
17
+ export * from "./index.shared.ts";
18
+ export * from "./providers/PageDescriptorProvider.ts";
19
+ export * from "./providers/ReactBrowserProvider.ts";
20
+ export * from "./providers/ReactServerProvider.ts";
21
+ export * from "./errors/RedirectionError.ts";
22
+
23
+ declare module "@alepha/core" {
24
+ interface Hooks {
25
+ "react:browser:render": {
26
+ context: PageReactContext;
27
+ hydration?: ReactHydrationState;
28
+ };
29
+ "react:server:render": {
30
+ request: ServerRequest;
31
+ pageRequest: PageRequest;
32
+ };
33
+ }
34
+ }
35
+
36
+ export class ReactModule {
37
+ protected readonly alepha = $inject(Alepha);
38
+
39
+ constructor() {
40
+ this.alepha //
41
+ .with(ServerModule)
42
+ .with(ServerLinksProvider)
43
+ .with(PageDescriptorProvider)
44
+ .with(ReactServerProvider);
45
+ }
46
+ }
47
+
48
+ __bind($page, ReactModule);
@@ -0,0 +1,43 @@
1
+ import type { Head } from "./ServerHeadProvider.ts";
2
+
3
+ export class BrowserHeadProvider {
4
+ renderHead(document: Document, head: Head): void {
5
+ if (head.title) {
6
+ document.title = head.title;
7
+ }
8
+
9
+ if (head.bodyAttributes) {
10
+ for (const [key, value] of Object.entries(head.bodyAttributes)) {
11
+ if (value) {
12
+ document.body.setAttribute(key, value);
13
+ } else {
14
+ document.body.removeAttribute(key);
15
+ }
16
+ }
17
+ }
18
+
19
+ if (head.htmlAttributes) {
20
+ for (const [key, value] of Object.entries(head.htmlAttributes)) {
21
+ if (value) {
22
+ document.documentElement.setAttribute(key, value);
23
+ } else {
24
+ document.documentElement.removeAttribute(key);
25
+ }
26
+ }
27
+ }
28
+
29
+ if (head.meta) {
30
+ for (const [key, value] of Object.entries(head.meta)) {
31
+ const meta = document.querySelector(`meta[name="${key}"]`);
32
+ if (meta) {
33
+ meta.setAttribute("content", value.content);
34
+ } else {
35
+ const newMeta = document.createElement("meta");
36
+ newMeta.setAttribute("name", key);
37
+ newMeta.setAttribute("content", value.content);
38
+ document.head.appendChild(newMeta);
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,146 @@
1
+ import { $hook, $inject, $logger, Alepha, EventEmitter } from "@alepha/core";
2
+ import { type Route, RouterProvider } from "@alepha/router";
3
+ import type { ReactNode } from "react";
4
+ import {
5
+ PageDescriptorProvider,
6
+ type PageReactContext,
7
+ type PageRoute,
8
+ type PageRouteEntry,
9
+ type RouterEvents,
10
+ type RouterRenderResult,
11
+ type RouterState,
12
+ type TransitionOptions,
13
+ isPageRoute,
14
+ } from "./PageDescriptorProvider.ts";
15
+
16
+ export interface BrowserRoute extends Route {
17
+ page: PageRoute;
18
+ }
19
+
20
+ export class BrowserRouterProvider extends RouterProvider<BrowserRoute> {
21
+ protected readonly log = $logger();
22
+ protected readonly alepha = $inject(Alepha);
23
+ protected readonly pageDescriptorProvider = $inject(PageDescriptorProvider);
24
+ public readonly events = new EventEmitter<RouterEvents>();
25
+
26
+ public add(entry: PageRouteEntry) {
27
+ this.pageDescriptorProvider.add(entry);
28
+ }
29
+
30
+ protected readonly configure = $hook({
31
+ name: "configure",
32
+ handler: async () => {
33
+ for (const page of this.pageDescriptorProvider.getPages()) {
34
+ // mount only if a view is provided
35
+ if (page.component || page.lazy) {
36
+ this.push({
37
+ path: page.match,
38
+ page,
39
+ });
40
+ }
41
+ }
42
+ },
43
+ });
44
+
45
+ public async transition(
46
+ url: URL,
47
+ options: TransitionOptions = {},
48
+ ): Promise<RouterRenderResult> {
49
+ const { pathname, search } = url;
50
+ const state: RouterState = {
51
+ pathname,
52
+ search,
53
+ layers: [],
54
+ head: {},
55
+ };
56
+
57
+ await this.events.emit("begin", undefined);
58
+
59
+ try {
60
+ const previous = options.previous;
61
+ const { route, params } = this.match(pathname);
62
+
63
+ const query: Record<string, string> = {};
64
+ if (search) {
65
+ for (const [key, value] of new URLSearchParams(search).entries()) {
66
+ query[key] = String(value);
67
+ }
68
+ }
69
+
70
+ if (isPageRoute(route)) {
71
+ const result = await this.pageDescriptorProvider.createLayers(
72
+ route.page,
73
+ {
74
+ url,
75
+ params: params ?? {},
76
+ query,
77
+ previous,
78
+ ...state,
79
+ head: state.head,
80
+ ...(options.context ?? {}),
81
+ },
82
+ );
83
+
84
+ if (result.redirect) {
85
+ return {
86
+ element: null,
87
+ layers: [],
88
+ redirect: result.redirect,
89
+ head: state.head,
90
+ };
91
+ }
92
+
93
+ state.layers = result.layers;
94
+ state.head = result.head;
95
+ }
96
+
97
+ if (state.layers.length === 0) {
98
+ state.layers.push({
99
+ name: "not-found",
100
+ element: "Not Found",
101
+ index: 0,
102
+ path: "/",
103
+ });
104
+ }
105
+
106
+ await this.events.emit("success", undefined);
107
+ } catch (e) {
108
+ this.log.error(e);
109
+ state.layers = [
110
+ {
111
+ name: "error",
112
+ element: this.pageDescriptorProvider.renderError(e as Error),
113
+ index: 0,
114
+ path: "/",
115
+ },
116
+ ];
117
+
118
+ await this.events.emit("error", e as Error);
119
+ }
120
+
121
+ if (!options.state) {
122
+ await this.events.emit("end", state);
123
+ return {
124
+ element: this.root(state, options.context),
125
+ layers: state.layers,
126
+ head: state.head,
127
+ };
128
+ }
129
+
130
+ options.state.layers = state.layers;
131
+ options.state.pathname = state.pathname;
132
+ options.state.search = state.search;
133
+ options.state.head = state.head;
134
+
135
+ await this.events.emit("end", options.state);
136
+ return {
137
+ element: this.root(state, options.context),
138
+ layers: options.state.layers,
139
+ head: state.head,
140
+ };
141
+ }
142
+
143
+ public root(state: RouterState, context: PageReactContext = {}): ReactNode {
144
+ return this.pageDescriptorProvider.root(state, context, this.events);
145
+ }
146
+ }