@alepha/react 0.5.0 → 0.5.1

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.
@@ -0,0 +1,944 @@
1
+ 'use strict';
2
+
3
+ var core = require('@alepha/core');
4
+ var react = require('react');
5
+ var server = require('@alepha/server');
6
+ var client = require('react-dom/client');
7
+ var pathToRegexp = require('path-to-regexp');
8
+
9
+ const pageDescriptorKey = "PAGE";
10
+ const $page = (options) => {
11
+ core.__descriptor(pageDescriptorKey);
12
+ return {
13
+ [core.KIND]: pageDescriptorKey,
14
+ options,
15
+ render: () => {
16
+ throw new core.NotImplementedError(pageDescriptorKey);
17
+ },
18
+ go: () => {
19
+ throw new core.NotImplementedError(pageDescriptorKey);
20
+ },
21
+ createAnchorProps: () => {
22
+ throw new core.NotImplementedError(pageDescriptorKey);
23
+ }
24
+ };
25
+ };
26
+ $page[core.KIND] = pageDescriptorKey;
27
+
28
+ const RouterContext = react.createContext(
29
+ void 0
30
+ );
31
+
32
+ const RouterLayerContext = react.createContext(void 0);
33
+
34
+ const NestedView = (props) => {
35
+ const app = react.useContext(RouterContext);
36
+ const layer = react.useContext(RouterLayerContext);
37
+ const index = layer?.index ?? 0;
38
+ const [view, setView] = react.useState(
39
+ app?.state.layers[index]?.element
40
+ );
41
+ react.useEffect(() => {
42
+ if (app?.alepha.isBrowser()) {
43
+ return app?.router.on("end", ({ layers }) => {
44
+ setView(layers[index]?.element);
45
+ });
46
+ }
47
+ }, [app]);
48
+ return view ?? props.children ?? null;
49
+ };
50
+
51
+ class RedirectException extends Error {
52
+ constructor(page) {
53
+ super("Redirection");
54
+ this.page = page;
55
+ }
56
+ }
57
+ class Router extends core.EventEmitter {
58
+ log = core.$logger();
59
+ alepha = core.$inject(core.Alepha);
60
+ pages = [];
61
+ notFoundPageRoute;
62
+ /**
63
+ * Get the page by name.
64
+ *
65
+ * @param name - Page name
66
+ * @return PageRoute
67
+ */
68
+ page(name) {
69
+ const found = this.pages.find((it) => it.name === name);
70
+ if (!found) {
71
+ throw new Error(`Page ${name} not found`);
72
+ }
73
+ return found;
74
+ }
75
+ /**
76
+ *
77
+ */
78
+ root(state, opts = {}) {
79
+ return react.createElement(
80
+ RouterContext.Provider,
81
+ {
82
+ value: {
83
+ state,
84
+ router: this,
85
+ alepha: this.alepha,
86
+ session: opts.user ? { user: opts.user } : void 0
87
+ }
88
+ },
89
+ state.layers[0]?.element
90
+ );
91
+ }
92
+ /**
93
+ *
94
+ * @param url
95
+ * @param options
96
+ */
97
+ async render(url, options = {}) {
98
+ const [pathname, search = ""] = url.split("?");
99
+ const state = {
100
+ pathname,
101
+ search,
102
+ layers: []
103
+ };
104
+ this.emit("begin", void 0);
105
+ try {
106
+ let layers = await this.match(url, options);
107
+ if (layers.length === 0) {
108
+ if (this.notFoundPageRoute) {
109
+ layers = await this.createLayers(url, this.notFoundPageRoute);
110
+ } else {
111
+ layers.push({
112
+ name: "not-found",
113
+ element: "Not Found",
114
+ index: 0,
115
+ path: "/"
116
+ });
117
+ }
118
+ }
119
+ state.layers = layers;
120
+ this.emit("success", void 0);
121
+ } catch (e) {
122
+ if (e instanceof RedirectException) {
123
+ return {
124
+ element: null,
125
+ layers: [],
126
+ redirect: typeof e.page === "string" ? e.page : this.href(e.page)
127
+ };
128
+ }
129
+ this.log.error(e);
130
+ state.layers = [
131
+ {
132
+ name: "error",
133
+ element: this.renderError(e),
134
+ index: 0,
135
+ path: "/"
136
+ }
137
+ ];
138
+ this.emit("error", e);
139
+ }
140
+ if (options.state) {
141
+ options.state.layers = state.layers;
142
+ options.state.pathname = state.pathname;
143
+ options.state.search = state.search;
144
+ this.emit("end", options.state);
145
+ return {
146
+ element: this.root(options.state, options),
147
+ layers: options.state.layers
148
+ };
149
+ }
150
+ this.emit("end", state);
151
+ return {
152
+ element: this.root(state, options),
153
+ layers: state.layers
154
+ };
155
+ }
156
+ /**
157
+ *
158
+ * @param url
159
+ * @param options
160
+ * @protected
161
+ */
162
+ async match(url, options = {}) {
163
+ const pages = this.pages;
164
+ const previous = options.previous;
165
+ const [pathname, search] = url.split("?");
166
+ for (const route of pages) {
167
+ if (route.children?.find((it) => !it.path || it.path === "/")) continue;
168
+ if (!route.match) continue;
169
+ const match2 = route.match.exec(pathname);
170
+ if (match2) {
171
+ const params = match2.params ?? {};
172
+ const query = {};
173
+ if (search) {
174
+ for (const [key, value] of new URLSearchParams(search).entries()) {
175
+ query[key] = String(value);
176
+ }
177
+ }
178
+ return await this.createLayers(
179
+ url,
180
+ route,
181
+ params,
182
+ query,
183
+ previous,
184
+ options.user
185
+ );
186
+ }
187
+ }
188
+ return [];
189
+ }
190
+ /**
191
+ * Create layers for the given route.
192
+ *
193
+ * @param route
194
+ * @param params
195
+ * @param query
196
+ * @param previous
197
+ * @param user
198
+ * @protected
199
+ */
200
+ async createLayers(url, route, params = {}, query = {}, previous = [], user) {
201
+ const layers = [];
202
+ let context = {};
203
+ const stack = [{ route }];
204
+ let parent = route.parent;
205
+ while (parent) {
206
+ stack.unshift({ route: parent });
207
+ parent = parent.parent;
208
+ }
209
+ let forceRefresh = false;
210
+ for (let i = 0; i < stack.length; i++) {
211
+ const it = stack[i];
212
+ const route2 = it.route;
213
+ const config = {};
214
+ try {
215
+ config.query = route2.schema?.query ? this.alepha.parse(route2.schema.query, query) : {};
216
+ } catch (e) {
217
+ it.error = e;
218
+ break;
219
+ }
220
+ try {
221
+ config.params = route2.schema?.params ? this.alepha.parse(route2.schema.params, params) : {};
222
+ } catch (e) {
223
+ it.error = e;
224
+ break;
225
+ }
226
+ it.config = {
227
+ ...config
228
+ };
229
+ if (!route2.resolve) {
230
+ continue;
231
+ }
232
+ if (previous?.[i] && !forceRefresh && previous[i].name === route2.name) {
233
+ const url2 = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
234
+ const prev = JSON.stringify({
235
+ part: url2(previous[i].part),
236
+ params: previous[i].config?.params ?? {}
237
+ });
238
+ const curr = JSON.stringify({
239
+ part: url2(route2.path),
240
+ params: config.params ?? {}
241
+ });
242
+ if (prev === curr) {
243
+ it.props = previous[i].props;
244
+ context = {
245
+ ...context,
246
+ ...it.props
247
+ };
248
+ continue;
249
+ }
250
+ forceRefresh = true;
251
+ }
252
+ try {
253
+ const props = await route2.resolve?.({
254
+ ...config,
255
+ ...context,
256
+ user,
257
+ url
258
+ }) ?? {};
259
+ it.props = {
260
+ ...props
261
+ };
262
+ context = {
263
+ ...context,
264
+ ...props
265
+ };
266
+ } catch (e) {
267
+ if (e instanceof RedirectException) {
268
+ throw e;
269
+ }
270
+ this.log.error(e);
271
+ it.error = e;
272
+ break;
273
+ }
274
+ }
275
+ let acc = "";
276
+ for (let i = 0; i < stack.length; i++) {
277
+ const it = stack[i];
278
+ const props = it.props ?? {};
279
+ const params2 = { ...it.config?.params };
280
+ for (const key of Object.keys(params2)) {
281
+ params2[key] = String(params2[key]);
282
+ }
283
+ acc += "/";
284
+ acc += it.route.path ? pathToRegexp.compile(it.route.path)(params2) : "";
285
+ const path = acc.replace(/\/+/, "/");
286
+ if (it.error) {
287
+ const errorHandler = this.getErrorHandler(it.route);
288
+ const element = errorHandler ? errorHandler({
289
+ ...it.config,
290
+ error: it.error,
291
+ url
292
+ }) : this.renderError(it.error);
293
+ layers.push({
294
+ props,
295
+ name: it.route.name,
296
+ part: it.route.path,
297
+ config: it.config,
298
+ element: this.renderView(i + 1, path, element),
299
+ index: i + 1,
300
+ path
301
+ });
302
+ break;
303
+ }
304
+ const layer = await this.createElement(it.route, {
305
+ ...props,
306
+ ...context
307
+ });
308
+ layers.push({
309
+ name: it.route.name,
310
+ props,
311
+ part: it.route.path,
312
+ config: it.config,
313
+ element: this.renderView(i + 1, path, layer),
314
+ index: i + 1,
315
+ path
316
+ });
317
+ }
318
+ return layers;
319
+ }
320
+ /**
321
+ *
322
+ * @param route
323
+ * @protected
324
+ */
325
+ getErrorHandler(route) {
326
+ if (route.errorHandler) return route.errorHandler;
327
+ let parent = route.parent;
328
+ while (parent) {
329
+ if (parent.errorHandler) return parent.errorHandler;
330
+ parent = parent.parent;
331
+ }
332
+ }
333
+ /**
334
+ *
335
+ * @param page
336
+ * @param props
337
+ * @protected
338
+ */
339
+ async createElement(page, props) {
340
+ if (page.lazy) {
341
+ const component = await page.lazy();
342
+ return react.createElement(component.default, props);
343
+ }
344
+ if (page.component) {
345
+ return react.createElement(page.component, props);
346
+ }
347
+ return void 0;
348
+ }
349
+ /**
350
+ *
351
+ * @param e
352
+ * @protected
353
+ */
354
+ renderError(e) {
355
+ return react.createElement("pre", { style: { overflow: "auto" } }, `${e.stack}`);
356
+ }
357
+ /**
358
+ * Render an empty view.
359
+ *
360
+ * @protected
361
+ */
362
+ renderEmptyView() {
363
+ return react.createElement(NestedView, {});
364
+ }
365
+ /**
366
+ * Create a valid href for the given page.
367
+ * @param page
368
+ * @param params
369
+ */
370
+ href(page, params = {}) {
371
+ const found = this.pages.find((it) => it.name === page.options.name);
372
+ if (!found) {
373
+ throw new Error(`Page ${page.options.name} not found`);
374
+ }
375
+ let url = found.path ?? "";
376
+ let parent = found.parent;
377
+ while (parent) {
378
+ url = `${parent.path ?? ""}/${url}`;
379
+ parent = parent.parent;
380
+ }
381
+ url = pathToRegexp.compile(url)(params);
382
+ return url.replace(/\/\/+/g, "/") || "/";
383
+ }
384
+ /**
385
+ *
386
+ * @param index
387
+ * @param path
388
+ * @param view
389
+ * @protected
390
+ */
391
+ renderView(index, path, view = this.renderEmptyView()) {
392
+ return react.createElement(
393
+ RouterLayerContext.Provider,
394
+ {
395
+ value: {
396
+ index,
397
+ path
398
+ }
399
+ },
400
+ view
401
+ );
402
+ }
403
+ /**
404
+ *
405
+ * @param entry
406
+ */
407
+ add(entry) {
408
+ if (this.alepha.isReady()) {
409
+ throw new Error("Router is already initialized");
410
+ }
411
+ if (entry.notFoundHandler) {
412
+ this.notFoundPageRoute = {
413
+ name: "not-found",
414
+ component: entry.notFoundHandler
415
+ };
416
+ }
417
+ entry.name ??= this.nextId();
418
+ const page = entry;
419
+ page.match = this.createMatchFunction(page);
420
+ this.pages.push(page);
421
+ if (page.children) {
422
+ for (const child of page.children) {
423
+ child.parent = page;
424
+ this.add(child);
425
+ }
426
+ }
427
+ }
428
+ /**
429
+ * Create a match function for the given page.
430
+ *
431
+ * @param page
432
+ * @protected
433
+ */
434
+ createMatchFunction(page) {
435
+ let url = page.path ?? "/";
436
+ let target = page.parent;
437
+ while (target) {
438
+ url = `${target.path ?? ""}/${url}`;
439
+ target = target.parent;
440
+ }
441
+ let path = url.replace(/\/\/+/g, "/");
442
+ if (path.endsWith("/")) {
443
+ path = path.slice(0, -1);
444
+ }
445
+ if (path.includes("?")) {
446
+ return {
447
+ exec: pathToRegexp.match(path.split("?")[0]),
448
+ path
449
+ };
450
+ }
451
+ return {
452
+ exec: pathToRegexp.match(path),
453
+ path
454
+ };
455
+ }
456
+ /**
457
+ *
458
+ */
459
+ empty() {
460
+ return this.pages.length === 0;
461
+ }
462
+ /**
463
+ *
464
+ * @protected
465
+ */
466
+ _next = 0;
467
+ /**
468
+ *
469
+ * @protected
470
+ */
471
+ nextId() {
472
+ this._next += 1;
473
+ return `P${this._next}`;
474
+ }
475
+ }
476
+
477
+ class PageDescriptorProvider {
478
+ alepha = core.$inject(core.Alepha);
479
+ router = core.$inject(Router);
480
+ configure = core.$hook({
481
+ name: "configure",
482
+ handler: () => {
483
+ const pages = this.alepha.getDescriptorValues($page);
484
+ for (const { value, key } of pages) {
485
+ value.options.name ??= key;
486
+ if (pages.find((it) => it.value.options.children?.().includes(value))) {
487
+ continue;
488
+ }
489
+ this.router.add(this.map(pages, value));
490
+ }
491
+ }
492
+ });
493
+ /**
494
+ * Transform
495
+ * @param pages
496
+ * @param target
497
+ * @protected
498
+ */
499
+ map(pages, target) {
500
+ const children = target.options.children?.() ?? [];
501
+ for (const it of pages) {
502
+ if (it.value.options.parent === target) {
503
+ children.push(it.value);
504
+ }
505
+ }
506
+ return {
507
+ ...target.options,
508
+ parent: void 0,
509
+ children: children.map((it) => this.map(pages, it))
510
+ };
511
+ }
512
+ }
513
+
514
+ class ReactBrowserProvider {
515
+ log = core.$logger();
516
+ client = core.$inject(server.HttpClient);
517
+ router = core.$inject(Router);
518
+ root;
519
+ transitioning;
520
+ state = { layers: [], pathname: "", search: "" };
521
+ /**
522
+ *
523
+ */
524
+ get document() {
525
+ return window.document;
526
+ }
527
+ /**
528
+ *
529
+ */
530
+ get history() {
531
+ return window.history;
532
+ }
533
+ /**
534
+ *
535
+ */
536
+ get url() {
537
+ return window.location.pathname + window.location.search;
538
+ }
539
+ /**
540
+ *
541
+ * @param props
542
+ */
543
+ async invalidate(props) {
544
+ const previous = [];
545
+ if (props) {
546
+ const [key] = Object.keys(props);
547
+ const value = props[key];
548
+ for (const layer of this.state.layers) {
549
+ if (layer.props?.[key]) {
550
+ previous.push({
551
+ ...layer,
552
+ props: {
553
+ ...layer.props,
554
+ [key]: value
555
+ }
556
+ });
557
+ break;
558
+ }
559
+ previous.push(layer);
560
+ }
561
+ }
562
+ await this.render({ previous });
563
+ }
564
+ /**
565
+ *
566
+ * @param url
567
+ * @param options
568
+ */
569
+ async go(url, options = {}) {
570
+ const result = await this.render({
571
+ url
572
+ });
573
+ if (result.url !== url) {
574
+ this.history.replaceState({}, "", result.url);
575
+ return;
576
+ }
577
+ if (options.replace) {
578
+ this.history.replaceState({}, "", url);
579
+ return;
580
+ }
581
+ this.history.pushState({}, "", url);
582
+ }
583
+ /**
584
+ *
585
+ * @param options
586
+ * @protected
587
+ */
588
+ async render(options = {}) {
589
+ const previous = options.previous ?? this.state.layers;
590
+ const url = options.url ?? this.url;
591
+ this.transitioning = { to: url };
592
+ const result = await this.router.render(url, {
593
+ previous,
594
+ state: this.state
595
+ });
596
+ if (result.redirect) {
597
+ return await this.render({ url: result.redirect });
598
+ }
599
+ this.transitioning = void 0;
600
+ return { url };
601
+ }
602
+ /**
603
+ * Get embedded layers from the server.
604
+ *
605
+ * @protected
606
+ */
607
+ getEmbeddedCache() {
608
+ try {
609
+ if ("__ssr" in window && typeof window.__ssr === "object") {
610
+ return window.__ssr;
611
+ }
612
+ } catch (error) {
613
+ console.error(error);
614
+ }
615
+ }
616
+ /**
617
+ *
618
+ * @protected
619
+ */
620
+ getRootElement() {
621
+ const root = this.document.getElementById("root");
622
+ if (root) {
623
+ return root;
624
+ }
625
+ const div = this.document.createElement("div");
626
+ div.id = "root";
627
+ this.document.body.appendChild(div);
628
+ return div;
629
+ }
630
+ // -------------------------------------------------------------------------------------------------------------------
631
+ /**
632
+ *
633
+ * @protected
634
+ */
635
+ ready = core.$hook({
636
+ name: "ready",
637
+ handler: async () => {
638
+ const cache = this.getEmbeddedCache();
639
+ const previous = cache?.layers ?? [];
640
+ const session = cache?.session ?? await this.client.of().session();
641
+ await this.render({ previous });
642
+ const element = this.router.root(this.state, session);
643
+ if (previous.length > 0) {
644
+ this.root = client.hydrateRoot(this.getRootElement(), element);
645
+ this.log.info("Hydrated root element");
646
+ } else {
647
+ this.root = client.createRoot(this.getRootElement());
648
+ this.root.render(element);
649
+ this.log.info("Created root element");
650
+ }
651
+ window.addEventListener("popstate", () => {
652
+ this.render();
653
+ });
654
+ }
655
+ });
656
+ /**
657
+ *
658
+ * @protected
659
+ */
660
+ stop = core.$hook({
661
+ name: "stop",
662
+ handler: async () => {
663
+ if (this.root) {
664
+ this.root.unmount();
665
+ this.log.info("Unmounted root element");
666
+ }
667
+ }
668
+ });
669
+ }
670
+
671
+ class RouterHookApi {
672
+ constructor(state, layer, browser) {
673
+ this.state = state;
674
+ this.layer = layer;
675
+ this.browser = browser;
676
+ }
677
+ /**
678
+ *
679
+ */
680
+ get current() {
681
+ return this.state;
682
+ }
683
+ /**
684
+ *
685
+ */
686
+ get pathname() {
687
+ return this.state.pathname;
688
+ }
689
+ /**
690
+ *
691
+ */
692
+ get query() {
693
+ const query = {};
694
+ for (const [key, value] of new URLSearchParams(
695
+ this.state.search
696
+ ).entries()) {
697
+ query[key] = String(value);
698
+ }
699
+ return query;
700
+ }
701
+ /**
702
+ *
703
+ */
704
+ async back() {
705
+ this.browser?.history.back();
706
+ }
707
+ /**
708
+ *
709
+ */
710
+ async forward() {
711
+ this.browser?.history.forward();
712
+ }
713
+ /**
714
+ *
715
+ * @param props
716
+ */
717
+ async invalidate(props) {
718
+ await this.browser?.invalidate(props);
719
+ }
720
+ /**
721
+ * Create a valid href for the given pathname.
722
+ *
723
+ * @param pathname
724
+ * @param layer
725
+ */
726
+ createHref(pathname, layer = this.layer) {
727
+ if (typeof pathname === "object") {
728
+ pathname = pathname.options.path ?? "";
729
+ }
730
+ return pathname.startsWith("/") ? pathname : `${layer.path}/${pathname}`.replace(/\/\/+/g, "/");
731
+ }
732
+ /**
733
+ *
734
+ * @param path
735
+ * @param options
736
+ */
737
+ async go(path, options = {}) {
738
+ return await this.browser?.go(this.createHref(path, this.layer), options);
739
+ }
740
+ /**
741
+ *
742
+ * @param path
743
+ */
744
+ createAnchorProps(path) {
745
+ const href = this.createHref(path, this.layer);
746
+ return {
747
+ href,
748
+ onClick: (ev) => {
749
+ ev.stopPropagation();
750
+ ev.preventDefault();
751
+ this.go(path).catch(console.error);
752
+ }
753
+ };
754
+ }
755
+ /**
756
+ * Set query params.
757
+ *
758
+ * @param record
759
+ * @param options
760
+ */
761
+ setQueryParams(record, options = {}) {
762
+ const search = new URLSearchParams(
763
+ options.merge ? {
764
+ ...this.query,
765
+ ...record
766
+ } : {
767
+ ...record
768
+ }
769
+ ).toString();
770
+ const state = search ? `${this.pathname}?${search}` : this.pathname;
771
+ if (options.push) {
772
+ window.history.pushState({}, "", state);
773
+ } else {
774
+ window.history.replaceState({}, "", state);
775
+ }
776
+ }
777
+ }
778
+
779
+ const useRouter = () => {
780
+ const ctx = react.useContext(RouterContext);
781
+ const layer = react.useContext(RouterLayerContext);
782
+ if (!ctx || !layer) {
783
+ throw new Error("useRouter must be used within a RouterProvider");
784
+ }
785
+ return react.useMemo(
786
+ () => new RouterHookApi(
787
+ ctx.state,
788
+ layer,
789
+ ctx.alepha.isBrowser() ? ctx.alepha.get(ReactBrowserProvider) : void 0
790
+ ),
791
+ [ctx.router, layer]
792
+ );
793
+ };
794
+
795
+ const useActive = (path) => {
796
+ const router = useRouter();
797
+ const ctx = react.useContext(RouterContext);
798
+ const layer = react.useContext(RouterLayerContext);
799
+ if (!ctx || !layer) {
800
+ throw new Error("useRouter must be used within a RouterProvider");
801
+ }
802
+ let name;
803
+ if (typeof path === "object" && path.options.name) {
804
+ name = path.options.name;
805
+ }
806
+ const [current, setCurrent] = react.useState(ctx.state.pathname);
807
+ const href = react.useMemo(() => router.createHref(path, layer), [path, layer]);
808
+ const [isPending, setPending] = react.useState(false);
809
+ const isActive = current === href;
810
+ react.useEffect(
811
+ () => ctx.router.on("end", ({ pathname }) => setCurrent(pathname)),
812
+ []
813
+ );
814
+ return {
815
+ name,
816
+ isPending,
817
+ isActive,
818
+ anchorProps: {
819
+ href,
820
+ onClick: (ev) => {
821
+ ev.stopPropagation();
822
+ ev.preventDefault();
823
+ if (isActive) return;
824
+ if (isPending) return;
825
+ setPending(true);
826
+ router.go(href).then(() => {
827
+ setPending(false);
828
+ });
829
+ }
830
+ }
831
+ };
832
+ };
833
+
834
+ const useInject = (classEntry) => {
835
+ const ctx = react.useContext(RouterContext);
836
+ if (!ctx) {
837
+ throw new Error("useRouter must be used within a <RouterProvider>");
838
+ }
839
+ return ctx.alepha.get(classEntry);
840
+ };
841
+
842
+ const useClient = () => {
843
+ return useInject(server.HttpClient);
844
+ };
845
+
846
+ const useQueryParams = (schema, options = {}) => {
847
+ const ctx = react.useContext(RouterContext);
848
+ if (!ctx) {
849
+ throw new Error("useQueryParams must be used within a RouterProvider");
850
+ }
851
+ const key = options.key ?? "q";
852
+ const router = useRouter();
853
+ const querystring = router.query[key];
854
+ const [queryParams, setQueryParams] = react.useState(
855
+ decode(ctx.alepha, schema, router.query[key])
856
+ );
857
+ react.useEffect(() => {
858
+ setQueryParams(decode(ctx.alepha, schema, querystring));
859
+ }, [querystring]);
860
+ return [
861
+ queryParams,
862
+ (queryParams2) => {
863
+ setQueryParams(queryParams2);
864
+ router.setQueryParams(
865
+ { [key]: encode(ctx.alepha, schema, queryParams2) },
866
+ {
867
+ merge: true
868
+ }
869
+ );
870
+ }
871
+ ];
872
+ };
873
+ const encode = (alepha, schema, data) => {
874
+ return btoa(JSON.stringify(alepha.parse(schema, data)));
875
+ };
876
+ const decode = (alepha, schema, data) => {
877
+ try {
878
+ return alepha.parse(schema, JSON.parse(atob(decodeURIComponent(data))));
879
+ } catch (error) {
880
+ return {};
881
+ }
882
+ };
883
+
884
+ const useRouterEvents = (opts = {}) => {
885
+ const ctx = react.useContext(RouterContext);
886
+ const layer = react.useContext(RouterLayerContext);
887
+ if (!ctx || !layer) {
888
+ throw new Error("useRouter must be used within a RouterProvider");
889
+ }
890
+ react.useEffect(() => {
891
+ const subs = [];
892
+ const onBegin = opts.onBegin;
893
+ const onEnd = opts.onEnd;
894
+ const onError = opts.onError;
895
+ if (onBegin) {
896
+ subs.push(ctx.router.on("begin", onBegin));
897
+ }
898
+ if (onEnd) {
899
+ subs.push(ctx.router.on("end", onEnd));
900
+ }
901
+ if (onError) {
902
+ subs.push(ctx.router.on("error", onError));
903
+ }
904
+ return () => {
905
+ for (const sub of subs) {
906
+ sub();
907
+ }
908
+ };
909
+ }, []);
910
+ };
911
+
912
+ const useRouterState = () => {
913
+ const ctx = react.useContext(RouterContext);
914
+ const layer = react.useContext(RouterLayerContext);
915
+ if (!ctx || !layer) {
916
+ throw new Error("useRouter must be used within a RouterProvider");
917
+ }
918
+ const [state, setState] = react.useState(ctx.state);
919
+ react.useEffect(
920
+ () => ctx.router.on("end", (it) => {
921
+ setState({ ...it });
922
+ }),
923
+ []
924
+ );
925
+ return state;
926
+ };
927
+
928
+ exports.$page = $page;
929
+ exports.NestedView = NestedView;
930
+ exports.PageDescriptorProvider = PageDescriptorProvider;
931
+ exports.ReactBrowserProvider = ReactBrowserProvider;
932
+ exports.RedirectException = RedirectException;
933
+ exports.Router = Router;
934
+ exports.RouterContext = RouterContext;
935
+ exports.RouterHookApi = RouterHookApi;
936
+ exports.RouterLayerContext = RouterLayerContext;
937
+ exports.pageDescriptorKey = pageDescriptorKey;
938
+ exports.useActive = useActive;
939
+ exports.useClient = useClient;
940
+ exports.useInject = useInject;
941
+ exports.useQueryParams = useQueryParams;
942
+ exports.useRouter = useRouter;
943
+ exports.useRouterEvents = useRouterEvents;
944
+ exports.useRouterState = useRouterState;