xote 4.3.0 → 4.3.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,130 @@
1
+ open Signals
2
+ module Component = Xote__Component
3
+ module Route = Xote__Route
4
+
5
+ // Browser location type
6
+ type location = {
7
+ pathname: string,
8
+ search: string,
9
+ hash: string,
10
+ }
11
+
12
+ // Global location signal - the core router state
13
+ let location: Signal.t<location> = Signal.make({
14
+ pathname: "/",
15
+ search: "",
16
+ hash: "",
17
+ })
18
+
19
+ // External bindings for History API
20
+ type historyState = {.}
21
+
22
+ @val @scope(("window", "history"))
23
+ external pushState: (historyState, string, string) => unit = "pushState"
24
+
25
+ @val @scope(("window", "history"))
26
+ external replaceState: (historyState, string, string) => unit = "replaceState"
27
+
28
+ @val @scope("window")
29
+ external addEventListener: (string, Dom.event => unit) => unit = "addEventListener"
30
+
31
+ @val @scope("window")
32
+ external removeEventListener: (string, Dom.event => unit) => unit = "removeEventListener"
33
+
34
+ // Parse current browser location from window.location
35
+ let getCurrentLocation = (): location => {
36
+ pathname: %raw(`window.location.pathname`),
37
+ search: %raw(`window.location.search`),
38
+ hash: %raw(`window.location.hash`),
39
+ }
40
+
41
+ // Initialize router - call this once at app start
42
+ let init = (): unit => {
43
+ // Set initial location from browser
44
+ Signal.set(location, getCurrentLocation())
45
+
46
+ // Listen for popstate (back/forward buttons)
47
+ let handlePopState = (_evt: Dom.event) => {
48
+ Signal.set(location, getCurrentLocation())
49
+ }
50
+
51
+ addEventListener("popstate", handlePopState)
52
+
53
+ // Note: No cleanup needed for SPA scenarios
54
+ // If cleanup is needed, return a disposer function
55
+ }
56
+
57
+ // Imperative navigation - push new history entry
58
+ let push = (pathname: string, ~search: string="", ~hash: string="", ()): unit => {
59
+ let newLocation = {pathname, search, hash}
60
+ let url = pathname ++ search ++ hash
61
+ let state: historyState = %raw("{}")
62
+ pushState(state, "", url)
63
+ Signal.set(location, newLocation)
64
+ }
65
+
66
+ // Imperative navigation - replace current history entry
67
+ let replace = (pathname: string, ~search: string="", ~hash: string="", ()): unit => {
68
+ let newLocation = {pathname, search, hash}
69
+ let url = pathname ++ search ++ hash
70
+ let state: historyState = %raw("{}")
71
+ replaceState(state, "", url)
72
+ Signal.set(location, newLocation)
73
+ }
74
+
75
+ // Route definition for routes() component
76
+ type routeConfig = {
77
+ pattern: string,
78
+ render: Route.params => Component.node,
79
+ }
80
+
81
+ // Single route component - renders if pattern matches
82
+ let route = (pattern: string, render: Route.params => Component.node): Component.node => {
83
+ let signal = Computed.make(() => {
84
+ let loc = Signal.get(location)
85
+ switch Route.match(pattern, loc.pathname) {
86
+ | Match(params) => [render(params)]
87
+ | NoMatch => []
88
+ }
89
+ })
90
+ Component.signalFragment(signal)
91
+ }
92
+
93
+ // Routes component - renders first matching route
94
+ let routes = (configs: array<routeConfig>): Component.node => {
95
+ let signal = Computed.make(() => {
96
+ let loc = Signal.get(location)
97
+ let matched = configs->Array.findMap(config => {
98
+ switch Route.match(config.pattern, loc.pathname) {
99
+ | Match(params) => Some(config.render(params))
100
+ | NoMatch => None
101
+ }
102
+ })
103
+
104
+ switch matched {
105
+ | Some(node) => [node]
106
+ | None => [] // No matching route - render nothing
107
+ }
108
+ })
109
+ Component.signalFragment(signal)
110
+ }
111
+
112
+ // Link component - handles navigation without page reload
113
+ let link = (
114
+ ~to: string,
115
+ ~attrs: array<(string, Component.attrValue)>=[],
116
+ ~children: array<Component.node>=[],
117
+ (),
118
+ ): Component.node => {
119
+ let handleClick = (_evt: Dom.event) => {
120
+ %raw(`_evt.preventDefault()`)
121
+ push(to, ())
122
+ }
123
+
124
+ Component.a(
125
+ ~attrs=Array.concat(attrs, [Component.attr("href", to)]),
126
+ ~events=[("click", handleClick)],
127
+ ~children,
128
+ (),
129
+ )
130
+ }
@@ -0,0 +1,116 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Signals from "rescript-signals/src/Signals.res.mjs";
4
+ import * as Core__Array from "@rescript/core/src/Core__Array.res.mjs";
5
+ import * as Xote__Route from "./Xote__Route.res.mjs";
6
+ import * as Xote__Component from "./Xote__Component.res.mjs";
7
+
8
+ let location = Signals.Signal.make({
9
+ pathname: "/",
10
+ search: "",
11
+ hash: ""
12
+ }, undefined, undefined);
13
+
14
+ function getCurrentLocation() {
15
+ return {
16
+ pathname: window.location.pathname,
17
+ search: window.location.search,
18
+ hash: window.location.hash
19
+ };
20
+ }
21
+
22
+ function init() {
23
+ Signals.Signal.set(location, getCurrentLocation());
24
+ let handlePopState = _evt => Signals.Signal.set(location, getCurrentLocation());
25
+ window.addEventListener("popstate", handlePopState);
26
+ }
27
+
28
+ function push(pathname, searchOpt, hashOpt, param) {
29
+ let search = searchOpt !== undefined ? searchOpt : "";
30
+ let hash = hashOpt !== undefined ? hashOpt : "";
31
+ let newLocation = {
32
+ pathname: pathname,
33
+ search: search,
34
+ hash: hash
35
+ };
36
+ let url = pathname + search + hash;
37
+ let state = {};
38
+ window.history.pushState(state, "", url);
39
+ Signals.Signal.set(location, newLocation);
40
+ }
41
+
42
+ function replace(pathname, searchOpt, hashOpt, param) {
43
+ let search = searchOpt !== undefined ? searchOpt : "";
44
+ let hash = hashOpt !== undefined ? hashOpt : "";
45
+ let newLocation = {
46
+ pathname: pathname,
47
+ search: search,
48
+ hash: hash
49
+ };
50
+ let url = pathname + search + hash;
51
+ let state = {};
52
+ window.history.replaceState(state, "", url);
53
+ Signals.Signal.set(location, newLocation);
54
+ }
55
+
56
+ function route(pattern, render) {
57
+ return Xote__Component.signalFragment(Signals.Computed.make(() => {
58
+ let loc = Signals.Signal.get(location);
59
+ let params = Xote__Route.match(pattern, loc.pathname);
60
+ if (typeof params !== "object") {
61
+ return [];
62
+ } else {
63
+ return [render(params._0)];
64
+ }
65
+ }, undefined));
66
+ }
67
+
68
+ function routes(configs) {
69
+ return Xote__Component.signalFragment(Signals.Computed.make(() => {
70
+ let loc = Signals.Signal.get(location);
71
+ let matched = Core__Array.findMap(configs, config => {
72
+ let params = Xote__Route.match(config.pattern, loc.pathname);
73
+ if (typeof params !== "object") {
74
+ return;
75
+ } else {
76
+ return config.render(params._0);
77
+ }
78
+ });
79
+ if (matched !== undefined) {
80
+ return [matched];
81
+ } else {
82
+ return [];
83
+ }
84
+ }, undefined));
85
+ }
86
+
87
+ function link(to, attrsOpt, childrenOpt, param) {
88
+ let attrs = attrsOpt !== undefined ? attrsOpt : [];
89
+ let children = childrenOpt !== undefined ? childrenOpt : [];
90
+ let handleClick = _evt => {
91
+ ((_evt.preventDefault()));
92
+ push(to, undefined, undefined, undefined);
93
+ };
94
+ return Xote__Component.a(attrs.concat([Xote__Component.attr("href", to)]), [[
95
+ "click",
96
+ handleClick
97
+ ]], children, undefined);
98
+ }
99
+
100
+ let Component;
101
+
102
+ let Route;
103
+
104
+ export {
105
+ Component,
106
+ Route,
107
+ location,
108
+ getCurrentLocation,
109
+ init,
110
+ push,
111
+ replace,
112
+ route,
113
+ routes,
114
+ link,
115
+ }
116
+ /* location Not a pure module */