xote 4.3.0 → 4.4.0
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/package.json +6 -2
- 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 +677 -0
- package/src/Xote__JSX.res.mjs +356 -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,356 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Core__Option from "@rescript/core/src/Core__Option.res.mjs";
|
|
4
|
+
import * as Xote__Component from "./Xote__Component.res.mjs";
|
|
5
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
6
|
+
|
|
7
|
+
function jsx(component, props) {
|
|
8
|
+
return component(props);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function jsxs(component, props) {
|
|
12
|
+
return component(props);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function jsxKeyed(component, props, key, param) {
|
|
16
|
+
return component(props);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function jsxsKeyed(component, props, key, param) {
|
|
20
|
+
return component(props);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function jsxFragment(props) {
|
|
24
|
+
let child = props.children;
|
|
25
|
+
if (child !== undefined) {
|
|
26
|
+
return child;
|
|
27
|
+
} else {
|
|
28
|
+
return Xote__Component.fragment([]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let array = Xote__Component.fragment;
|
|
33
|
+
|
|
34
|
+
function $$null() {
|
|
35
|
+
return Xote__Component.text("");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function signal(s) {
|
|
39
|
+
return s;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function computed(f) {
|
|
43
|
+
return f;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function convertAttrValue(key, value) {
|
|
47
|
+
if (typeof value === "function") {
|
|
48
|
+
return Xote__Component.computedAttr(key, value);
|
|
49
|
+
} else if (typeof value === "object" && Core__Option.isSome(value.id)) {
|
|
50
|
+
return Xote__Component.signalAttr(key, value);
|
|
51
|
+
} else {
|
|
52
|
+
return Xote__Component.attr(key, value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function propsToAttrs(props) {
|
|
57
|
+
let attrs = [];
|
|
58
|
+
let v = props.id;
|
|
59
|
+
if (v !== undefined) {
|
|
60
|
+
attrs.push(convertAttrValue("id", Primitive_option.valFromOption(v)));
|
|
61
|
+
}
|
|
62
|
+
let v$1 = props.class;
|
|
63
|
+
if (v$1 !== undefined) {
|
|
64
|
+
attrs.push(convertAttrValue("class", Primitive_option.valFromOption(v$1)));
|
|
65
|
+
}
|
|
66
|
+
let v$2 = props.style;
|
|
67
|
+
if (v$2 !== undefined) {
|
|
68
|
+
attrs.push(convertAttrValue("style", Primitive_option.valFromOption(v$2)));
|
|
69
|
+
}
|
|
70
|
+
let v$3 = props.type;
|
|
71
|
+
if (v$3 !== undefined) {
|
|
72
|
+
attrs.push(convertAttrValue("type", Primitive_option.valFromOption(v$3)));
|
|
73
|
+
}
|
|
74
|
+
let v$4 = props.name;
|
|
75
|
+
if (v$4 !== undefined) {
|
|
76
|
+
attrs.push(convertAttrValue("name", Primitive_option.valFromOption(v$4)));
|
|
77
|
+
}
|
|
78
|
+
let v$5 = props.value;
|
|
79
|
+
if (v$5 !== undefined) {
|
|
80
|
+
attrs.push(convertAttrValue("value", Primitive_option.valFromOption(v$5)));
|
|
81
|
+
}
|
|
82
|
+
let v$6 = props.placeholder;
|
|
83
|
+
if (v$6 !== undefined) {
|
|
84
|
+
attrs.push(convertAttrValue("placeholder", Primitive_option.valFromOption(v$6)));
|
|
85
|
+
}
|
|
86
|
+
let match = props.disabled;
|
|
87
|
+
if (match !== undefined && match) {
|
|
88
|
+
attrs.push(Xote__Component.attr("disabled", "true"));
|
|
89
|
+
}
|
|
90
|
+
let match$1 = props.checked;
|
|
91
|
+
if (match$1 !== undefined && match$1) {
|
|
92
|
+
attrs.push(Xote__Component.attr("checked", "true"));
|
|
93
|
+
}
|
|
94
|
+
let match$2 = props.required;
|
|
95
|
+
if (match$2 !== undefined && match$2) {
|
|
96
|
+
attrs.push(Xote__Component.attr("required", "true"));
|
|
97
|
+
}
|
|
98
|
+
let match$3 = props.readOnly;
|
|
99
|
+
if (match$3 !== undefined && match$3) {
|
|
100
|
+
attrs.push(Xote__Component.attr("readonly", "true"));
|
|
101
|
+
}
|
|
102
|
+
let v$7 = props.maxLength;
|
|
103
|
+
if (v$7 !== undefined) {
|
|
104
|
+
attrs.push(Xote__Component.attr("maxlength", v$7.toString()));
|
|
105
|
+
}
|
|
106
|
+
let v$8 = props.minLength;
|
|
107
|
+
if (v$8 !== undefined) {
|
|
108
|
+
attrs.push(Xote__Component.attr("minlength", v$8.toString()));
|
|
109
|
+
}
|
|
110
|
+
let v$9 = props.min;
|
|
111
|
+
if (v$9 !== undefined) {
|
|
112
|
+
attrs.push(convertAttrValue("min", Primitive_option.valFromOption(v$9)));
|
|
113
|
+
}
|
|
114
|
+
let v$10 = props.max;
|
|
115
|
+
if (v$10 !== undefined) {
|
|
116
|
+
attrs.push(convertAttrValue("max", Primitive_option.valFromOption(v$10)));
|
|
117
|
+
}
|
|
118
|
+
let v$11 = props.step;
|
|
119
|
+
if (v$11 !== undefined) {
|
|
120
|
+
attrs.push(convertAttrValue("step", Primitive_option.valFromOption(v$11)));
|
|
121
|
+
}
|
|
122
|
+
let v$12 = props.pattern;
|
|
123
|
+
if (v$12 !== undefined) {
|
|
124
|
+
attrs.push(convertAttrValue("pattern", Primitive_option.valFromOption(v$12)));
|
|
125
|
+
}
|
|
126
|
+
let v$13 = props.autoComplete;
|
|
127
|
+
if (v$13 !== undefined) {
|
|
128
|
+
attrs.push(convertAttrValue("autocomplete", Primitive_option.valFromOption(v$13)));
|
|
129
|
+
}
|
|
130
|
+
let match$4 = props.multiple;
|
|
131
|
+
if (match$4 !== undefined && match$4) {
|
|
132
|
+
attrs.push(Xote__Component.attr("multiple", "true"));
|
|
133
|
+
}
|
|
134
|
+
let v$14 = props.accept;
|
|
135
|
+
if (v$14 !== undefined) {
|
|
136
|
+
attrs.push(convertAttrValue("accept", Primitive_option.valFromOption(v$14)));
|
|
137
|
+
}
|
|
138
|
+
let v$15 = props.rows;
|
|
139
|
+
if (v$15 !== undefined) {
|
|
140
|
+
attrs.push(Xote__Component.attr("rows", v$15.toString()));
|
|
141
|
+
}
|
|
142
|
+
let v$16 = props.cols;
|
|
143
|
+
if (v$16 !== undefined) {
|
|
144
|
+
attrs.push(Xote__Component.attr("cols", v$16.toString()));
|
|
145
|
+
}
|
|
146
|
+
let v$17 = props.for;
|
|
147
|
+
if (v$17 !== undefined) {
|
|
148
|
+
attrs.push(convertAttrValue("for", Primitive_option.valFromOption(v$17)));
|
|
149
|
+
}
|
|
150
|
+
let v$18 = props.href;
|
|
151
|
+
if (v$18 !== undefined) {
|
|
152
|
+
attrs.push(convertAttrValue("href", Primitive_option.valFromOption(v$18)));
|
|
153
|
+
}
|
|
154
|
+
let v$19 = props.target;
|
|
155
|
+
if (v$19 !== undefined) {
|
|
156
|
+
attrs.push(convertAttrValue("target", Primitive_option.valFromOption(v$19)));
|
|
157
|
+
}
|
|
158
|
+
let v$20 = props.src;
|
|
159
|
+
if (v$20 !== undefined) {
|
|
160
|
+
attrs.push(convertAttrValue("src", Primitive_option.valFromOption(v$20)));
|
|
161
|
+
}
|
|
162
|
+
let v$21 = props.alt;
|
|
163
|
+
if (v$21 !== undefined) {
|
|
164
|
+
attrs.push(convertAttrValue("alt", Primitive_option.valFromOption(v$21)));
|
|
165
|
+
}
|
|
166
|
+
let v$22 = props.width;
|
|
167
|
+
if (v$22 !== undefined) {
|
|
168
|
+
attrs.push(convertAttrValue("width", Primitive_option.valFromOption(v$22)));
|
|
169
|
+
}
|
|
170
|
+
let v$23 = props.height;
|
|
171
|
+
if (v$23 !== undefined) {
|
|
172
|
+
attrs.push(convertAttrValue("height", Primitive_option.valFromOption(v$23)));
|
|
173
|
+
}
|
|
174
|
+
let v$24 = props.role;
|
|
175
|
+
if (v$24 !== undefined) {
|
|
176
|
+
attrs.push(convertAttrValue("role", Primitive_option.valFromOption(v$24)));
|
|
177
|
+
}
|
|
178
|
+
let v$25 = props.tabIndex;
|
|
179
|
+
if (v$25 !== undefined) {
|
|
180
|
+
attrs.push(Xote__Component.attr("tabindex", v$25.toString()));
|
|
181
|
+
}
|
|
182
|
+
let v$26 = props["aria-label"];
|
|
183
|
+
if (v$26 !== undefined) {
|
|
184
|
+
attrs.push(convertAttrValue("aria-label", Primitive_option.valFromOption(v$26)));
|
|
185
|
+
}
|
|
186
|
+
let match$5 = props["aria-hidden"];
|
|
187
|
+
if (match$5 !== undefined) {
|
|
188
|
+
if (match$5) {
|
|
189
|
+
attrs.push(Xote__Component.attr("aria-hidden", "true"));
|
|
190
|
+
} else {
|
|
191
|
+
attrs.push(Xote__Component.attr("aria-hidden", "false"));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
let match$6 = props["aria-expanded"];
|
|
195
|
+
if (match$6 !== undefined) {
|
|
196
|
+
if (match$6) {
|
|
197
|
+
attrs.push(Xote__Component.attr("aria-expanded", "true"));
|
|
198
|
+
} else {
|
|
199
|
+
attrs.push(Xote__Component.attr("aria-expanded", "false"));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
let match$7 = props["aria-selected"];
|
|
203
|
+
if (match$7 !== undefined) {
|
|
204
|
+
if (match$7) {
|
|
205
|
+
attrs.push(Xote__Component.attr("aria-selected", "true"));
|
|
206
|
+
} else {
|
|
207
|
+
attrs.push(Xote__Component.attr("aria-selected", "false"));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
let _dataObj = props.data;
|
|
211
|
+
if (_dataObj !== undefined) {
|
|
212
|
+
((Object.entries(_dataObj).forEach(([key, value]) => {
|
|
213
|
+
attrs.push(convertAttrValue("data-" + key, value))
|
|
214
|
+
})));
|
|
215
|
+
}
|
|
216
|
+
return attrs;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function propsToEvents(props) {
|
|
220
|
+
let events = [];
|
|
221
|
+
let handler = props.onClick;
|
|
222
|
+
if (handler !== undefined) {
|
|
223
|
+
events.push([
|
|
224
|
+
"click",
|
|
225
|
+
handler
|
|
226
|
+
]);
|
|
227
|
+
}
|
|
228
|
+
let handler$1 = props.onInput;
|
|
229
|
+
if (handler$1 !== undefined) {
|
|
230
|
+
events.push([
|
|
231
|
+
"input",
|
|
232
|
+
handler$1
|
|
233
|
+
]);
|
|
234
|
+
}
|
|
235
|
+
let handler$2 = props.onChange;
|
|
236
|
+
if (handler$2 !== undefined) {
|
|
237
|
+
events.push([
|
|
238
|
+
"change",
|
|
239
|
+
handler$2
|
|
240
|
+
]);
|
|
241
|
+
}
|
|
242
|
+
let handler$3 = props.onSubmit;
|
|
243
|
+
if (handler$3 !== undefined) {
|
|
244
|
+
events.push([
|
|
245
|
+
"submit",
|
|
246
|
+
handler$3
|
|
247
|
+
]);
|
|
248
|
+
}
|
|
249
|
+
let handler$4 = props.onFocus;
|
|
250
|
+
if (handler$4 !== undefined) {
|
|
251
|
+
events.push([
|
|
252
|
+
"focus",
|
|
253
|
+
handler$4
|
|
254
|
+
]);
|
|
255
|
+
}
|
|
256
|
+
let handler$5 = props.onBlur;
|
|
257
|
+
if (handler$5 !== undefined) {
|
|
258
|
+
events.push([
|
|
259
|
+
"blur",
|
|
260
|
+
handler$5
|
|
261
|
+
]);
|
|
262
|
+
}
|
|
263
|
+
let handler$6 = props.onKeyDown;
|
|
264
|
+
if (handler$6 !== undefined) {
|
|
265
|
+
events.push([
|
|
266
|
+
"keydown",
|
|
267
|
+
handler$6
|
|
268
|
+
]);
|
|
269
|
+
}
|
|
270
|
+
let handler$7 = props.onKeyUp;
|
|
271
|
+
if (handler$7 !== undefined) {
|
|
272
|
+
events.push([
|
|
273
|
+
"keyup",
|
|
274
|
+
handler$7
|
|
275
|
+
]);
|
|
276
|
+
}
|
|
277
|
+
let handler$8 = props.onMouseEnter;
|
|
278
|
+
if (handler$8 !== undefined) {
|
|
279
|
+
events.push([
|
|
280
|
+
"mouseenter",
|
|
281
|
+
handler$8
|
|
282
|
+
]);
|
|
283
|
+
}
|
|
284
|
+
let handler$9 = props.onMouseLeave;
|
|
285
|
+
if (handler$9 !== undefined) {
|
|
286
|
+
events.push([
|
|
287
|
+
"mouseleave",
|
|
288
|
+
handler$9
|
|
289
|
+
]);
|
|
290
|
+
}
|
|
291
|
+
return events;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function getChildren(props) {
|
|
295
|
+
let child = props.children;
|
|
296
|
+
if (child !== undefined) {
|
|
297
|
+
if (child.TAG === "Fragment") {
|
|
298
|
+
return child._0;
|
|
299
|
+
} else {
|
|
300
|
+
return [child];
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function createElement(tag, props) {
|
|
308
|
+
return {
|
|
309
|
+
TAG: "Element",
|
|
310
|
+
tag: tag,
|
|
311
|
+
attrs: propsToAttrs(props),
|
|
312
|
+
events: propsToEvents(props),
|
|
313
|
+
children: getChildren(props)
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let jsx$1 = createElement;
|
|
318
|
+
|
|
319
|
+
let jsxs$1 = createElement;
|
|
320
|
+
|
|
321
|
+
function jsxKeyed$1(tag, props, key, param) {
|
|
322
|
+
return createElement(tag, props);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function jsxsKeyed$1(tag, props, key, param) {
|
|
326
|
+
return createElement(tag, props);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let Elements = {
|
|
330
|
+
signal: signal,
|
|
331
|
+
computed: computed,
|
|
332
|
+
convertAttrValue: convertAttrValue,
|
|
333
|
+
propsToAttrs: propsToAttrs,
|
|
334
|
+
propsToEvents: propsToEvents,
|
|
335
|
+
getChildren: getChildren,
|
|
336
|
+
createElement: createElement,
|
|
337
|
+
jsx: jsx$1,
|
|
338
|
+
jsxs: jsxs$1,
|
|
339
|
+
jsxKeyed: jsxKeyed$1,
|
|
340
|
+
jsxsKeyed: jsxsKeyed$1
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
let Component;
|
|
344
|
+
|
|
345
|
+
export {
|
|
346
|
+
Component,
|
|
347
|
+
jsx,
|
|
348
|
+
jsxs,
|
|
349
|
+
jsxKeyed,
|
|
350
|
+
jsxsKeyed,
|
|
351
|
+
jsxFragment,
|
|
352
|
+
array,
|
|
353
|
+
$$null,
|
|
354
|
+
Elements,
|
|
355
|
+
}
|
|
356
|
+
/* Xote__Component Not a pure module */
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Pure route matching logic (no signals, no DOM)
|
|
2
|
+
|
|
3
|
+
// Route parameter map
|
|
4
|
+
type params = Dict.t<string>
|
|
5
|
+
|
|
6
|
+
// Match result
|
|
7
|
+
type matchResult =
|
|
8
|
+
| Match(params)
|
|
9
|
+
| NoMatch
|
|
10
|
+
|
|
11
|
+
// Route segment - either static or dynamic parameter
|
|
12
|
+
type segment =
|
|
13
|
+
| Static(string)
|
|
14
|
+
| Param(string)
|
|
15
|
+
|
|
16
|
+
// Parse a route pattern like "/users/:id/posts/:postId"
|
|
17
|
+
// Returns array of segments, where dynamic segments are marked
|
|
18
|
+
let parsePattern = (pattern: string): array<segment> => {
|
|
19
|
+
pattern
|
|
20
|
+
->String.split("/")
|
|
21
|
+
->Array.filterMap(seg => {
|
|
22
|
+
if seg == "" {
|
|
23
|
+
None
|
|
24
|
+
} else if String.startsWith(seg, ":") {
|
|
25
|
+
Some(Param(String.sliceToEnd(seg, ~start=1)))
|
|
26
|
+
} else {
|
|
27
|
+
Some(Static(seg))
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Match a pathname against a parsed pattern
|
|
33
|
+
let matchPath = (pattern: array<segment>, pathname: string): matchResult => {
|
|
34
|
+
let pathSegments =
|
|
35
|
+
pathname
|
|
36
|
+
->String.split("/")
|
|
37
|
+
->Array.filter(s => s != "")
|
|
38
|
+
|
|
39
|
+
// Length must match
|
|
40
|
+
if Array.length(pattern) != Array.length(pathSegments) {
|
|
41
|
+
NoMatch
|
|
42
|
+
} else {
|
|
43
|
+
let params = Dict.make()
|
|
44
|
+
let matches = pattern->Array.everyWithIndex((seg, idx) => {
|
|
45
|
+
let pathSeg = pathSegments->Array.getUnsafe(idx)
|
|
46
|
+
switch seg {
|
|
47
|
+
| Static(expected) => pathSeg == expected
|
|
48
|
+
| Param(name) => {
|
|
49
|
+
params->Dict.set(name, pathSeg)
|
|
50
|
+
true
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
matches ? Match(params) : NoMatch
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Convenience: match a pattern string against pathname
|
|
60
|
+
let match = (pattern: string, pathname: string): matchResult => {
|
|
61
|
+
matchPath(parsePattern(pattern), pathname)
|
|
62
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Core__Array from "@rescript/core/src/Core__Array.res.mjs";
|
|
4
|
+
|
|
5
|
+
function parsePattern(pattern) {
|
|
6
|
+
return Core__Array.filterMap(pattern.split("/"), seg => {
|
|
7
|
+
if (seg === "") {
|
|
8
|
+
return;
|
|
9
|
+
} else if (seg.startsWith(":")) {
|
|
10
|
+
return {
|
|
11
|
+
TAG: "Param",
|
|
12
|
+
_0: seg.slice(1)
|
|
13
|
+
};
|
|
14
|
+
} else {
|
|
15
|
+
return {
|
|
16
|
+
TAG: "Static",
|
|
17
|
+
_0: seg
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function matchPath(pattern, pathname) {
|
|
24
|
+
let pathSegments = pathname.split("/").filter(s => s !== "");
|
|
25
|
+
if (pattern.length !== pathSegments.length) {
|
|
26
|
+
return "NoMatch";
|
|
27
|
+
}
|
|
28
|
+
let params = {};
|
|
29
|
+
let matches = pattern.every((seg, idx) => {
|
|
30
|
+
let pathSeg = pathSegments[idx];
|
|
31
|
+
if (seg.TAG === "Static") {
|
|
32
|
+
return pathSeg === seg._0;
|
|
33
|
+
}
|
|
34
|
+
params[seg._0] = pathSeg;
|
|
35
|
+
return true;
|
|
36
|
+
});
|
|
37
|
+
if (matches) {
|
|
38
|
+
return {
|
|
39
|
+
TAG: "Match",
|
|
40
|
+
_0: params
|
|
41
|
+
};
|
|
42
|
+
} else {
|
|
43
|
+
return "NoMatch";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function match(pattern, pathname) {
|
|
48
|
+
return matchPath(parsePattern(pattern), pathname);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
parsePattern,
|
|
53
|
+
matchPath,
|
|
54
|
+
match,
|
|
55
|
+
}
|
|
56
|
+
/* No side effect */
|
|
@@ -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 */
|