xote 4.2.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.
- package/README.md +5 -5
- package/dist/xote.cjs +1 -1
- package/dist/xote.mjs +523 -613
- package/dist/xote.umd.js +1 -1
- package/package.json +7 -9
- package/rescript.json +30 -0
- package/src/Xote.res +23 -0
- package/src/Xote.res.mjs +88 -0
- package/src/Xote__Component.res +495 -0
- package/src/Xote__Component.res.mjs +487 -0
- package/src/Xote__JSX.res +305 -0
- package/src/Xote__JSX.res.mjs +244 -0
- package/src/Xote__Route.res +62 -0
- package/src/Xote__Route.res.mjs +56 -0
- package/src/Xote__Router.res +130 -0
- package/src/Xote__Router.res.mjs +116 -0
|
@@ -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 */
|